Typescript migration part 2 (#261)

Converts everything to typescript, adding `@ts-expect-error`s where
necessary. Also makes some progress converting elements to use
decorators, but we're not fully there yet — I think it'll make sense to
continue to do that in future PRs, rather than doing it all in one go,
just so that we can keep up with the dev branch.
This commit is contained in:
Emma Segal-Grossman
2023-11-29 15:53:48 -05:00
committed by emma
parent e9438eb724
commit ad38694598
28 changed files with 1141 additions and 613 deletions
+425
View File
@@ -0,0 +1,425 @@
import { LitElement, html, css } from "lit";
import { wrapCss } from "./misc";
import prettyBytes from "pretty-bytes";
import { parseURLSchemeHostPath } from "./pageutils";
import { property } from "lit/decorators.js";
import { LoadInfo } from "./item";
// ===========================================================================
class Loader extends LitElement {
@property({ type: String }) sourceUrl?: string;
@property({ type: Object }) loadInfo: LoadInfo | null = null;
@property({ type: String }) state = "waiting";
@property({ type: Number }) progress = 0;
@property({ type: Number }) percent = 0;
@property({ type: Number }) currentSize = 0;
@property({ type: Number }) totalSize = 0;
@property({ type: String }) error?: string;
@property({ type: Number }) total = 0;
@property({ type: String }) status?: string;
@property({ type: String }) coll = "";
@property({ type: String }) embed?: string;
@property({ type: Boolean }) tryFileHandle = !!window.showOpenFilePicker;
@property({ type: Boolean }) errorAllowRetry = false;
@property({ type: String }) extraMsg?: string;
@property({ type: String }) swName?: string;
pingInterval: number | NodeJS.Timer = 0;
fileHandle: FileSystemHandle | null = null;
noWebWorker = false;
worker?: Worker | null;
// Google Drive
_gdWait?: Promise<LoadInfo>;
_gdResolve!: (value: LoadInfo | PromiseLike<LoadInfo>) => void;
firstUpdated() {
this.initMessages();
//this.doLoad();
}
initMessages() {
this.noWebWorker = Boolean(this.loadInfo && this.loadInfo.noWebWorker);
if (!this.noWebWorker) {
this.worker = new Worker(this.swName!);
} else {
if (!navigator.serviceWorker) {
return;
}
// TODO figure out if this is okay?
this.worker = navigator.serviceWorker as unknown as Worker;
}
this.worker.addEventListener("message", (event) => {
switch (event.data.msg_type) {
case "collProgress":
if (event.data.name === this.coll) {
this.percent = event.data.percent;
if (event.data.error) {
this.error = event.data.error;
this.state = "errored";
this.errorAllowRetry = true;
this.fileHandle = event.data.fileHandle;
if (this.error === "missing_local_file") {
this.tryFileHandle = false;
} else if (
this.error === "permission_needed" &&
event.data.fileHandle
) {
this.state = "permission_needed";
break;
}
}
if (event.data.currentSize && event.data.totalSize) {
this.currentSize = event.data.currentSize;
this.totalSize = event.data.totalSize;
}
this.extraMsg = event.data.extraMsg;
}
break;
case "collAdded":
if (event.data.name === this.coll) {
if (!this.total) {
this.total = 100;
}
this.progress = this.total;
this.percent = 100;
this.dispatchEvent(
new CustomEvent("coll-loaded", { detail: event.data }),
);
if (!this.noWebWorker) {
this.worker?.terminate();
} else {
if (this.pingInterval) {
clearInterval(this.pingInterval);
}
}
this.worker = null;
}
break;
}
});
}
async doLoad() {
let sourceUrl = this.sourceUrl;
let source: LoadInfo | null = null;
this.percent = this.currentSize = this.totalSize = 0;
// const noSWError = getSWErrorMsg();
if (this.loadInfo && this.loadInfo.swError) {
this.state = "errored";
this.error = this.loadInfo.swError;
this.errorAllowRetry = false;
return;
}
// custom protocol handlers here...
try {
const { scheme, host, path } = parseURLSchemeHostPath(sourceUrl);
switch (scheme) {
case "googledrive":
this.state = "googledrive";
source = (await this.googledriveInit()) ?? null;
break;
case "s3":
source = {
sourceUrl,
loadUrl: `https://${host}.s3.amazonaws.com${path}`,
name: this.sourceUrl,
};
break;
case "file":
if (!this.loadInfo && !this.tryFileHandle) {
this.state = "errored";
this.error = `\
File URLs can not be entered directly or shared.
You can select a file to upload from the main page by clicking the 'Choose File...' button.`;
this.errorAllowRetry = false;
return;
}
source = this.loadInfo;
break;
case "proxy":
sourceUrl = "proxy:" + sourceUrl!.slice("proxy://".length);
break;
}
} catch (e) {
console.log(e);
}
if (!source) {
source = { sourceUrl };
}
this.state = "started";
let type = undefined;
let extraConfig: LoadInfo["extraConfig"] = undefined;
if (this.loadInfo) {
source.newFullImport = this.loadInfo.newFullImport;
source.loadEager = this.loadInfo.loadEager;
source.noCache = this.loadInfo.noCache;
if (this.loadInfo.extraConfig) {
extraConfig = this.loadInfo.extraConfig;
}
// todo: too special case?
if (
sourceUrl!.startsWith("proxy:") &&
extraConfig &&
extraConfig.recording
) {
// @ts-expect-error - TS2322 - Type '"recordingproxy"' is not assignable to type 'undefined'.
type = "recordingproxy";
}
}
const msg = {
msg_type: "addColl",
name: this.coll,
extraConfig,
type,
skipExisting: true,
file: source,
};
if (!navigator.serviceWorker.controller) {
await new Promise((resolve) => {
navigator.serviceWorker.addEventListener("controllerchange", () =>
// @ts-expect-error - TS2794 - Expected 1 arguments, but got 0. Did you forget to include 'void' in your type argument to 'Promise'?
resolve(),
);
});
}
if (this.worker) {
if (!this.noWebWorker) {
this.worker.postMessage(msg);
} else {
// @ts-expect-error - TS2531 - Object is possibly 'null'.
navigator.serviceWorker.controller.postMessage(msg);
// ping service worker with messages to avoid shutdown while loading
// (mostly for Firefox)
this.pingInterval = setInterval(() => {
// @ts-expect-error - TS2531 - Object is possibly 'null'.
navigator.serviceWorker.controller.postMessage({ msg_type: "ping" });
}, 15000);
}
}
}
googledriveInit() {
this._gdWait = new Promise((resolve) => (this._gdResolve = resolve));
return this._gdWait;
}
async onLoadReady(event) {
if (this._gdResolve) {
//const digest = await digestMessage(url, 'SHA-256');
//this.coll = "id-" + digest.slice(0, 12);
this._gdResolve(event.detail);
}
}
onCancel() {
if (!this.worker) {
return;
}
const msg = { msg_type: "cancelLoad", name: this.coll };
if (!this.noWebWorker) {
this.worker.postMessage(msg);
return;
}
if (navigator.serviceWorker && navigator.serviceWorker.controller) {
navigator.serviceWorker.controller.postMessage(msg);
if (this.pingInterval) {
clearInterval(this.pingInterval);
}
}
}
updated(changedProperties) {
if (
(this.sourceUrl && changedProperties.has("sourceUrl")) ||
changedProperties.has("tryFileHandle")
) {
this.doLoad();
}
}
static get styles() {
return wrapCss(css`
:host {
height: 100%;
display: flex;
}
.logo {
width: 96px;
height: 96px;
margin: 1em;
flex-grow: 1;
}
.progress-div {
position: relative;
width: 400px !important;
}
.progress-label {
position: absolute;
top: 0;
left: 50%;
transform: translateX(-50%);
font-size: calc(1.5rem / 1.5);
line-height: 1.5rem;
}
.loaded-prog {
margin-bottom: 1em;
}
.error {
white-space: pre-wrap;
margin-bottom: 2em;
}
section.container {
margin: auto;
}
.extra-msg {
font-size: 0.8rem;
}
`);
}
render() {
return html`
<section class="container">
<div class="has-text-centered is-flex">
<wr-anim-logo class="logo" size="96px"></wr-anim-logo>
</div>
${!this.embed
? html` <div class="level">
<p class="level-item">Loading&nbsp;<b>${this.sourceUrl}</b>...</p>
</div>`
: ""}
<div class="level">
<div class="level-item has-text-centered">
${this.renderContent()}
</div>
</div>
</section>
`;
}
renderContent() {
switch (this.state) {
case "googledrive":
return html`<wr-gdrive
.sourceUrl=${this.sourceUrl!}
@load-ready=${this.onLoadReady}
></wr-gdrive>`;
case "started":
return html` <div class="progress-div">
<progress
id="progress"
class="progress is-primary is-large"
value="${this.percent}"
max="100"
></progress>
<label class="progress-label" for="progress">${this.percent}%</label>
${this.currentSize && this.totalSize
? html` <div class="loaded-prog">
Loaded
<b>${prettyBytes(this.currentSize)}</b>
of
<b>${prettyBytes(this.totalSize)}</b>
${this.extraMsg &&
html` <p class="extra-msg">(${this.extraMsg})</p> `}
</div>`
: html``}
${!this.embed
? html` <button @click="${this.onCancel}" class="button is-danger">
Cancel
</button>`
: ""}
</div>`;
case "errored":
return html` <div class="has-text-left">
<div class="error has-text-danger">${this.error}</div>
<div>
${this.errorAllowRetry
? html` <a
class="button is-warning"
@click=${() => window.parent.location.reload()}
>Try Again</a
>`
: ""}
${this.embed
? html``
: html` <a href="/" class="button is-warning">Back</a>`}
</div>
</div>`;
case "permission_needed":
return html` <div class="has-text-left">
<div class="">
Permission is needed to reload the archive file. (Click
<i>Cancel</i> to cancel loading this archive.)
</div>
<button @click="${this.onAskPermission}" class="button is-primary">
Show Permission
</button>
<a href="/" class="button is-danger">Cancel</a>
</div>`;
case "waiting":
default:
return html`<progress
class="progress is-primary is-large"
style="max-width: 400px"
></progress>`;
}
}
async onAskPermission() {
// eslint-disable-next-line @typescript-eslint/no-explicit-any
const result = await (this.fileHandle as any)?.requestPermission({
mode: "read",
});
if (result === "granted") {
this.doLoad();
}
}
}
customElements.define("wr-loader", Loader);
export { Loader };