store-wasm-host.mjs raw

   1  // store-wasm-host.mjs — Worker bootstrap for store.wasm.
   2  //
   3  // Owns the 'smesh' IndexedDB (events + DMs). Receives S_* messages from the
   4  // supervisor, executes IDB operations, returns SR_* responses.
   5  //
   6  // No WebSocket or DOM access. Bridge surface: store_worker_* + idb_*.
   7  
   8  import {
   9    makeCoreHelpers, makeWasi, makeCommonBridge,
  10    computeBuildHash,
  11  } from './bridge-common.mjs';
  12  import * as idb from './$runtime/idb.mjs';
  13  
  14  let mem, xp;
  15  const _sabTable = new Map();
  16  const _sabSeqRef = { value: 0 };
  17  const _spawnedWorkerChans = new Map();
  18  const _wasmUrlRef = { value: null };
  19  const _buildHashRef = { value: null };
  20  
  21  const _enc = new TextEncoder();
  22  const _dec = new TextDecoder();
  23  
  24  function readStr(ptr, len) {
  25    if (len <= 0) return '';
  26    try {
  27      return _dec.decode(new Uint8Array(mem.buffer, ptr >>> 0, len));
  28    } catch (err) {
  29      self.postMessage('["__ERROR","readStr ptr=' + ptr + ' len=' + len + ' err=' + String(err) + '"]');
  30      throw err;
  31    }
  32  }
  33  
  34  function writeStr(s) {
  35    const bytes = _enc.encode('' + s);
  36    const ptr = xp.__alloc(bytes.length) >>> 0;
  37    try {
  38      new Uint8Array(mem.buffer, ptr, bytes.length).set(bytes);
  39    } catch (err) {
  40      self.postMessage('["__ERROR","writeStr ptr=' + ptr + ' len=' + bytes.length + ' err=' + String(err) + '"]');
  41      throw err;
  42    }
  43    return [ptr, bytes.length];
  44  }
  45  
  46  function writeI32(addr, val) {
  47    new DataView(mem.buffer).setInt32(addr >>> 0, val, true);
  48  }
  49  
  50  function writeBytes(data) {
  51    const u = (data instanceof Uint8Array) ? data : (data && data.$array != null
  52      ? (() => { const u = new Uint8Array(data.$length); for (let i = 0; i < data.$length; i++) u[i] = data.$array[data.$offset + i]; return u; })()
  53      : (typeof data === 'string' ? _enc.encode(data) : new Uint8Array(0)));
  54    const ptr = xp.__alloc(u.length) >>> 0;
  55    new Uint8Array(mem.buffer, ptr, u.length).set(u);
  56    return [ptr, u.length];
  57  }
  58  
  59  function readBytes(ptr, len) {
  60    if (len <= 0) return new Uint8Array(0);
  61    return new Uint8Array(mem.buffer, ptr >>> 0, len);
  62  }
  63  
  64  let _wasmDead = false;
  65  function _trap(fn) {
  66    try { fn(); } catch (err) {
  67      if (!_wasmDead) {
  68        _wasmDead = true;
  69        self.postMessage('["__WASM_FATAL","' + jsonEsc(String(err)) + '"]');
  70      }
  71    }
  72  }
  73  
  74  function cb0(id) { _trap(() => xp.__cb0(id)); }
  75  function cbs(id, s) {
  76    const [ptr, len] = writeStr(s);
  77    _trap(() => xp.__cbs(id, ptr, len));
  78  }
  79  function cbb(id, val) { _trap(() => xp.__cbb(id, val ? 1 : 0)); }
  80  function cbss(id, s1, s2) {
  81    const [p1, l1] = writeStr(s1);
  82    const [p2, l2] = writeStr(s2);
  83    _trap(() => xp.__cbss(id, p1, l1, p2, l2));
  84  }
  85  function cbdata(id, data) {
  86    const [ptr, len] = writeBytes(data);
  87    _trap(() => xp.__cbdata(id, ptr, len));
  88  }
  89  
  90  const h = { readStr, readBytes, writeStr, writeI32, writeBytes, cb0, cbs, cbb, cbdata };
  91  
  92  let _workerMsgCBID = 0;
  93  
  94  function jsonEsc(s) {
  95    return String(s)
  96      .replace(/\\/g, '\\\\')
  97      .replace(/"/g, '\\"')
  98      .replace(/\n/g, '\\n')
  99      .replace(/\r/g, '\\r')
 100      .replace(/\t/g, '\\t');
 101  }
 102  
 103  function deliverToWasm(msg) {
 104    if (_workerMsgCBID) cbs(_workerMsgCBID, msg);
 105  }
 106  
 107  const bridge = {
 108    // --- store worker-internal bridge ---
 109    store_worker_on_message(cbID) {
 110      _workerMsgCBID = cbID;
 111    },
 112    store_worker_post(ptr, len) {
 113      self.postMessage(readStr(ptr, len));
 114    },
 115    store_worker_set_timeout(ms, cbID) {
 116      return setTimeout(() => cb0(cbID), ms);
 117    },
 118    store_worker_clear_timeout(handle) {
 119      clearTimeout(handle);
 120    },
 121    store_worker_now_seconds() {
 122      return BigInt(Math.floor(Date.now() / 1000));
 123    },
 124  
 125    // --- idb (IndexedDB for Nostr events and DMs) ---
 126    idb_set_enc_key(p, l, _c) { idb.SetEncKey(readStr(p, l)); },
 127    idb_open(cbID) { idb.Open(() => cb0(cbID)); },
 128    idb_save_event(p, l, _c, cbID) { idb.SaveEvent(readStr(p, l), (v) => cbb(cbID, v)); },
 129    idb_query_events(p, l, _c, cbID) { idb.QueryEvents(readStr(p, l), (s) => cbs(cbID, s)); },
 130    idb_save_dm(p, l, _c, cbID) { idb.SaveDM(readStr(p, l), (s) => cbs(cbID, s)); },
 131    idb_query_dms(pp, pl, _pc, limit, until, cbID) {
 132      idb.QueryDMs(readStr(pp, pl), limit, Number(until), (s) => cbs(cbID, s));
 133    },
 134    idb_get_conversation_list(cbID) { idb.GetConversationList((s) => cbs(cbID, s)); },
 135    idb_clear_dms_by_peer(p, l, _c, cbID) { idb.ClearDMsByPeer(readStr(p, l), () => cb0(cbID)); },
 136    idb_set_version(p, l, _c) { idb.SetVersion(readStr(p, l)); },
 137    idb_mls_save_group(gp, gl, _gc, sp, sl, _sc, cbID) {
 138      idb.MlsSaveGroup(readStr(gp, gl), readStr(sp, sl), () => cb0(cbID));
 139    },
 140    idb_mls_load_group(gp, gl, _gc, cbID) {
 141      idb.MlsLoadGroup(readStr(gp, gl), (s) => cbs(cbID, s));
 142    },
 143    idb_mls_list_groups(cbID) { idb.MlsListGroups((s) => cbs(cbID, s)); },
 144    idb_mls_save_kpp(p, l, _c, cbID) { idb.MlsSaveKPP(readStr(p, l), () => cb0(cbID)); },
 145    idb_mls_load_kpp(cbID) { idb.MlsLoadKPP((s) => cbs(cbID, s)); },
 146  
 147    // --- KV stores (profiles, settings, cache) ---
 148    idb_kv_get(sp, sl, _sc, kp, kl, _kc, cbID) {
 149      idb.KVGet(readStr(sp, sl), readStr(kp, kl), (v) => cbs(cbID, v));
 150    },
 151    idb_kv_put(sp, sl, _sc, kp, kl, _kc, vp, vl, _vc) {
 152      idb.KVPut(readStr(sp, sl), readStr(kp, kl), readStr(vp, vl));
 153    },
 154    idb_kv_get_all(sp, sl, _sc, fnID, doneID) {
 155      idb.KVGetAll(readStr(sp, sl), (k, v) => cbss(fnID, k, v), () => cb0(doneID));
 156    },
 157  
 158    // --- common (channel ops, spawn, subtle_random_bytes) ---
 159    ...makeCommonBridge(h, _sabTable, _sabSeqRef, _wasmUrlRef, _buildHashRef, _spawnedWorkerChans),
 160  };
 161  
 162  const wasi = makeWasi(() => mem);
 163  
 164  self.addEventListener('unhandledrejection', function(ev) {
 165    self.postMessage('["__ERROR","unhandledrejection: ' + jsonEsc(String(ev.reason)) + '"]');
 166  });
 167  
 168  self.onmessage = async function(e) {
 169    const d = e.data;
 170  
 171    if (d && d.type === 'init' && d.mode === 'root') {
 172      try {
 173        const wasmBytes = await fetch(d.wasmUrl, { cache: 'no-store' }).then(r => r.arrayBuffer());
 174        const buildHash = await computeBuildHash(wasmBytes);
 175        _wasmUrlRef.value = d.wasmUrl;
 176        _buildHashRef.value = buildHash;
 177  
 178        const { instance } = await WebAssembly.instantiate(wasmBytes,
 179          { bridge, wasi_snapshot_preview1: wasi });
 180        mem = instance.exports.memory;
 181        xp = instance.exports;
 182        _trap(() => xp._start());
 183      } catch (err) {
 184        self.postMessage('["__ERROR","boot: ' + jsonEsc(String(err)) + '"]');
 185      }
 186      return;
 187    }
 188  
 189    if (typeof d === 'string') {
 190      deliverToWasm(d);
 191    }
 192  };
 193