worker-host.mjs raw
1 // worker-host.mjs — a Moxie domain running in a dedicated Worker.
2 //
3 // The module never runs on the page thread: channel_recv uses Atomics.wait,
4 // which the main browser thread forbids, and spawn() creates a new Worker
5 // from inside one. Two kinds of work arrive here over the same message port:
6 //
7 // {kind:'root', wasm} the application itself: _start()
8 // {kind:'entry', wasm, fnIdx, args, chans} a spawned domain: __spawn_entry
9 //
10 // Either way the module is instantiated with is_worker_context() = 1, and a
11 // spawned domain is given the channel handles the parent created so the child
12 // owns the far end of the same SPSC rings. Nothing else crosses the boundary.
13 import { buildHostImports, buildWorkerBridge, setHostModule } from './host.mjs';
14
15 let memory;
16
17 globalThis.addEventListener('error', (e) => {
18 globalThis.postMessage({ kind: 'error', error: String((e && e.error && e.error.stack) || e.message) });
19 });
20 globalThis.addEventListener('unhandledrejection', (e) => {
21 globalThis.postMessage({ kind: 'error', error: String((e.reason && e.reason.stack) || e.reason) });
22 });
23
24 // bindChannels turns the parent's handle/SAB pairs into this domain's channel
25 // table, so channel_send/recv/close address the far end of the same rings.
26 function bindChannels(list) {
27 const channels = new Map();
28 for (const c of list || []) channels.set(c.handle, c.sab);
29 return channels;
30 }
31
32 async function instantiate(wasmPath, channels) {
33 const response = await fetch(wasmPath);
34 const bytes = await response.arrayBuffer();
35 const result = await WebAssembly.instantiate(bytes, {
36 ...buildHostImports(() => memory),
37 bridge: buildWorkerBridge(channels),
38 });
39 memory = result.instance.exports.memory;
40 return result.instance;
41 }
42
43 function runRoot(wasmPath) {
44 instantiate(wasmPath, new Map()).then((inst) => {
45 setHostModule(inst);
46 inst.exports._start();
47 globalThis.postMessage({ kind: 'done' });
48 }).catch((e) => {
49 globalThis.postMessage({ kind: 'error', error: String((e && e.stack) || e) });
50 });
51 }
52
53 function runEntry(wasmPath, fnIdx, args, chanList) {
54 instantiate(wasmPath, bindChannels(chanList)).then((inst) => {
55 setHostModule(inst);
56 if (typeof inst.exports.__spawn_entry !== 'function') {
57 throw new Error('module has no __spawn_entry export; it was not built with spawn support');
58 }
59 // The compiler packs scalar args into linear memory; __spawn_entry reads
60 // them from there, and channel params arrive as int32 handles.
61 const ptr = inst.exports.__alloc(args.length);
62 new Uint8Array(memory.buffer, ptr, args.length).set(args);
63 const dv = new DataView(memory.buffer);
64 const handles = (chanList || []).map(c => c.handle);
65 const hptr = inst.exports.__alloc(handles.length * 4);
66 handles.forEach((h, i) => dv.setInt32(hptr + i * 4, h, true));
67 inst.exports.__spawn_entry(fnIdx, ptr, args.length, hptr, handles.length);
68 globalThis.postMessage({ kind: 'done' });
69 }).catch((e) => {
70 globalThis.postMessage({ kind: 'error', error: String((e && e.stack) || e) });
71 });
72 }
73
74 globalThis.addEventListener('message', (e) => {
75 const m = e.data;
76 if (!m) return;
77 if (m.kind === 'root') runRoot(m.wasm);
78 else if (m.kind === 'entry') runEntry(m.wasm, m.fnIdx, m.args || new Uint8Array(0), m.chans);
79 });
80