Skip to content

Commit ab852fe

Browse files
committed
y
1 parent a0ffee3 commit ab852fe

File tree

2 files changed

+261
-62
lines changed

2 files changed

+261
-62
lines changed

module.ts

+85-62
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,9 @@
33
* SPDX-License-Identifier: GPL-3.0-or-later
44
*/
55

6+
// @deno-types="./polyfills/async-disposable-stack.ts"
7+
import { AsyncDisposableStack } from "./polyfills/async-disposable-stack.js";
8+
69
// @ts-ignore
710
// @deno-types="./std/semver.ts"
811
import { parse, parseRange, satisfies } from "./std/semver.js";
@@ -24,23 +27,21 @@ import { SPOTIFY_VERSION } from "./static.js";
2427
// @deno-types="./transform.ts"
2528
import { createTransformer, type Transformer } from "./transform.js";
2629

27-
export type IndexMixinFn = (context: MixinContext) => void | PromiseLike<void>;
28-
export type IndexPreloadFn = (
29-
context: PreloadContext,
30-
) => void | PromiseLike<void> | PromiseLike<(() => void)> | PromiseLike<(() => PromiseLike<void>)>;
31-
export type IndexLoadFn = (
32-
context: LoadContext,
33-
) => void | PromiseLike<void> | PromiseLike<(() => void)> | PromiseLike<(() => PromiseLike<void>)>;
30+
export type IndexMixinFn = (context: MixinContext) => SyncOrAsync<void>;
31+
export type IndexPreloadFn = (context: PreloadContext) => SyncOrAsync<void> | PromiseLike<DisposeFn | void>;
32+
export type IndexLoadFn = (context: LoadContext) => SyncOrAsync<void> | PromiseLike<DisposeFn | void>;
3433

35-
export type JSIndex = {
34+
export interface JSIndex {
3635
mixin?: IndexMixinFn;
3736
preload?: IndexPreloadFn;
3837
load?: IndexLoadFn;
39-
};
38+
disposableStack: AsyncDisposableStack;
39+
}
4040

41-
export type CSSIndex = {
41+
export interface CSSIndex {
4242
default: CSSStyleSheet;
43-
};
43+
disposableStack: AsyncDisposableStack;
44+
}
4445

4546
export type ModuleIdentifier = string;
4647
export type Version = string;
@@ -431,12 +432,10 @@ export class ModuleInstance extends ModuleInstanceBase<Module> implements MixinL
431432

432433
public awaitedMixins = new Array<Promise<void>>();
433434

434-
_unloadJs: (() => Promise<void>) | null = null;
435-
_unloadCss: (() => void) | null = null;
436435
private mixinsLoaded = false;
437436
private loaded = false;
438-
private jsIndex: JSIndex | null = null;
439-
private cssIndex: CSSIndex | null = null;
437+
_jsIndex: JSIndex | null = null;
438+
_cssIndex: CSSIndex | null = null;
440439

441440
public transition = new Transition();
442441
private dependants = new Set<ModuleInstance>();
@@ -485,14 +484,14 @@ export class ModuleInstance extends ModuleInstanceBase<Module> implements MixinL
485484

486485
async #loadMixins() {
487486
await this.#loadJsIndex();
488-
if (!this.jsIndex) {
487+
if (!this._jsIndex) {
489488
return;
490489
}
491490

492491
console.time(`${this.getModuleIdentifier()}#loadMixins`);
493492
try {
494493
const mixinContext: MixinContext = { module: this, transformer: this.transformer };
495-
await this.jsIndex.mixin?.(mixinContext);
494+
await this._jsIndex.mixin?.(mixinContext);
496495
} catch (e) {
497496
console.error(
498497
new Error(
@@ -513,25 +512,20 @@ export class ModuleInstance extends ModuleInstanceBase<Module> implements MixinL
513512

514513
async #preloadJs() {
515514
await this.#loadJsIndex();
516-
if (!this.jsIndex) {
515+
const index = this._jsIndex;
516+
if (!index) {
517517
return;
518518
}
519519

520-
this._unloadJs = async () => {
521-
this._unloadJs = null;
522-
};
523-
524520
console.time(`${this.getModuleIdentifier()}#preloadJs`);
525521
try {
526522
const preloadContext: PreloadContext = { module: this };
527-
const predispose = await this.jsIndex.preload?.(preloadContext);
528-
const unloadJs = this._unloadJs;
529-
this._unloadJs = async () => {
530-
await predispose?.();
531-
await unloadJs();
532-
};
523+
const predispose = await index.preload?.(preloadContext);
524+
if (predispose) {
525+
index.disposableStack.defer(predispose);
526+
}
533527
} catch (e) {
534-
await this._unloadJs!();
528+
await index.disposableStack.disposeAsync();
535529
console.error(
536530
new Error(
537531
`Error preloading javascript for \`${this.getModuleIdentifier()}\``,
@@ -543,25 +537,20 @@ export class ModuleInstance extends ModuleInstanceBase<Module> implements MixinL
543537
}
544538

545539
async #loadJs() {
546-
if (!this.jsIndex) {
547-
return;
548-
}
549-
550-
if (!this._unloadJs) {
540+
const index = this._jsIndex;
541+
if (!index || index.disposableStack.disposed) {
551542
return;
552543
}
553544

554545
console.time(`${this.getModuleIdentifier()}#loadJs`);
555546
try {
556547
const loadContext: PreloadContext = { module: this };
557-
const dispose = await this.jsIndex.load?.(loadContext);
558-
const predispose = this._unloadJs!;
559-
this._unloadJs = async () => {
560-
await dispose?.();
561-
await predispose();
562-
};
548+
const dispose = await index.load?.(loadContext);
549+
if (dispose) {
550+
index.disposableStack.defer(dispose);
551+
}
563552
} catch (e) {
564-
await this._unloadJs!();
553+
await index.disposableStack.disposeAsync();
565554
console.error(
566555
new Error(
567556
`Error loading javascript for \`${this.getModuleIdentifier()}\``,
@@ -574,39 +563,56 @@ export class ModuleInstance extends ModuleInstanceBase<Module> implements MixinL
574563

575564
async #loadCss() {
576565
await this.#loadCssIndex();
577-
if (!this.cssIndex) {
566+
const index = this._cssIndex;
567+
if (!index) {
578568
return;
579569
}
580570

581-
const styleSheet = this.cssIndex.default;
582-
document.adoptedStyleSheets.push(styleSheet);
583-
584-
this._unloadCss = () => {
585-
this._unloadCss = null;
586-
document.adoptedStyleSheets = document.adoptedStyleSheets.filter((sheet) => sheet !== styleSheet);
587-
};
571+
console.time(`${this.getModuleIdentifier()}#loadCss`);
572+
try {
573+
const styleSheet = index.default;
574+
document.adoptedStyleSheets.push(styleSheet);
575+
index.disposableStack.defer(() => {
576+
document.adoptedStyleSheets = document.adoptedStyleSheets.filter((sheet) => sheet !== styleSheet);
577+
});
578+
} catch (e) {
579+
await index.disposableStack.disposeAsync();
580+
console.error(
581+
new Error(
582+
`Error loading css for \`${this.getModuleIdentifier()}\``,
583+
{ cause: e },
584+
),
585+
);
586+
}
587+
console.timeEnd(`${this.getModuleIdentifier()}#loadCss`);
588588
}
589589

590590
async #loadJsIndex() {
591+
this._jsIndex = null;
591592
const { js } = this.metadata?.entries ?? {};
592593
if (!js) {
593594
return;
594595
}
595596

596597
const now = Date.now();
597598
const uniqueEntry = `${this.getRelPath(js)!}?t=${now}`;
598-
this.jsIndex = await import(uniqueEntry);
599+
this._jsIndex = Object.assign({}, await import(uniqueEntry), {
600+
disposableStack: new AsyncDisposableStack(),
601+
});
599602
}
600603

601604
async #loadCssIndex() {
605+
this._cssIndex = null;
602606
const { css } = this.metadata?.entries ?? {};
603607
if (!css) {
604608
return;
605609
}
606610

607611
const now = Date.now();
608612
const uniqueEntry = `${this.getRelPath(css)!}?t=${now}`;
609-
this.cssIndex = await import(uniqueEntry, { with: { type: "css" } });
613+
this._cssIndex = Object.assign({}, await import(uniqueEntry, { with: { type: "css" } }), {
614+
disposableStack: new AsyncDisposableStack(),
615+
});
610616
}
611617

612618
private canLoadMixinsRecur() {
@@ -718,8 +724,8 @@ export class ModuleInstance extends ModuleInstanceBase<Module> implements MixinL
718724
Array.from(this.dependants).map((dependant) => dependant.unloadRecur()),
719725
);
720726

721-
await this._unloadJs?.();
722-
await this._unloadCss?.();
727+
await this._jsIndex?.disposableStack.disposeAsync();
728+
await this._cssIndex?.disposableStack.disposeAsync();
723729

724730
resolve();
725731
}
@@ -1099,30 +1105,47 @@ export const enableAllLoadable = () =>
10991105
getLoadableChildrenInstances().map((instance) => instance.load()),
11001106
);
11011107

1102-
export type DisposeFn = () => void | PromiseLike<void>;
1108+
// ...
1109+
1110+
export type ContextPromise =
1111+
& PromiseWithResolvers<DisposeFn | void>
1112+
& {
1113+
wrap: (promise: Promise<DisposeFn | void>) => Promise<DisposeFn | void>;
1114+
};
1115+
1116+
function createContextPromise(): ContextPromise {
1117+
const promise = Promise.withResolvers<DisposeFn | void>();
1118+
return Object.assign(promise, {
1119+
wrap: ($: Promise<DisposeFn | void>) => $.then((v) => promise.resolve(v), (e) => promise.reject(e)),
1120+
});
1121+
}
1122+
1123+
export type SyncOrAsync<T> = T | PromiseLike<T>;
1124+
export type DisposeFn = () => SyncOrAsync<void>;
11031125
export type PreloadContext = { module: ModuleInstance };
11041126
export type LoadContext = { module: ModuleInstance };
11051127
export type MixinContext = { module: ModuleInstance; transformer: Transformer };
1106-
export type ContextWithDispose<C extends {}> = C & { dispose: PromiseWithResolvers<DisposeFn> };
1128+
export type ContextWithPromise<C extends {}> = C & { promise: ContextPromise };
11071129
export const hotwire = <C extends {}>(
11081130
meta: ImportMeta,
11091131
url: string,
11101132
_import: () => Promise<any>,
11111133
raw = false,
11121134
) => {
1113-
const p = Promise.withResolvers<ContextWithDispose<C>>();
1135+
const p = Promise.withResolvers<ContextWithPromise<C>>();
11141136

11151137
const nurl = normalizeUrl(url, meta.url, raw);
11161138
globalThis["__HOTWIRED__"][nurl]?.reject(`outdated hotwire: ${nurl}`);
11171139
globalThis["__HOTWIRED__"][nurl] = p;
11181140

1119-
return async (ctx: C): Promise<DisposeFn> => {
1120-
const dispose = Promise.withResolvers<DisposeFn>();
1121-
p.resolve({ ...ctx, dispose } as ContextWithDispose<C>);
1122-
return await Promise.race([dispose.promise, _import()]);
1141+
return async <R extends void | DisposeFn>(ctx: C): Promise<R> => {
1142+
const promise = createContextPromise();
1143+
p.resolve({ ...ctx, promise } as ContextWithPromise<C>);
1144+
_import().catch((e) => promise.reject(e));
1145+
return (await promise.promise) as R;
11231146
};
11241147
};
1125-
export const hotwired = <C extends {}>(meta: ImportMeta): Promise<ContextWithDispose<C>> => {
1148+
export const hotwired = <C extends {}>(meta: ImportMeta): Promise<ContextWithPromise<C>> => {
11261149
const nurl = normalizeUrl(meta.url);
11271150
const p = globalThis["__HOTWIRED__"][nurl];
11281151
if (!p) {
@@ -1132,7 +1155,7 @@ export const hotwired = <C extends {}>(meta: ImportMeta): Promise<ContextWithDis
11321155
};
11331156

11341157
declare global {
1135-
var __HOTWIRED__: Record<string, PromiseWithResolvers<ContextWithDispose<any>>>;
1158+
var __HOTWIRED__: Record<string, PromiseWithResolvers<ContextWithPromise<any>>>;
11361159
}
11371160

11381161
globalThis["__HOTWIRED__"] = {};

0 commit comments

Comments
 (0)