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