/ proof-of-concept / plate-dynamics-lab-2
Sign in
proof-of-concept / plate-dynamics-lab-2
plate-dynamics-lab-2 / packages / core / src / runner / workerRunner.ts
111 lines · 3.1 KBBlameHistoryRaw
1import type { BackendInfo, RunRequest, RunResult, Runner } from "./types";
2import type { FromWorker, ToWorker } from "./protocol";
4interface Pending {
5 resolve: (r: RunResult) => void;
6 onOutput?: (text: string) => void;
7 output: string[];
8 t0: number;
9}
11/**
12 * Runs numbl in a Web Worker the runner owns. Cancelling terminates the worker
13 * and a fresh one is created for the next run, so a runaway eigensolve can
14 * always be stopped, with or without cross-origin isolation.
15 */
16export class WorkerRunner implements Runner {
17 private worker: Worker | null = null;
18 private nextId = 1;
19 private pending = new Map<number, Pending>();
20 private infoResolvers = new Map<number, (b: BackendInfo) => void>();
21 private cachedBackend: BackendInfo | null = null;
22 private readonly createWorker: () => Worker;
24 constructor(createWorker?: () => Worker) {
25 this.createWorker =
26 createWorker ??
27 (() => new Worker(new URL("./worker.ts", import.meta.url), { type: "module" }));
28 }
30 get backend(): Promise<BackendInfo> {
31 if (this.cachedBackend) return Promise.resolve(this.cachedBackend);
32 return new Promise((resolve) => {
33 const id = this.nextId++;
34 this.infoResolvers.set(id, (b) => {
35 this.cachedBackend = b;
36 resolve(b);
37 });
38 this.ensureWorker().postMessage({ type: "info", id } satisfies ToWorker);
39 });
40 }
42 private ensureWorker(): Worker {
43 if (this.worker) return this.worker;
44 const w = this.createWorker();
45 w.onmessage = (e: MessageEvent<FromWorker>) => this.onMessage(e.data);
46 w.onerror = (e) => this.failAll(`numbl worker crashed: ${e.message || "unknown error"}`);
47 this.worker = w;
48 return w;
49 }
51 private onMessage(msg: FromWorker) {
52 if (msg.type === "info") {
53 this.infoResolvers.get(msg.id)?.(msg.backend);
54 this.infoResolvers.delete(msg.id);
55 return;
56 }
57 const p = this.pending.get(msg.id);
58 if (!p) return;
59 if (msg.type === "output") {
60 p.output.push(msg.text);
61 p.onOutput?.(msg.text);
62 } else if (msg.type === "done") {
63 this.pending.delete(msg.id);
64 p.resolve(msg.result);
65 }
66 }
68 private failAll(error: string, cancelled = false) {
69 const backend = this.cachedBackend ?? {
70 engine: "numbl",
71 version: "unknown",
72 optimization: "1",
73 host: "web-worker",
74 };
75 for (const p of this.pending.values()) {
76 p.resolve({
77 ok: false,
78 error,
79 output: p.output,
80 wallMs: performance.now() - p.t0,
81 backend,
82 cancelled,
83 });
84 }
85 this.pending.clear();
86 }
88 run(req: RunRequest, onOutput?: (text: string) => void): Promise<RunResult> {
89 return new Promise((resolve) => {
90 const id = this.nextId++;
91 this.pending.set(id, { resolve, onOutput, output: [], t0: performance.now() });
92 this.ensureWorker().postMessage({
93 type: "run",
94 id,
95 bundle: req.bundle,
96 inputs: req.inputs,
97 } satisfies ToWorker);
98 });
99 }
101 cancel(): void {
102 if (!this.worker) return;
103 this.worker.terminate();
104 this.worker = null;
105 this.failAll("run cancelled", true);
106 }
108 dispose(): void {
109 this.cancel();
110 }
↑↓ move↵openescclose