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