@cfasim-ui/wasm 0.10.0 → 0.11.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/package.json +2 -2
- package/src/wasm.worker.ts +45 -5
- package/src/wasmWorkerApi.test.ts +189 -0
- package/src/wasmWorkerApi.ts +112 -39
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@cfasim-ui/wasm",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.11.0",
|
|
4
4
|
"type": "module",
|
|
5
5
|
"description": "WebAssembly integration for cfasim-ui",
|
|
6
6
|
"license": "Apache-2.0",
|
|
@@ -20,7 +20,7 @@
|
|
|
20
20
|
"./vite": "./src/vitePlugin.js"
|
|
21
21
|
},
|
|
22
22
|
"dependencies": {
|
|
23
|
-
"@cfasim-ui/shared": "0.
|
|
23
|
+
"@cfasim-ui/shared": "0.11.0"
|
|
24
24
|
},
|
|
25
25
|
"peerDependencies": {
|
|
26
26
|
"vue": "^3.5.0"
|
package/src/wasm.worker.ts
CHANGED
|
@@ -1,12 +1,22 @@
|
|
|
1
|
+
// Subpath imports keep Vue out of the worker: the package barrel re-exports
|
|
2
|
+
// `useUrlParams`, which imports it.
|
|
1
3
|
import {
|
|
2
4
|
postWithTransfer,
|
|
3
5
|
postModelOutputsWithTransfer,
|
|
4
6
|
postErrorWithTransfer,
|
|
5
7
|
} from "@cfasim-ui/shared/transfer";
|
|
8
|
+
import type { WasmTrapKind } from "@cfasim-ui/shared/transfer";
|
|
9
|
+
import {
|
|
10
|
+
describeWasmTrap,
|
|
11
|
+
describeWasmInitTrap,
|
|
12
|
+
isWasmTrap,
|
|
13
|
+
} from "@cfasim-ui/shared/workerError";
|
|
6
14
|
import type { WorkerMessage } from "./messages.js";
|
|
7
15
|
|
|
8
16
|
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
9
|
-
|
|
17
|
+
type WasmModule = Record<string, any>;
|
|
18
|
+
|
|
19
|
+
const modulePromises = new Map<string, Promise<WasmModule>>();
|
|
10
20
|
|
|
11
21
|
const baseUrl = import.meta.env.BASE_URL ?? "/";
|
|
12
22
|
|
|
@@ -19,8 +29,7 @@ function wasmVersion(model: string): string | undefined {
|
|
|
19
29
|
return __CFASIM_WASM_VERSIONS__?.[model];
|
|
20
30
|
}
|
|
21
31
|
|
|
22
|
-
|
|
23
|
-
function ensureModule(model: string): Promise<Record<string, any>> {
|
|
32
|
+
function ensureModule(model: string): Promise<WasmModule> {
|
|
24
33
|
if (!modulePromises.has(model)) {
|
|
25
34
|
const promise = (async () => {
|
|
26
35
|
const dir = `${self.location.origin}${baseUrl}wasm/${model}/`;
|
|
@@ -35,6 +44,9 @@ function ensureModule(model: string): Promise<Record<string, any>> {
|
|
|
35
44
|
} else {
|
|
36
45
|
await mod.default();
|
|
37
46
|
}
|
|
47
|
+
// cfasim-model's panic recorder (releases after 0.10.0). Installed after
|
|
48
|
+
// init so it chains onto any hook the model's start function set.
|
|
49
|
+
mod.__cfasim_install_panic_hook?.();
|
|
38
50
|
return mod;
|
|
39
51
|
})();
|
|
40
52
|
promise.catch(() => {
|
|
@@ -47,9 +59,10 @@ function ensureModule(model: string): Promise<Record<string, any>> {
|
|
|
47
59
|
|
|
48
60
|
self.onmessage = async (event: MessageEvent<WorkerMessage>) => {
|
|
49
61
|
const { id, model, fn, args } = event.data;
|
|
62
|
+
let mod: WasmModule | undefined;
|
|
50
63
|
try {
|
|
51
64
|
const t0 = performance.now();
|
|
52
|
-
|
|
65
|
+
mod = await ensureModule(model);
|
|
53
66
|
const result = mod[fn](...args);
|
|
54
67
|
const elapsed = Math.round((performance.now() - t0) * 10) / 10;
|
|
55
68
|
console.log(`[wasm-worker] ${model}.${fn} ${elapsed}ms`);
|
|
@@ -81,6 +94,33 @@ self.onmessage = async (event: MessageEvent<WorkerMessage>) => {
|
|
|
81
94
|
postWithTransfer(self, id, result);
|
|
82
95
|
}
|
|
83
96
|
} catch (error) {
|
|
84
|
-
|
|
97
|
+
if (!isWasmTrap(error)) {
|
|
98
|
+
const text = error instanceof Error ? error.message : String(error);
|
|
99
|
+
console.error(`[wasm-worker] ${model}.${fn} failed: ${text}`, error);
|
|
100
|
+
postErrorWithTransfer(self, id, error);
|
|
101
|
+
return;
|
|
102
|
+
}
|
|
103
|
+
// The instance is unusable after a trap: a panic does not unwind, so
|
|
104
|
+
// destructors never ran, and the glue keeps a half-initialized instance
|
|
105
|
+
// when init trapped. The main thread replaces the worker on `trap`.
|
|
106
|
+
const trapMessage = (error as Error).message;
|
|
107
|
+
let message: string;
|
|
108
|
+
let trap: WasmTrapKind;
|
|
109
|
+
if (mod) {
|
|
110
|
+
const takePanic = mod.__cfasim_take_panic;
|
|
111
|
+
let panic: string | undefined;
|
|
112
|
+
try {
|
|
113
|
+
panic = takePanic?.() as string | undefined;
|
|
114
|
+
} catch {
|
|
115
|
+
// Reading the string allocates, which can trap again after an OOM.
|
|
116
|
+
}
|
|
117
|
+
message = describeWasmTrap(trapMessage, panic, !!takePanic);
|
|
118
|
+
trap = "call";
|
|
119
|
+
} else {
|
|
120
|
+
message = describeWasmInitTrap(trapMessage);
|
|
121
|
+
trap = "init";
|
|
122
|
+
}
|
|
123
|
+
console.error(`[wasm-worker] ${model}.${fn} failed: ${message}`, error);
|
|
124
|
+
postErrorWithTransfer(self, id, new Error(message), { trap });
|
|
85
125
|
}
|
|
86
126
|
};
|
|
@@ -0,0 +1,189 @@
|
|
|
1
|
+
import { describe, it, expect, vi, beforeAll } from "vitest";
|
|
2
|
+
import type { WorkerMessage } from "./messages.js";
|
|
3
|
+
|
|
4
|
+
class FakeWorker extends EventTarget {
|
|
5
|
+
static instances: FakeWorker[] = [];
|
|
6
|
+
posted: WorkerMessage[] = [];
|
|
7
|
+
terminated = false;
|
|
8
|
+
|
|
9
|
+
constructor() {
|
|
10
|
+
super();
|
|
11
|
+
FakeWorker.instances.push(this);
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
postMessage(message: WorkerMessage) {
|
|
15
|
+
this.posted.push(message);
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
terminate() {
|
|
19
|
+
this.terminated = true;
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
reply(data: Record<string, unknown>) {
|
|
23
|
+
this.dispatchEvent(new MessageEvent("message", { data }));
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
fail(message: string) {
|
|
27
|
+
const e = new Event("error") as Event & { message: string };
|
|
28
|
+
e.message = message;
|
|
29
|
+
this.dispatchEvent(e);
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
static current(): FakeWorker {
|
|
33
|
+
return FakeWorker.instances[FakeWorker.instances.length - 1];
|
|
34
|
+
}
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
let runWasm: typeof import("./wasmWorkerApi.js").runWasm;
|
|
38
|
+
let cancelWasm: typeof import("./wasmWorkerApi.js").cancelWasm;
|
|
39
|
+
|
|
40
|
+
beforeAll(async () => {
|
|
41
|
+
vi.stubGlobal("Worker", FakeWorker);
|
|
42
|
+
({ runWasm, cancelWasm } = await import("./wasmWorkerApi.js"));
|
|
43
|
+
});
|
|
44
|
+
|
|
45
|
+
function lastPosted(w: FakeWorker): WorkerMessage {
|
|
46
|
+
return w.posted[w.posted.length - 1];
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
const PANIC = "Model panicked at src/lib.rs:21:9: rate must be <= 5, got 9";
|
|
50
|
+
|
|
51
|
+
describe("runWasm", () => {
|
|
52
|
+
it("resolves a plain result by request id", async () => {
|
|
53
|
+
const w = FakeWorker.current();
|
|
54
|
+
const p = runWasm("m", "double", "2");
|
|
55
|
+
const { id } = lastPosted(w);
|
|
56
|
+
w.reply({ id: id + 1000, result: "not mine" });
|
|
57
|
+
w.reply({ id, result: 4 });
|
|
58
|
+
await expect(p).resolves.toBe(4);
|
|
59
|
+
});
|
|
60
|
+
|
|
61
|
+
it("rejects an ordinary error, even an empty one, and keeps the worker", async () => {
|
|
62
|
+
const w = FakeWorker.current();
|
|
63
|
+
const p = runWasm("m", "fallible", "9");
|
|
64
|
+
w.reply({ id: lastPosted(w).id, error: "rate must be <= 5, got 9" });
|
|
65
|
+
await expect(p).rejects.toThrow("rate must be <= 5, got 9");
|
|
66
|
+
const empty = runWasm("m", "fallible", "");
|
|
67
|
+
w.reply({ id: lastPosted(w).id, error: "" });
|
|
68
|
+
await expect(empty).rejects.toThrow();
|
|
69
|
+
expect(w.terminated).toBe(false);
|
|
70
|
+
expect(FakeWorker.current()).toBe(w);
|
|
71
|
+
});
|
|
72
|
+
|
|
73
|
+
it("replaces the worker after a trap and re-sends the other calls", async () => {
|
|
74
|
+
const old = FakeWorker.current();
|
|
75
|
+
const trapped = runWasm("m", "boom", "9");
|
|
76
|
+
const survivor = runWasm("m", "ok", "3");
|
|
77
|
+
const trappedId = old.posted[old.posted.length - 2].id;
|
|
78
|
+
const survivorId = lastPosted(old).id;
|
|
79
|
+
|
|
80
|
+
old.reply({ id: trappedId, error: PANIC, trap: "call" });
|
|
81
|
+
await expect(trapped).rejects.toThrow(PANIC);
|
|
82
|
+
|
|
83
|
+
const fresh = FakeWorker.current();
|
|
84
|
+
expect(old.terminated).toBe(true);
|
|
85
|
+
expect(fresh).not.toBe(old);
|
|
86
|
+
expect(fresh.posted).toEqual([
|
|
87
|
+
{ id: survivorId, model: "m", fn: "ok", args: ["3"] },
|
|
88
|
+
]);
|
|
89
|
+
|
|
90
|
+
// Late events from the retired worker are ignored; the fresh one answers.
|
|
91
|
+
old.reply({ id: survivorId, result: "stale" });
|
|
92
|
+
old.fail("stale worker error");
|
|
93
|
+
fresh.reply({ id: survivorId, result: 6 });
|
|
94
|
+
await expect(survivor).resolves.toBe(6);
|
|
95
|
+
|
|
96
|
+
// The replacement is live for new calls.
|
|
97
|
+
const next = runWasm("m", "ok", "4");
|
|
98
|
+
fresh.reply({ id: lastPosted(fresh).id, result: 8 });
|
|
99
|
+
await expect(next).resolves.toBe(8);
|
|
100
|
+
});
|
|
101
|
+
|
|
102
|
+
it("fails queued repeats of the trapped call instead of re-sending them", async () => {
|
|
103
|
+
const old = FakeWorker.current();
|
|
104
|
+
const first = runWasm("m", "boom", "9");
|
|
105
|
+
const repeat = runWasm("m", "boom", "9");
|
|
106
|
+
const other = runWasm("m", "boom", "1");
|
|
107
|
+
const firstId = old.posted[old.posted.length - 3].id;
|
|
108
|
+
const otherId = lastPosted(old).id;
|
|
109
|
+
|
|
110
|
+
old.reply({ id: firstId, error: PANIC, trap: "call" });
|
|
111
|
+
await expect(first).rejects.toThrow(PANIC);
|
|
112
|
+
await expect(repeat).rejects.toThrow(PANIC);
|
|
113
|
+
|
|
114
|
+
const fresh = FakeWorker.current();
|
|
115
|
+
expect(fresh.posted.map((m) => m.id)).toEqual([otherId]);
|
|
116
|
+
fresh.reply({ id: otherId, result: 1 });
|
|
117
|
+
await expect(other).resolves.toBe(1);
|
|
118
|
+
});
|
|
119
|
+
|
|
120
|
+
it("fails every queued call into a model whose init trapped", async () => {
|
|
121
|
+
const old = FakeWorker.current();
|
|
122
|
+
const init = runWasm("broken", "a");
|
|
123
|
+
const sameModel = runWasm("broken", "b", "x");
|
|
124
|
+
const otherModel = runWasm("fine", "a");
|
|
125
|
+
const initId = old.posted[old.posted.length - 3].id;
|
|
126
|
+
const otherId = lastPosted(old).id;
|
|
127
|
+
|
|
128
|
+
const message = "Model panicked or aborted while initializing";
|
|
129
|
+
vi.useFakeTimers();
|
|
130
|
+
try {
|
|
131
|
+
old.reply({ id: initId, error: message, trap: "init" });
|
|
132
|
+
await expect(init).rejects.toThrow(message);
|
|
133
|
+
await expect(sameModel).rejects.toThrow(message);
|
|
134
|
+
|
|
135
|
+
const fresh = FakeWorker.current();
|
|
136
|
+
expect(old.terminated).toBe(true);
|
|
137
|
+
expect(fresh.posted.map((m) => m.id)).toEqual([otherId]);
|
|
138
|
+
fresh.reply({ id: otherId, result: "ok" });
|
|
139
|
+
await expect(otherModel).resolves.toBe("ok");
|
|
140
|
+
|
|
141
|
+
// Within the retry window a new call into that model is rejected
|
|
142
|
+
// without reaching the worker; afterwards it is sent again.
|
|
143
|
+
await expect(runWasm("broken", "a")).rejects.toThrow(message);
|
|
144
|
+
expect(fresh.posted).toHaveLength(1);
|
|
145
|
+
vi.advanceTimersByTime(2000);
|
|
146
|
+
const later = runWasm("broken", "a");
|
|
147
|
+
expect(fresh.posted).toHaveLength(2);
|
|
148
|
+
fresh.reply({ id: lastPosted(fresh).id, result: "fixed" });
|
|
149
|
+
await expect(later).resolves.toBe("fixed");
|
|
150
|
+
} finally {
|
|
151
|
+
vi.useRealTimers();
|
|
152
|
+
}
|
|
153
|
+
});
|
|
154
|
+
|
|
155
|
+
it("fails pending calls on a worker error and starts fresh after a short wait", async () => {
|
|
156
|
+
vi.useFakeTimers();
|
|
157
|
+
try {
|
|
158
|
+
const w = FakeWorker.current();
|
|
159
|
+
const p = runWasm("m", "slow");
|
|
160
|
+
w.fail("Worker failed to load");
|
|
161
|
+
await expect(p).rejects.toThrow("Worker failed to load");
|
|
162
|
+
|
|
163
|
+
// Within the retry window the same rejection comes back, no spawn.
|
|
164
|
+
await expect(runWasm("m", "ok", "0")).rejects.toThrow(
|
|
165
|
+
"Worker failed to load",
|
|
166
|
+
);
|
|
167
|
+
expect(FakeWorker.current()).toBe(w);
|
|
168
|
+
|
|
169
|
+
vi.advanceTimersByTime(2000);
|
|
170
|
+
const retry = runWasm("m", "ok", "1");
|
|
171
|
+
const fresh = FakeWorker.current();
|
|
172
|
+
expect(fresh).not.toBe(w);
|
|
173
|
+
expect(w.terminated).toBe(true);
|
|
174
|
+
fresh.reply({ id: lastPosted(fresh).id, result: 2 });
|
|
175
|
+
await expect(retry).resolves.toBe(2);
|
|
176
|
+
} finally {
|
|
177
|
+
vi.useRealTimers();
|
|
178
|
+
}
|
|
179
|
+
});
|
|
180
|
+
|
|
181
|
+
it("cancelWasm rejects in-flight calls with 'cancelled'", async () => {
|
|
182
|
+
const w = FakeWorker.current();
|
|
183
|
+
const p = runWasm("m", "slow");
|
|
184
|
+
cancelWasm();
|
|
185
|
+
await expect(p).rejects.toThrow("cancelled");
|
|
186
|
+
expect(w.terminated).toBe(true);
|
|
187
|
+
expect(FakeWorker.current()).not.toBe(w);
|
|
188
|
+
});
|
|
189
|
+
});
|
package/src/wasmWorkerApi.ts
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import type { TransferableResponse } from "@cfasim-ui/shared";
|
|
1
|
+
import type { TransferableResponse, WasmTrapKind } from "@cfasim-ui/shared";
|
|
2
2
|
import {
|
|
3
3
|
unwrapResponse,
|
|
4
4
|
describeWorkerError,
|
|
@@ -8,15 +8,28 @@ import type { WorkerMessage } from "./messages.js";
|
|
|
8
8
|
|
|
9
9
|
let lastId = 0;
|
|
10
10
|
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
11
|
+
interface Pending {
|
|
12
|
+
message: WorkerMessage;
|
|
13
|
+
resolve: (value: unknown) => void;
|
|
14
|
+
reject: (reason: Error) => void;
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
// In-flight calls keyed by request id. `cancelWasm` fails them when it
|
|
18
|
+
// terminates the worker mid-run, worker-level errors drain them all at once,
|
|
19
|
+
// and a trap re-sends the survivors to the replacement worker.
|
|
20
|
+
const pending = new Map<number, Pending>();
|
|
15
21
|
|
|
16
|
-
// Set
|
|
17
|
-
//
|
|
18
|
-
//
|
|
22
|
+
// Set when the worker fires an `error` / `messageerror` event, which fails
|
|
23
|
+
// every pending call. A `runWasm` after `WORKER_RETRY_MS` starts a fresh
|
|
24
|
+
// worker; sooner ones get the same rejection, so a worker script that never
|
|
25
|
+
// loads costs one spawn per retry window, not one per slider tick.
|
|
19
26
|
let workerDeadError: Error | null = null;
|
|
27
|
+
let workerDiedAt = 0;
|
|
28
|
+
const WORKER_RETRY_MS = 2000;
|
|
29
|
+
|
|
30
|
+
// Models whose initialization trapped, with the same retry window: each
|
|
31
|
+
// retry costs a worker and a module load.
|
|
32
|
+
const initTraps = new Map<string, { error: Error; at: number }>();
|
|
20
33
|
|
|
21
34
|
function newWorker(): Worker {
|
|
22
35
|
const w = new Worker(new URL("./wasm.worker.ts", import.meta.url), {
|
|
@@ -25,50 +38,112 @@ function newWorker(): Worker {
|
|
|
25
38
|
function failAll(message: string) {
|
|
26
39
|
if (workerDeadError) return;
|
|
27
40
|
workerDeadError = new Error(message);
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
41
|
+
workerDiedAt = Date.now();
|
|
42
|
+
const entries = Array.from(pending.values());
|
|
43
|
+
pending.clear();
|
|
44
|
+
for (const entry of entries) entry.reject(workerDeadError);
|
|
31
45
|
}
|
|
46
|
+
// Events from a worker that has since been replaced are stale: its
|
|
47
|
+
// requests were re-sent to, or failed with, the current worker.
|
|
32
48
|
w.addEventListener("error", (e: ErrorEvent) => {
|
|
49
|
+
if (w !== worker) return;
|
|
33
50
|
failAll(describeWorkerError(e));
|
|
34
51
|
});
|
|
35
52
|
w.addEventListener("messageerror", () => {
|
|
53
|
+
if (w !== worker) return;
|
|
36
54
|
failAll(MESSAGEERROR_TEXT);
|
|
37
55
|
});
|
|
56
|
+
w.addEventListener("message", (event: MessageEvent<TransferableResponse>) => {
|
|
57
|
+
if (w !== worker) return;
|
|
58
|
+
const data = event.data;
|
|
59
|
+
const entry = data && pending.get(data.id);
|
|
60
|
+
if (!entry) return;
|
|
61
|
+
pending.delete(data.id);
|
|
62
|
+
if (data.error !== undefined) {
|
|
63
|
+
const error = new Error(data.error);
|
|
64
|
+
if (data.trap) replaceWorker(entry.message, data.trap, error);
|
|
65
|
+
entry.reject(error);
|
|
66
|
+
} else {
|
|
67
|
+
entry.resolve(unwrapResponse(data));
|
|
68
|
+
}
|
|
69
|
+
});
|
|
38
70
|
return w;
|
|
39
71
|
}
|
|
40
72
|
|
|
41
73
|
let worker = newWorker();
|
|
42
74
|
|
|
75
|
+
function restartWorker(): void {
|
|
76
|
+
worker.terminate();
|
|
77
|
+
workerDeadError = null;
|
|
78
|
+
worker = newWorker();
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
function sameCall(a: WorkerMessage, b: WorkerMessage): boolean {
|
|
82
|
+
return (
|
|
83
|
+
a.model === b.model &&
|
|
84
|
+
a.fn === b.fn &&
|
|
85
|
+
a.args.length === b.args.length &&
|
|
86
|
+
a.args.every((arg, i) => arg === b.args[i])
|
|
87
|
+
);
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
/**
|
|
91
|
+
* Replace the worker after a trap, since the old instance may be poisoned
|
|
92
|
+
* (a panic never unwinds). Other in-flight calls move to the new worker,
|
|
93
|
+
* except those that would trap the same way: the same call again, or any
|
|
94
|
+
* call into a model whose init trapped. Calls with other inputs are re-sent
|
|
95
|
+
* even though they may trap too, each costing another replacement: failing
|
|
96
|
+
* them would report an error for inputs that might work and never retry.
|
|
97
|
+
*/
|
|
98
|
+
function replaceWorker(
|
|
99
|
+
trapped: WorkerMessage,
|
|
100
|
+
kind: WasmTrapKind,
|
|
101
|
+
error: Error,
|
|
102
|
+
): void {
|
|
103
|
+
if (kind === "init") {
|
|
104
|
+
initTraps.set(trapped.model, { error, at: Date.now() });
|
|
105
|
+
}
|
|
106
|
+
restartWorker();
|
|
107
|
+
for (const [id, entry] of Array.from(pending)) {
|
|
108
|
+
const doomed =
|
|
109
|
+
kind === "init"
|
|
110
|
+
? entry.message.model === trapped.model
|
|
111
|
+
: sameCall(entry.message, trapped);
|
|
112
|
+
if (doomed) {
|
|
113
|
+
pending.delete(id);
|
|
114
|
+
entry.reject(error);
|
|
115
|
+
} else {
|
|
116
|
+
worker.postMessage(entry.message);
|
|
117
|
+
}
|
|
118
|
+
}
|
|
119
|
+
}
|
|
120
|
+
|
|
43
121
|
export function runWasm(
|
|
44
122
|
model: string,
|
|
45
123
|
fn: string,
|
|
46
124
|
...args: string[]
|
|
47
125
|
): Promise<unknown> {
|
|
48
126
|
if (workerDeadError) {
|
|
49
|
-
|
|
127
|
+
// The worker's module failed to load or it posted something unreadable.
|
|
128
|
+
// That can be transient (a dev server restart), so try a fresh one
|
|
129
|
+
// rather than failing every call for the rest of the page's life.
|
|
130
|
+
if (Date.now() - workerDiedAt < WORKER_RETRY_MS) {
|
|
131
|
+
return Promise.reject(workerDeadError);
|
|
132
|
+
}
|
|
133
|
+
restartWorker();
|
|
134
|
+
}
|
|
135
|
+
const initTrap = initTraps.get(model);
|
|
136
|
+
if (initTrap) {
|
|
137
|
+
if (Date.now() - initTrap.at < WORKER_RETRY_MS) {
|
|
138
|
+
return Promise.reject(initTrap.error);
|
|
139
|
+
}
|
|
140
|
+
initTraps.delete(model);
|
|
50
141
|
}
|
|
51
142
|
return new Promise((resolve, reject) => {
|
|
52
143
|
const id = ++lastId;
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
const myWorker = worker;
|
|
57
|
-
pendingRejects.set(id, reject);
|
|
58
|
-
|
|
59
|
-
function listener(event: MessageEvent<TransferableResponse>) {
|
|
60
|
-
if (event.data?.id !== id) return;
|
|
61
|
-
myWorker.removeEventListener("message", listener);
|
|
62
|
-
pendingRejects.delete(id);
|
|
63
|
-
if (event.data.error) {
|
|
64
|
-
reject(new Error(event.data.error));
|
|
65
|
-
} else {
|
|
66
|
-
resolve(unwrapResponse(event.data));
|
|
67
|
-
}
|
|
68
|
-
}
|
|
69
|
-
|
|
70
|
-
myWorker.addEventListener("message", listener);
|
|
71
|
-
myWorker.postMessage({ id, model, fn, args } as WorkerMessage);
|
|
144
|
+
const message: WorkerMessage = { id, model, fn, args };
|
|
145
|
+
pending.set(id, { message, resolve, reject });
|
|
146
|
+
worker.postMessage(message);
|
|
72
147
|
});
|
|
73
148
|
}
|
|
74
149
|
|
|
@@ -80,13 +155,11 @@ export function runWasm(
|
|
|
80
155
|
* known to be obsolete (e.g. the user has changed inputs).
|
|
81
156
|
*/
|
|
82
157
|
export function cancelWasm(): void {
|
|
83
|
-
if (
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
for (const reject of rejects) {
|
|
90
|
-
reject(new Error("cancelled"));
|
|
158
|
+
if (pending.size === 0) return;
|
|
159
|
+
const entries = Array.from(pending.values());
|
|
160
|
+
pending.clear();
|
|
161
|
+
restartWorker();
|
|
162
|
+
for (const entry of entries) {
|
|
163
|
+
entry.reject(new Error("cancelled"));
|
|
91
164
|
}
|
|
92
165
|
}
|