relay-proxy-wasm-host.mjs raw

   1  // relay-proxy-wasm-host.mjs - Worker bootstrap for relay-proxy.wasm.
   2  //
   3  // Runs in a dedicated Web Worker. The page (wasm-host.mjs) is the supervisor.
   4  //
   5  // Page protocol (JSON-array messages over the natural Worker channel):
   6  //   page -> worker (via supervisor relay): REQ, CLOSE, EVENT, PUBLISH_TO,
   7  //                                          PROXY, SET_PUBKEY, SET_WRITE_RELAYS,
   8  //                                          RELAY_INFO, DM_*, MLS_*, etc.
   9  //   worker -> page: READY, EVENT, EOSE, OK, RELAY_INFO_RESULT, etc.
  10  //
  11  // Subsequent phases add the bridge surface for outbound WS, IDB writes, and
  12  // signing requests routed through the supervisor (Option B for Q7).
  13  
  14  import {
  15    makeCoreHelpers, makeWasi, makeCommonBridge,
  16    computeBuildHash,
  17  } from './bridge-common.mjs';
  18  import * as idb from './$runtime/idb.mjs';
  19  import * as ws from './$runtime/ws.mjs';
  20  
  21  let mem, xp;
  22  const _sabTable = new Map();
  23  const _sabSeqRef = { value: 0 };
  24  const _spawnedWorkerChans = new Map();
  25  const _wasmUrlRef = { value: null };
  26  const _buildHashRef = { value: null };
  27  
  28  // Build h with diagnostic wrappers around readStr/writeStr. The makeCoreHelpers
  29  // closures are private; we rebuild equivalent helpers here using mem/xp
  30  // directly so we can wrap the Uint8Array view construction with a try/catch
  31  // that emits a structured error message before the wasm traps. This is
  32  // production-side instrumentation for diagnosing the volume-regime
  33  // RangeError (see Phase 2.6 revert + task #27).
  34  const _enc = new TextEncoder();
  35  const _dec = new TextDecoder();
  36  function readStr(ptr, len) {
  37    if (len <= 0) return '';
  38    try {
  39      return _dec.decode(new Uint8Array(mem.buffer, ptr >>> 0, len));
  40    } catch (err) {
  41      self.postMessage('["__ERROR","readStr ptr=' + ptr + ' len=' + len + ' bufLen=' + mem.buffer.byteLength + ' err=' + String(err) + '"]');
  42      throw err;
  43    }
  44  }
  45  function readBytes(ptr, len) {
  46    if (len <= 0) return new Uint8Array(0);
  47    return new Uint8Array(mem.buffer, ptr >>> 0, len);
  48  }
  49  function writeStr(s) {
  50    const bytes = _enc.encode('' + s);
  51    const ptr = xp.__alloc(bytes.length) >>> 0;
  52    try {
  53      new Uint8Array(mem.buffer, ptr, bytes.length).set(bytes);
  54    } catch (err) {
  55      self.postMessage('["__ERROR","writeStr ptr=' + ptr + ' sLen=' + bytes.length + ' bufLen=' + mem.buffer.byteLength + ' err=' + String(err) + '"]');
  56      throw err;
  57    }
  58    return [ptr, bytes.length];
  59  }
  60  function writeI32(addr, val) {
  61    new DataView(mem.buffer).setInt32(addr >>> 0, val, true);
  62  }
  63  function writeBytes(data) {
  64    const u = (data instanceof Uint8Array) ? data : (data && data.$array != null
  65      ? (() => { const u = new Uint8Array(data.$length); for (let i = 0; i < data.$length; i++) u[i] = data.$array[data.$offset + i]; return u; })()
  66      : (typeof data === 'string' ? _enc.encode(data) : new Uint8Array(0)));
  67    const ptr = xp.__alloc(u.length) >>> 0;
  68    try {
  69      new Uint8Array(mem.buffer, ptr, u.length).set(u);
  70    } catch (err) {
  71      self.postMessage('["__ERROR","writeBytes ptr=' + ptr + ' len=' + u.length + ' bufLen=' + mem.buffer.byteLength + ' err=' + String(err) + '"]');
  72      throw err;
  73    }
  74    return [ptr, u.length];
  75  }
  76  // _trap wraps every call into WASM: a RuntimeError (e.g. call_indirect
  77  // out-of-bounds when the gc_leaking heap is exhausted) propagates as an
  78  // uncaught synchronous error and fires Worker onerror → restart loop.
  79  // try/catch here is the JS equivalent of defer/recover: WASM traps are
  80  // catchable as RuntimeErrors at the JS boundary even though Moxie's own
  81  // recover() cannot see them.
  82  // Track whether the WASM has fatally crashed so we only terminate once.
  83  let _wasmDead = false;
  84  function _trap(fn, label) {
  85    try { fn(); } catch (err) {
  86      if (!_wasmDead) {
  87        _wasmDead = true;
  88        const stack = err && err.stack ? err.stack : '(no stack)';
  89        console.error('[rproxy] WASM FATAL in ' + (label||'?') + ':', err, '\nstack:', stack);
  90        self.postMessage('["__WASM_FATAL","' + jsonEsc(String(err)) + ' [in ' + jsonEsc(label||'?') + '] stack: ' + jsonEsc(stack) + '"]');
  91      }
  92    }
  93  }
  94  function cb0(id) { _trap(() => xp.__cb0(id), 'cb0('+id+')'); }
  95  function cbs(id, s) {
  96    const [ptr, len] = writeStr(s);
  97    _trap(() => xp.__cbs(id, ptr, len), 'cbs('+id+',len='+len+')');
  98  }
  99  function cbdata(id, data) {
 100    const [ptr, len] = writeBytes(data);
 101    _trap(() => xp.__cbdata(id, ptr, len), 'cbdata('+id+',len='+len+')');
 102  }
 103  const h = { readStr, readBytes, writeStr, writeI32, writeBytes, cb0, cbs, cbdata };
 104  
 105  // Callback shapes not in core helpers.
 106  function cbb(id, val) { _trap(() => xp.__cbb(id, val ? 1 : 0), 'cbb('+id+')'); }
 107  function cbi(id, val) { _trap(() => xp.__cbi(id, val | 0), 'cbi('+id+','+val+')'); }
 108  function cbis(id, ival, s) {
 109    const [ptr, len] = writeStr(s == null ? '' : String(s));
 110    _trap(() => xp.__cbis(id, ival | 0, ptr, len), 'cbis('+id+',conn='+ival+',len='+len+')');
 111  }
 112  function cbiis(id, a, b, s) {
 113    const [ptr, len] = writeStr(s == null ? '' : String(s));
 114    _trap(() => xp.__cbiis(id, a | 0, b | 0, ptr, len), 'cbiis('+id+','+a+','+b+',len='+len+')');
 115  }
 116  
 117  let _workerMsgCBID = 0;
 118  
 119  function jsonEsc(s) {
 120    return String(s)
 121      .replace(/\\/g, '\\\\')
 122      .replace(/"/g, '\\"')
 123      .replace(/\n/g, '\\n')
 124      .replace(/\r/g, '\\r')
 125      .replace(/\t/g, '\\t');
 126  }
 127  
 128  function deliverToWasm(msg) {
 129    if (_workerMsgCBID && cbs) {
 130      cbs(_workerMsgCBID, msg);
 131    }
 132  }
 133  
 134  const bridge = {
 135    // --- relayproxy worker-internal bridge ---
 136    relayproxy_worker_on_message(cbID) {
 137      _workerMsgCBID = cbID;
 138    },
 139    relayproxy_worker_post(ptr, len) {
 140      self.postMessage(readStr(ptr, len));
 141    },
 142    relayproxy_worker_set_timeout(ms, cbID) {
 143      return setTimeout(() => cb0(cbID), ms);
 144    },
 145    relayproxy_worker_clear_timeout(handle) {
 146      clearTimeout(handle);
 147    },
 148    relayproxy_worker_now_seconds() {
 149      return BigInt(Math.floor(Date.now() / 1000));
 150    },
 151  
 152    // --- ws (WebSocket connections to remote relays; native WebSocket in Worker) ---
 153    ws_dial(up, ul, _uc, msgID, openID, closeID, errID) {
 154      const url = readStr(up, ul);
 155      return ws.Dial(url,
 156        (conn, s) => { cbis(msgID, conn, s); },
 157        (conn) => { cbi(openID, conn); },
 158        (conn, code, reason) => { cbiis(closeID, conn, code, reason); },
 159        (conn) => { cbi(errID, conn); },
 160      );
 161    },
 162    ws_send(conn, mp, ml, _mc) { return ws.Send(conn, readStr(mp, ml)) ? 1 : 0; },
 163    ws_close(conn) { ws.Close(conn); },
 164    ws_ready_state(conn) { return ws.ReadyState(conn); },
 165  
 166    // --- idb (worker has its own connection to the shared 'smesh' database) ---
 167    idb_set_enc_key(p, l, _c) { idb.SetEncKey(readStr(p, l)); },
 168    idb_open(cbID) { idb.Open(() => cb0(cbID)); },
 169    idb_save_event(p, l, _c, cbID) { idb.SaveEvent(readStr(p, l), (v) => cbb(cbID, v)); },
 170    idb_query_events(p, l, _c, cbID) { idb.QueryEvents(readStr(p, l), (s) => cbs(cbID, s)); },
 171    idb_save_dm(p, l, _c, cbID) { idb.SaveDM(readStr(p, l), (s) => cbs(cbID, s)); },
 172    idb_query_dms(pp, pl, _pc, limit, until, cbID) {
 173      idb.QueryDMs(readStr(pp, pl), limit, Number(until), (s) => cbs(cbID, s));
 174    },
 175    idb_get_conversation_list(cbID) { idb.GetConversationList((s) => cbs(cbID, s)); },
 176    idb_clear_dms_by_peer(p, l, _c, cbID) { idb.ClearDMsByPeer(readStr(p, l), () => cb0(cbID)); },
 177    idb_set_version(p, l, _c) { idb.SetVersion(readStr(p, l)); },
 178  
 179    // --- common (channel ops, subtle_random_bytes, spawn) ---
 180    ...makeCommonBridge(h, _sabTable, _sabSeqRef, _wasmUrlRef, _buildHashRef, _spawnedWorkerChans),
 181  };
 182  
 183  const wasi = makeWasi(() => mem);
 184  
 185  self.addEventListener('unhandledrejection', function(ev) {
 186    self.postMessage('["__ERROR","unhandledrejection: ' + jsonEsc(String(ev.reason)) + '"]');
 187  });
 188  
 189  self.onmessage = async function(e) {
 190    const d = e.data;
 191  
 192    if (d && d.type === 'init' && d.mode === 'root') {
 193      try {
 194        const wasmBytes = await fetch(d.wasmUrl, {cache: 'no-store'}).then(r => r.arrayBuffer());
 195        const buildHash = await computeBuildHash(wasmBytes);
 196        _wasmUrlRef.value = d.wasmUrl;
 197        _buildHashRef.value = buildHash;
 198  
 199        const { instance } = await WebAssembly.instantiate(wasmBytes,
 200          { bridge, wasi_snapshot_preview1: wasi });
 201        mem = instance.exports.memory;
 202        xp = instance.exports;
 203        _trap(() => xp._start(), '_start');
 204        // moxie main() calls relayproxy.WorkerPost(`["READY"]`) which routes
 205        // through self.postMessage above.
 206      } catch (err) {
 207        self.postMessage('["__ERROR","boot: ' + jsonEsc(String(err)) + '"]');
 208      }
 209      return;
 210    }
 211  
 212    // All non-init messages are page -> worker traffic; deliver as-is.
 213    if (typeof d === 'string') {
 214      deliverToWasm(d);
 215    }
 216  };
 217