Skip to content

Commit

Permalink
Reliable support for conditional imports
Browse files Browse the repository at this point in the history
I was struggling with dynamic imports for a while before I realized I
was actually working on the halting problem. This is hinted at in the
top-level await proposal:
https://github.com/tc39/proposal-top-level-await#rejected-deadlock-prevention-mechanisms

Instead, we treat resolved dynamic imports basically as static imports,
and they will participate in the cyclic resolution algorithm. Since
dynamic imports in practice tend to just be well-behaved conditional or
lazy imports this works out well.

This are probably some races here if a new dynamic import is dispatched
while an update is processing but haven't actually run into it myself.
  • Loading branch information
laverdet committed Aug 7, 2023
1 parent ac71889 commit 0586bc4
Show file tree
Hide file tree
Showing 10 changed files with 726 additions and 530 deletions.
6 changes: 4 additions & 2 deletions __tests__/__fixtures__/module.ts
Original file line number Diff line number Diff line change
Expand Up @@ -63,10 +63,12 @@ export class TestModule {
return controller.application.requestUpdateResult();
}

async update(source: () => string) {
async update(source?: () => string) {
assert(this.environment !== undefined);
assert(this.vm !== undefined);
this.source = source;
if (source) {
this.source = source;
}
this.vm = undefined;
const vm = this.instantiate(this.environment);
vm.evaluated = true;
Expand Down
14 changes: 14 additions & 0 deletions __tests__/accept.ts
Original file line number Diff line number Diff line change
Expand Up @@ -19,3 +19,17 @@ test("accept handlers should run top down", async () => {
const result = await main.releaseUpdate();
expect(result?.type).toBe(UpdateStatus.success);
});

test("should be able to accept a cycle root", async () => {
const main = new TestModule(() =>
`import {} from ${left};
import.meta.hot.accept(${left});`);
const left: TestModule = new TestModule(() =>
`import {} from ${right};`);
const right = new TestModule(() =>
`import {} from ${left};`);
await main.dispatch();
await right.update(() => "");
const result = await main.releaseUpdate();
expect(result?.type).toBe(UpdateStatus.success);
});
25 changes: 25 additions & 0 deletions __tests__/dynamic.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,25 @@
/* eslint-disable @typescript-eslint/restrict-template-expressions */
import { expect, test } from "@jest/globals";
import { UpdateStatus } from "../runtime/controller.js";
import { TestModule } from "./__fixtures__/module.js";

test("dynamic import with error throws", async () => {
const main = new TestModule(() =>
`const module = await import(${error});`);
const error = new TestModule(() =>
"throw new Error()");
await expect(main.dispatch()).rejects.toThrow();
});

test("lazy dynamic import with error is recoverable", async () => {
const main = new TestModule(() =>
`const module = import(${error});
await module.catch(() => {});
import.meta.hot.accept(${error});`);
const error = new TestModule(() =>
"throw new Error()");
await main.dispatch();
await error.update(() => "");
const result = await main.releaseUpdate();
expect(result?.type).toBe(UpdateStatus.success);
});
40 changes: 40 additions & 0 deletions __tests__/reload.ts
Original file line number Diff line number Diff line change
Expand Up @@ -109,3 +109,43 @@ test("declined with accept import", async () => {
const result = await main.releaseUpdate();
expect(result?.type).toBe(UpdateStatus.success);
});

test("errors are recoverable", async () => {
const main = new TestModule(() =>
`import { counter } from ${accepted};
import.meta.hot.accept(${accepted}, () => {
expect(counter).toBe(3);
globalThis.seen = true;
});`);
const accepted = new TestModule(() =>
"export const counter = 1;");
await main.dispatch();
await accepted.update(() =>
`export const counter = 2;
throw new Error();`);
const result = await main.releaseUpdate();
expect(result?.type).toBe(UpdateStatus.evaluationFailure);
await accepted.update(() =>
"export const counter = 3;");
const result2 = await main.releaseUpdate();
expect(result2?.type).toBe(UpdateStatus.success);
expect(main.global.seen).toBe(true);
});

test("errors persist", async () => {
const main = new TestModule(() =>
`import {} from ${error};
import.meta.hot.accept();`);
const error = new TestModule(() => "");
await main.dispatch();
await error.update(() =>
"throw new Error();");
const result = await main.releaseUpdate();
expect(result?.type).toBe(UpdateStatus.evaluationFailure);
await main.update();
const result2 = await main.releaseUpdate();
expect(result2?.type).toBe(UpdateStatus.evaluationFailure);
await error.update(() => "");
const result3 = await main.releaseUpdate();
expect(result3?.type).toBe(UpdateStatus.success);
});
27 changes: 27 additions & 0 deletions functional.ts
Original file line number Diff line number Diff line change
Expand Up @@ -27,6 +27,33 @@ export function somePredicate(predicates: Iterable<Predicate<unknown> | Predicat
(predicate, next) => value => predicate(value) || next(value));
}

/**
* Iterate each item from an iterable of iterables.
* @internal
*/
// TypeScript can't figure out the types on these so extra hints are needed.
export function concat<Type>(iterator: Iterable<Type>[]): IterableIterator<Type>;
/** @internal */
// eslint-disable-next-line @typescript-eslint/unified-signatures
export function concat<Type>(iterator: IterableIterator<Iterable<Type>>): IterableIterator<Type>;
/** @internal */
// eslint-disable-next-line @typescript-eslint/unified-signatures
export function concat<Type>(iterator: Iterable<Iterable<Type>>): IterableIterator<Type>;

/**
* Iterate each item in a statically supplied argument vector of iterables.
* @internal
*/
export function concat<Type>(...args: Iterable<Type>[]): IterableIterator<Type>;

export function *concat(...args: any[]) {
for (const iterable of args.length === 1 ? args[0] : args) {
for (const value of iterable) {
yield value;
}
}
}

/**
* Remove all non-truthy elements from a type
* @internal
Expand Down
Loading

0 comments on commit 0586bc4

Please sign in to comment.