wasm-app-worker-host.mjs raw
1 // wasm-app-worker-host.mjs — Worker bootstrap for app.wasm.
2 // Runs in a Worker; the main page thread (wasm-host.mjs) is the supervisor.
3 //
4 // Bridge proxy protocol (Worker → supervisor):
5 // { type: 'dom-sync', op, args?, sab } — sync DOM call, Atomics.wait on sab
6 // { type: 'localstorage-sync', op, args, sab } — sync localStorage read
7 // { type: 'dom', op, args? } — fire-and-forget DOM call
8 // { type: 'localstorage', op, args } — fire-and-forget localStorage write/remove
9 // { type: 'dom-register', op, cbID } — register event callback with supervisor
10 // { type: 'signer-async', op, args?, cbID, cbType } — async signer call
11 //
12 // Callback protocol (supervisor → Worker):
13 // { type: '__cb0', id } — void callback
14 // { type: '__cbs', id, str } — string callback
15 // { type: '__cbb', id, val } — bool callback
16 // { type: '__cbdata', id, bytes } — bytes callback (ArrayBuffer)
17 //
18 // Sync SAB layout (Int32Array view):
19 // [0]: done flag (0=waiting, 1=done)
20 // [1]: int32 result
21 // [2]: string byte length (0 if no string result)
22 // bytes[12..]: string result bytes
23
24 import {
25 makeCoreHelpers, makeWasi, makeCommonBridge,
26 computeBuildHash, createSpawnedWorker,
27 } from './bridge-common.mjs';
28
29 let mem, xp;
30 const _sabTable = new Map();
31 const _sabSeqRef = { value: 0 };
32 const _spawnedWorkerChans = new Map();
33 const _wasmUrlRef = { value: null };
34 const _buildHashRef = { value: null };
35
36 const h = makeCoreHelpers(() => mem, () => xp);
37 const { readStr, readBytes, writeStr, writeI32, writeBytes, cb0, cbs, cbb, cbdata } = h;
38
39 // ── Sync proxy helper ──────────────────────────────────────────────────────
40
41 // Allocate a reusable sync SAB (12-byte header + 512KB string buffer).
42 // Rendered note HTML can exceed 8KB for long posts; 512KB covers all cases.
43 const _syncSab = new SharedArrayBuffer(12 + 512 * 1024);
44 const _syncI32 = new Int32Array(_syncSab);
45
46 function syncCallInt(type, op, args) {
47 _syncI32[0] = 0;
48 self.postMessage({ type, op, args, sab: _syncSab });
49 Atomics.wait(_syncI32, 0, 0);
50 return _syncI32[1];
51 }
52
53 function syncCallStr(type, op, args) {
54 _syncI32[0] = 0;
55 self.postMessage({ type, op, args, sab: _syncSab });
56 Atomics.wait(_syncI32, 0, 0);
57 const len = _syncI32[2];
58 if (len <= 0) return '';
59 // Copy from SAB to regular ArrayBuffer before decoding:
60 // Firefox prohibits TextDecoder.decode on SAB-backed Uint8Array views.
61 const copy = new Uint8Array(len);
62 copy.set(new Uint8Array(_syncSab, 12, len));
63 return new TextDecoder().decode(copy);
64 }
65
66 // ── Bridge ─────────────────────────────────────────────────────────────────
67
68 // Helpers for the various bridge patterns
69 function ffdom(op, args) { self.postMessage({ type: 'dom', op, args }); }
70 function syncDomInt(op, args) { return syncCallInt('dom-sync', op, args); }
71 function syncDomStr(op, args, rPtrAddr, rLenAddr) {
72 const str = syncCallStr('dom-sync-str', op, args);
73 const [ptr, len] = writeStr(str);
74 writeI32(rPtrAddr, ptr); writeI32(rLenAddr, len);
75 }
76 function cbdom(op, args, cbID, extra) {
77 self.postMessage({ type: 'dom-cb', op, args, cbID, ...extra });
78 }
79 function sigAsync(op, args, cbID, cbType) {
80 self.postMessage({ type: 'signer-async', op, args, cbID, cbType });
81 }
82
83 // relayproxy: forward JSON-array messages to the relay-proxy worker via
84 // the supervisor. Inbound messages from the worker arrive as
85 // '__relayproxy_msg' and dispatch to the cbID registered by
86 // relayproxy_on_message.
87 let _relayProxyMsgCBID = 0;
88 function relayProxySend(msg) {
89 self.postMessage({ type: 'relayproxy-send', msg });
90 }
91
92 // Domain worker message cbIDs: set by X_on_message bridge functions.
93 let _profileMsgCBID = 0, _feedMsgCBID = 0, _mlsMsgCBID = 0, _notifMsgCBID = 0;
94
95 const bridge = {
96 // ── DOM sync int ─────────────────────────────────────────────────────────
97 dom_body() { return syncDomInt('body', null); },
98 dom_create_element(ptr, len) { return syncDomInt('create_element', [readStr(ptr, len)]); },
99 dom_create_text_node(ptr, len) { return syncDomInt('create_text_node', [readStr(ptr, len)]); },
100 dom_get_element_by_id(ptr, len) { return syncDomInt('get_element_by_id', [readStr(ptr, len)]); },
101 dom_query_selector(ptr, len) { return syncDomInt('query_selector', [readStr(ptr, len)]); },
102 dom_query_selector_from(root, ptr, len) { return syncDomInt('query_selector_from', [root, readStr(ptr, len)]); },
103 dom_prefers_dark() { return syncDomInt('prefers_dark', null); },
104 dom_first_child(el) { return syncDomInt('first_child', [el]); },
105 dom_first_element_child(el) { return syncDomInt('first_element_child', [el]); },
106 dom_next_sibling(el) { return syncDomInt('next_sibling', [el]); },
107 dom_bounding_client_left(el) { return syncDomInt('bounding_client_left', [el]); },
108 dom_get_viewport_height() { return syncDomInt('get_viewport_height', null); },
109 dom_get_viewport_width() { return syncDomInt('get_viewport_width', null); },
110 // NowSeconds returns int64 — WASM expects BigInt return value
111 dom_now_seconds() { return BigInt(syncDomInt('now_seconds', null)); },
112 dom_timezone_offset_seconds() { return syncDomInt('timezone_offset_seconds', null); },
113 dom_confirm(ptr, len) { return syncDomInt('confirm', [readStr(ptr, len)]); },
114 dom_set_timeout(cbID, ms) { return syncDomInt('set_timeout', [ms, cbID]); },
115
116 // ── DOM sync string (outptrs) ────────────────────────────────────────────
117 dom_hostname(rPtrAddr, rLenAddr) { syncDomStr('hostname', null, rPtrAddr, rLenAddr); },
118 dom_port(rPtrAddr, rLenAddr) { syncDomStr('port', null, rPtrAddr, rLenAddr); },
119 dom_user_agent(rPtrAddr, rLenAddr) { syncDomStr('user_agent', null, rPtrAddr, rLenAddr); },
120 dom_get_path(rPtrAddr, rLenAddr) { syncDomStr('get_path', null, rPtrAddr, rLenAddr); },
121 dom_get_hash(rPtrAddr, rLenAddr) { syncDomStr('get_hash', null, rPtrAddr, rLenAddr); },
122 dom_get_property(el, pPtr, pLen, pCap, rPtrAddr, rLenAddr) {
123 syncDomStr('get_property', [el, readStr(pPtr, pLen)], rPtrAddr, rLenAddr);
124 },
125 dom_get_attribute(el, nPtr, nLen, nCap, rPtrAddr, rLenAddr) {
126 syncDomStr('get_attribute', [el, readStr(nPtr, nLen)], rPtrAddr, rLenAddr);
127 },
128
129 // ── DOM fire-and-forget ──────────────────────────────────────────────────
130 dom_console_log(ptr, len) { ffdom('console_log', [readStr(ptr, len)]); },
131 dom_append_child(parent, child) { ffdom('append_child', [parent, child]); },
132 dom_remove_child(parent, child) { ffdom('remove_child', [parent, child]); },
133 dom_remove(el) { ffdom('remove', [el]); },
134 dom_insert_before(parent, n, ref) { ffdom('insert_before', [parent, n, ref]); },
135 dom_set_attribute(el, nPtr, nLen, nCap, vPtr, vLen) {
136 ffdom('set_attribute', [el, readStr(nPtr, nLen), readStr(vPtr, vLen)]);
137 },
138 dom_remove_attribute(el, nPtr, nLen, nCap) { ffdom('remove_attribute', [el, readStr(nPtr, nLen)]); },
139 dom_set_text_content(el, ptr, len) { ffdom('set_text_content', [el, readStr(ptr, len)]); },
140 dom_set_inner_html(el, ptr, len) { ffdom('set_inner_html', [el, readStr(ptr, len)]); },
141 dom_set_style(el, pPtr, pLen, pCap, vPtr, vLen) {
142 ffdom('set_style', [el, readStr(pPtr, pLen), readStr(vPtr, vLen)]);
143 },
144 dom_set_property(el, pPtr, pLen, pCap, vPtr, vLen) {
145 ffdom('set_property', [el, readStr(pPtr, pLen), readStr(vPtr, vLen)]);
146 },
147 dom_add_class(el, ptr, len) { ffdom('add_class', [el, readStr(ptr, len)]); },
148 dom_remove_class(el, ptr, len) { ffdom('remove_class', [el, readStr(ptr, len)]); },
149 dom_focus(el) { ffdom('focus', [el]); },
150 dom_release_element(el) { ffdom('release_element', [el]); },
151 dom_release_all(ptr, len) { ffdom('release_all', [Array.from(new Int32Array(mem.buffer, ptr, len))]); },
152 dom_release_children(el) { ffdom('release_children', [el]); },
153 dom_push_state(ptr, len) { ffdom('push_state', [readStr(ptr, len)]); },
154 dom_replace_state(ptr, len) { ffdom('replace_state', [readStr(ptr, len)]); },
155 dom_back() { ffdom('back', null); },
156 dom_location_reload() { ffdom('location_reload', null); },
157 dom_location_assign(ptr, len) { ffdom('location_assign', [readStr(ptr, len)]); },
158 dom_hard_refresh() { ffdom('hard_refresh', null); },
159 dom_clear_timeout(id) { ffdom('clear_timeout', [id]); },
160 dom_clear_storage_prefix(ptr, len) { ffdom('clear_storage_prefix', [readStr(ptr, len)]); },
161 dom_idb_set_enc_key(ptr, len) { ffdom('idb_set_enc_key', [readStr(ptr, len)]); },
162 dom_idb_put(sPtr, sLen, sC, kPtr, kLen, kC, vPtr, vLen) {
163 ffdom('idb_put', [readStr(sPtr, sLen), readStr(kPtr, kLen), readStr(vPtr, vLen)]);
164 },
165 dom_post_to_sw(ptr, len) { ffdom('post_to_sw', [readStr(ptr, len)]); },
166 dom_download_text(fnPtr, fnLen, fnC, cPtr, cLen, cC, mPtr, mLen) {
167 ffdom('download_text', [readStr(fnPtr, fnLen), readStr(cPtr, cLen), readStr(mPtr, mLen)]);
168 },
169 dom_insert_mention_chip(el, charCount, npubPtr, npubLen, npubC, nPtr, nLen, nC, picPtr, picLen) {
170 ffdom('insert_mention_chip', [el, charCount, readStr(npubPtr, npubLen), readStr(nPtr, nLen), readStr(picPtr, picLen)]);
171 },
172
173 // ── DOM async callbacks ──────────────────────────────────────────────────
174 dom_add_event_listener(el, evPtr, evLen, evC, cbID) {
175 cbdom('add_event_listener', [el, readStr(evPtr, evLen)], cbID);
176 },
177 dom_add_self_event_listener(el, evPtr, evLen, evC, cbID) {
178 cbdom('add_self_event_listener', [el, readStr(evPtr, evLen)], cbID);
179 },
180 dom_add_enter_key_listener(el, cbID) { cbdom('add_enter_key_listener', [el], cbID); },
181 dom_remove_event_listener(el, evPtr, evLen, evC, cbID) {
182 cbdom('remove_event_listener', [el, readStr(evPtr, evLen)], cbID);
183 },
184 dom_on_pop_state(cbID) { cbdom('on_pop_state', null, cbID); },
185 dom_intercept_internal_links(cbID) { cbdom('intercept_internal_links', null, cbID); },
186 dom_on_sw_message(cbID) { cbdom('on_sw_message', null, cbID); },
187 dom_request_animation_frame(cbID) { cbdom('request_animation_frame', null, cbID); },
188 dom_observe_resize(el, cbID) { cbdom('observe_resize', [el], cbID); },
189 dom_unobserve_resize(el) { ffdom('unobserve_resize', [el]); },
190 dom_on_pull_refresh(el, indEl, cbID) { cbdom('on_pull_refresh', [el, indEl], cbID); },
191 dom_on_paste_image(el, cbID) { cbdom('on_paste_image', [el], cbID); },
192 dom_on_drop_image(el, cbID) { cbdom('on_drop_image', [el], cbID); },
193 dom_pick_file_text(aPtr, aLen, aC, cbID) { cbdom('pick_file_text', [readStr(aPtr, aLen)], cbID); },
194 dom_pick_file_base64(aPtr, aLen, aC, cbID) { cbdom('pick_file_base64', [readStr(aPtr, aLen)], cbID); },
195 dom_fetch_text(uPtr, uLen, uC, cbID) { cbdom('fetch_text', [readStr(uPtr, uLen)], cbID); },
196 dom_fetch_relay_info(uPtr, uLen, uC, cbID) { cbdom('fetch_relay_info', [readStr(uPtr, uLen)], cbID); },
197 dom_fetch_put_blob_base64(uPtr, uLen, uC, bPtr, bLen, bC, ctPtr, ctLen, ctC, ahPtr, ahLen, ahC, cbID) {
198 cbdom('fetch_put_blob_base64', [readStr(uPtr, uLen), readStr(bPtr, bLen), readStr(ctPtr, ctLen), readStr(ahPtr, ahLen)], cbID);
199 },
200 dom_read_clipboard(cbID) { cbdom('read_clipboard', null, cbID); },
201 dom_write_clipboard(ptr, len, cap, cbID) { cbdom('write_clipboard', [readStr(ptr, len)], cbID); },
202 dom_write_primary_selection(ptr, len, cap) { cbdom('write_primary_selection', [readStr(ptr, len)], 0); },
203 dom_copy_image_to_clipboard(el, cbID) { cbdom('copy_image_to_clipboard', [el], cbID); },
204 dom_window_open(ptr, len, cap) { cbdom('window_open', [readStr(ptr, len)], 0); },
205 dom_encode_uri_component(ptr, len, cap, rPtr, rLen) {
206 const s = encodeURIComponent(readStr(ptr, len));
207 const [p, l] = writeStr(s); writeI32(rPtr, p); writeI32(rLen, l);
208 },
209 dom_idb_get(sPtr, sLen, sC, kPtr, kLen, kC, cbID) {
210 cbdom('idb_get', [readStr(sPtr, sLen), readStr(kPtr, kLen)], cbID);
211 },
212 dom_idb_get_all(sPtr, sLen, sC, eachCB, doneCB) {
213 cbdom('idb_get_all', [readStr(sPtr, sLen)], eachCB, { doneCB });
214 },
215 dom_get_bounding_rect(el, cbID) { cbdom('get_bounding_rect', [el], cbID); },
216 dom_on_keydown(el, cbID) { cbdom('on_keydown', [el], cbID); },
217
218 // ── Markdown ──────────────────────────────────────────────────────────────
219 markdown_render(tPtr, tLen, tCap, rPtrAddr, rLenAddr) {
220 // markdown_render is sync (pure computation in runtime); proxy to supervisor
221 const html = syncCallStr('markdown', 'render', [readStr(tPtr, tLen)]);
222 const [ptr, len] = writeStr(html);
223 writeI32(rPtrAddr, ptr); writeI32(rLenAddr, len);
224 },
225
226 // ── localStorage ─────────────────────────────────────────────────────────
227 localstorage_get_item(kPtr, kLen, kC, rPtrAddr, rLenAddr) {
228 const str = syncCallStr('localstorage-sync', 'get_item', [readStr(kPtr, kLen)]);
229 const [ptr, len] = writeStr(str);
230 writeI32(rPtrAddr, ptr); writeI32(rLenAddr, len);
231 },
232 localstorage_set_item(kPtr, kLen, kC, vPtr, vLen) {
233 self.postMessage({ type: 'localstorage', op: 'set_item', args: [readStr(kPtr, kLen), readStr(vPtr, vLen)] });
234 },
235 localstorage_remove_item(kPtr, kLen) {
236 self.postMessage({ type: 'localstorage', op: 'remove_item', args: [readStr(kPtr, kLen)] });
237 },
238
239 // ── WebSocket ─────────────────────────────────────────────────────────────
240 ws_dial(uPtr, uLen, uC, onMsgCB, onOpenCB, onCloseCB, onErrCB) {
241 return syncCallInt('ws', 'dial', [readStr(uPtr, uLen), onMsgCB, onOpenCB, onCloseCB, onErrCB]);
242 },
243 ws_send(conn, ptr, len, cap) {
244 return syncCallInt('ws', 'send', [conn, readStr(ptr, len)]);
245 },
246 ws_close(conn) { self.postMessage({ type: 'ws', op: 'close', args: [conn] }); },
247 ws_ready_state(conn) { return syncCallInt('ws', 'ready_state', [conn]); },
248
249 // ── Signer ────────────────────────────────────────────────────────────────
250 signer_has_signer() { return syncCallInt('signer-sync', 'has_signer', null); },
251 signer_has_mls() { return syncCallInt('signer-sync', 'has_mls', null); },
252 // Persistent state channel: fires on every vault/identity state change
253 signer_on_state_change(cbID) { sigAsync('on_state_change', null, cbID, 'cbs'); },
254 signer_is_installed(cbID) { sigAsync('is_installed', null, cbID, 'cbb'); },
255 signer_get_public_key(cbID) { sigAsync('get_public_key', null, cbID, 'cbs'); },
256 signer_sign_event(ptr, len, cap, cbID) { sigAsync('sign_event', [readStr(ptr, len)], cbID, 'cbs'); },
257 signer_get_shared_secret(pPtr, pLen, pC, cbID) { sigAsync('get_shared_secret', [readStr(pPtr, pLen)], cbID, 'cbs'); },
258 signer_get_vault_status(cbID) { sigAsync('get_vault_status', null, cbID, 'cbs'); },
259 signer_nip04_encrypt(ppPtr, ppLen, ppC, ptPtr, ptLen, ptC, cbID) {
260 sigAsync('nip04_encrypt', [readStr(ppPtr, ppLen), readStr(ptPtr, ptLen)], cbID, 'cbs');
261 },
262 signer_nip04_decrypt(ppPtr, ppLen, ppC, ctPtr, ctLen, ctC, cbID) {
263 sigAsync('nip04_decrypt', [readStr(ppPtr, ppLen), readStr(ctPtr, ctLen)], cbID, 'cbs');
264 },
265 signer_nip44_encrypt(ppPtr, ppLen, ppC, ptPtr, ptLen, ptC, cbID) {
266 sigAsync('nip44_encrypt', [readStr(ppPtr, ppLen), readStr(ptPtr, ptLen)], cbID, 'cbs');
267 },
268 signer_nip44_decrypt(ppPtr, ppLen, ppC, ctPtr, ctLen, ctC, cbID) {
269 sigAsync('nip44_decrypt', [readStr(ppPtr, ppLen), readStr(ctPtr, ctLen)], cbID, 'cbs');
270 },
271 signer_switch_identity(pPtr, pLen, pC, cbID) { sigAsync('switch_identity', [readStr(pPtr, pLen)], cbID, 'cbb'); },
272 signer_create_vault(pPtr, pLen, pC, cbID) { sigAsync('create_vault', [readStr(pPtr, pLen)], cbID, 'cbb'); },
273 signer_unlock_vault(pPtr, pLen, pC, cbID) { sigAsync('unlock_vault', [readStr(pPtr, pLen)], cbID, 'cbb'); },
274 signer_last_unlock_error(cbID) { sigAsync('last_unlock_error', null, cbID, 'cbs'); },
275 signer_lock_vault(cbID) { sigAsync('lock_vault', null, cbID, 'cb0'); },
276 signer_list_identities(cbID) { sigAsync('list_identities', null, cbID, 'cbs'); },
277 signer_add_identity(nPtr, nLen, nC, cbID) { sigAsync('add_identity', [readStr(nPtr, nLen)], cbID, 'cbb'); },
278 signer_remove_identity(pPtr, pLen, pC, cbID) { sigAsync('remove_identity', [readStr(pPtr, pLen)], cbID, 'cbb'); },
279 signer_export_vault(pPtr, pLen, pC, cbID) { sigAsync('export_vault', [readStr(pPtr, pLen)], cbID, 'cbs'); },
280 signer_import_vault(dPtr, dLen, dC, cbID) { sigAsync('import_vault', [readStr(dPtr, dLen)], cbID, 'cbb'); },
281 signer_is_hd(cbID) { sigAsync('is_hd', null, cbID, 'cbb'); },
282 signer_get_mnemonic(cbID) { sigAsync('get_mnemonic', null, cbID, 'cbs'); },
283 signer_validate_mnemonic(mPtr, mLen, mC, cbID) { sigAsync('validate_mnemonic', [readStr(mPtr, mLen)], cbID, 'cbb'); },
284 signer_generate_mnemonic(cbID) { sigAsync('generate_mnemonic', null, cbID, 'cbs'); },
285 signer_create_hd_vault(pPtr, pLen, pC, mPtr, mLen, mC, cbID) {
286 sigAsync('create_hd_vault', [readStr(pPtr, pLen), readStr(mPtr, mLen)], cbID, 'cbs');
287 },
288 signer_restore_hd_vault(pPtr, pLen, pC, mPtr, mLen, mC, nPtr, nLen, nC, cbID) {
289 sigAsync('restore_hd_vault', [readStr(pPtr, pLen), readStr(mPtr, mLen), readStr(nPtr, nLen)], cbID, 'cbb');
290 },
291 signer_derive_identity(nPtr, nLen, nC, cbID) { sigAsync('derive_identity', [readStr(nPtr, nLen)], cbID, 'cbs'); },
292 signer_nsec_login(nPtr, nLen, nC, cbID) { sigAsync('nsec_login', [readStr(nPtr, nLen)], cbID, 'cbb'); },
293 // wasmProbeAccount(index int32, cbID int32) — takes int not string ptr
294 signer_probe_account(index, cbID) { sigAsync('probe_account', [index], cbID, 'cbs'); },
295 signer_reset_extension(cbID) { sigAsync('reset_extension', null, cbID, 'cbb'); },
296 signer_nwc_list(cbID) { sigAsync('nwc_list', null, cbID, 'cbs'); },
297 signer_nwc_add(uPtr, uLen, uC, aPtr, aLen, aC, createdAt, cbID) {
298 sigAsync('nwc_add', [readStr(uPtr, uLen), readStr(aPtr, aLen), Number(createdAt)], cbID, 'cbs');
299 },
300 signer_nwc_remove(iPtr, iLen, iC, cbID) { sigAsync('nwc_remove', [readStr(iPtr, iLen)], cbID, 'cbb'); },
301 signer_nwc_build_request(iPtr, iLen, iC, mPtr, mLen, mC, pPtr, pLen, pC, ePtr, eLen, eC, createdAt, cbID) {
302 sigAsync('nwc_build_request', [readStr(iPtr, iLen), readStr(mPtr, mLen), readStr(pPtr, pLen), readStr(ePtr, eLen), Number(createdAt)], cbID, 'cbs');
303 },
304 signer_nwc_parse_response(iPtr, iLen, iC, ctPtr, ctLen, ctC, ePtr, eLen, eC, cbID) {
305 sigAsync('nwc_parse_response', [readStr(iPtr, iLen), readStr(ctPtr, ctLen), readStr(ePtr, eLen)], cbID, 'cbs');
306 },
307 signer_ecdh_with_secret(skPtr, skLen, skC, pkPtr, pkLen, pkC, cbID) {
308 sigAsync('smesh.ecdhWithSecret', [readStr(skPtr, skLen), readStr(pkPtr, pkLen)], cbID, 'cbs');
309 },
310 signer_sign_with_secret(skPtr, skLen, skC, evPtr, evLen, evC, cbID) {
311 sigAsync('smesh.signWithSecret', [readStr(skPtr, skLen), readStr(evPtr, evLen)], cbID, 'cbs');
312 },
313 signer_pubkey_from_secret(skPtr, skLen, skC, cbID) {
314 sigAsync('smesh.pubkeyFromSecret', [readStr(skPtr, skLen)], cbID, 'cbs');
315 },
316
317 // ── Relay-proxy worker (consumer side) ────────────────────────────────────
318 relayproxy_send(ptr, len) {
319 relayProxySend(readStr(ptr, len));
320 },
321 relayproxy_on_message(cbID) {
322 _relayProxyMsgCBID = cbID;
323 },
324
325 // ── Domain workers (consumer side) ───────────────────────────────────────
326 profile_send(ptr, len) { self.postMessage({ type: 'profile-send', msg: readStr(ptr, len) }); },
327 profile_on_message(cbID) { _profileMsgCBID = cbID; },
328 feed_send(ptr, len) { self.postMessage({ type: 'feed-send', msg: readStr(ptr, len) }); },
329 feed_on_message(cbID) { _feedMsgCBID = cbID; },
330 mlsw_send(ptr, len) { self.postMessage({ type: 'mls-send', msg: readStr(ptr, len) }); },
331 mlsw_on_message(cbID) { _mlsMsgCBID = cbID; },
332 notif_send(ptr, len) { self.postMessage({ type: 'notif-send', msg: readStr(ptr, len) }); },
333 notif_on_message(cbID) { _notifMsgCBID = cbID; },
334 // Worker-internal side stubs (dead-code-eliminated when linked as consumer)
335 profile_worker_on_message() {}, profile_worker_post() {},
336 profile_worker_set_timeout() { return 0; }, profile_worker_clear_timeout() {},
337 profile_worker_now_seconds() { return BigInt(0); },
338 feed_worker_on_message() {}, feed_worker_post() {},
339 feed_worker_set_timeout() { return 0; }, feed_worker_clear_timeout() {},
340 feed_worker_now_seconds() { return BigInt(0); },
341 mlsw_worker_on_message() {}, mlsw_worker_post() {},
342 mlsw_worker_set_timeout() { return 0; }, mlsw_worker_clear_timeout() {},
343 mlsw_worker_now_seconds() { return BigInt(0); },
344 notif_worker_on_message() {}, notif_worker_post() {},
345 notif_worker_set_timeout() { return 0; }, notif_worker_clear_timeout() {},
346 notif_worker_now_seconds() { return BigInt(0); },
347
348 // ── Common: SAB channels, spawn, subtle_random_bytes ──────────────────────
349 ...makeCommonBridge(h, _sabTable, _sabSeqRef, _wasmUrlRef, _buildHashRef, _spawnedWorkerChans),
350 };
351
352 const wasi = makeWasi(() => mem);
353
354 // ── Callback dispatch helpers ───────────────────────────────────────────────
355 // Wrap every WASM call in try-catch: xp.__cb* can trap (unreachable),
356 // and inside an async onmessage the exception becomes an unhandled promise
357 // rejection that neither worker.onerror nor the browser console catches.
358
359 function _dispatch(fn) {
360 try { fn(); } catch (err) {
361 self.postMessage({ type: 'error', fatal: false,
362 message: '[cb-dispatch] ' + String(err) + (err && err.stack ? '\n' + err.stack : '') });
363 }
364 }
365
366 self.addEventListener('unhandledrejection', function(ev) {
367 self.postMessage({ type: 'error', fatal: false,
368 message: '[unhandledrejection] ' + String(ev.reason) });
369 });
370
371 // ── Message handler ────────────────────────────────────────────────────────
372
373 self.onmessage = async function(e) {
374 const d = e.data;
375
376 // Callback dispatch from supervisor
377 if (d.type === '__cb_release') {
378 if (xp && xp.__cb_release) {
379 for (let i = 0; i < d.ids.length; i++) {
380 try { xp.__cb_release(d.ids[i]); } catch(_) {}
381 }
382 }
383 return;
384 }
385 if (d.type === '__cb0') { _dispatch(() => cb0(d.id)); return; }
386 if (d.type === '__cbs') { _dispatch(() => cbs(d.id, d.str)); return; }
387 if (d.type === '__cbb') { _dispatch(() => xp.__cbb(d.id, d.val)); return; }
388 if (d.type === '__cbdata') { _dispatch(() => cbdata(d.id, new Uint8Array(d.bytes))); return; }
389 if (d.type === '__cbss') {
390 _dispatch(() => {
391 const [p1, l1] = writeStr(d.s1); const [p2, l2] = writeStr(d.s2);
392 xp.__cbss(d.id, p1, l1, p2, l2);
393 }); return;
394 }
395 if (d.type === '__cbii') { _dispatch(() => xp.__cbii(d.id, d.a, d.b)); return; }
396 if (d.type === '__cb6i') { _dispatch(() => xp.__cb6i(d.id, d.a, d.b, d.c, d.d, d.e, d.f)); return; }
397 if (d.type === '__keydown') {
398 if (xp && xp.__hook_keydown) {
399 _dispatch(() => { const [ptr, len] = writeStr(d.key); xp.__hook_keydown(d.id, ptr, len); });
400 }
401 return;
402 }
403 // WebSocket callbacks
404 if (d.type === '__ws-msg') {
405 _dispatch(() => { const [ptr, len] = writeStr(d.msg); xp.__cbis(d.id, d.connId, ptr, len); }); return;
406 }
407 if (d.type === '__ws-open') { _dispatch(() => xp.__cbi(d.id, d.connId)); return; }
408 if (d.type === '__ws-close') {
409 _dispatch(() => { const [ptr, len] = writeStr(d.reason || ''); xp.__cbiis(d.id, d.connId, d.code || 0, ptr, len); }); return;
410 }
411 if (d.type === '__ws-err') { _dispatch(() => xp.__cbi(d.id, d.connId)); return; }
412 // Signer state push (persistent channel): supervisor fires this whenever state changes
413 if (d.type === '__signer-state') { _dispatch(() => cbs(d.id, d.state)); return; }
414 // Relay-proxy worker message dispatch: supervisor relays JSON-array strings.
415 if (d.type === '__relayproxy_msg') {
416 if (_relayProxyMsgCBID) {
417 _dispatch(() => cbs(_relayProxyMsgCBID, d.msg || ''));
418 }
419 return;
420 }
421 // Domain worker message dispatch
422 if (d.type === '__profile_msg') { if (_profileMsgCBID) _dispatch(() => cbs(_profileMsgCBID, d.msg || '')); return; }
423 if (d.type === '__feed_msg') { if (_feedMsgCBID) _dispatch(() => cbs(_feedMsgCBID, d.msg || '')); return; }
424 if (d.type === '__mls_msg') { if (_mlsMsgCBID) _dispatch(() => cbs(_mlsMsgCBID, d.msg || '')); return; }
425 if (d.type === '__notif_msg') { if (_notifMsgCBID) _dispatch(() => cbs(_notifMsgCBID, d.msg || '')); return; }
426 if (d.type === '__mem_query') {
427 if (!xp || !mem) return;
428 const reply = { type: '__mem_reply' };
429 if (typeof xp.__moxie_total_alloc === 'function') {
430 reply.totalAlloc = Number(xp.__moxie_total_alloc());
431 reply.mallocs = Number(xp.__moxie_mallocs());
432 reply.heapPtr = xp.__moxie_heap_ptr() >>> 0;
433 }
434 if (typeof xp.__moxie_alloc_n_sites === 'function') {
435 const n = xp.__moxie_alloc_n_sites();
436 if (n > 0) {
437 const countersPtr = xp.__moxie_alloc_counters_ptr() >>> 0;
438 const sizesPtr = xp.__moxie_alloc_sizes_ptr() >>> 0;
439 reply.allocN = n;
440 reply.allocCounts = Array.from(new BigUint64Array(mem.buffer, countersPtr, n), v => Number(v));
441 reply.allocSizes = Array.from(new BigUint64Array(mem.buffer, sizesPtr, n), v => Number(v));
442 }
443 }
444 self.postMessage(reply);
445 return;
446 }
447
448 if (d.type === 'init' && d.mode === 'root') {
449 try {
450 const wasmBytes = await fetch(d.wasmUrl, {cache: 'no-store'}).then(r => r.arrayBuffer());
451 const buildHash = await computeBuildHash(wasmBytes);
452 _wasmUrlRef.value = d.wasmUrl;
453 _buildHashRef.value = buildHash;
454 self.postMessage({ type: 'hello', buildHash });
455
456 const { instance } = await WebAssembly.instantiate(wasmBytes,
457 { bridge, wasi_snapshot_preview1: wasi });
458 mem = instance.exports.memory;
459 xp = instance.exports;
460 xp._start();
461 self.postMessage({ type: 'ready' });
462 } catch (err) {
463 self.postMessage({ type: 'error', fatal: true, message: String(err) });
464 }
465 return;
466 }
467
468 if (d.type === 'init' && d.mode === 'spawn') {
469 try {
470 const wasmBytes = await fetch(d.wasmUrl, {cache: 'no-store'}).then(r => r.arrayBuffer());
471 const childHash = await computeBuildHash(wasmBytes);
472 if (d.buildHash && childHash !== d.buildHash) {
473 self.postMessage({ type: 'error', fatal: true,
474 message: 'spawn_domain: build hash mismatch' });
475 self.close(); return;
476 }
477 _wasmUrlRef.value = d.wasmUrl;
478 _buildHashRef.value = childHash;
479 const { instance } = await WebAssembly.instantiate(wasmBytes,
480 { bridge, wasi_snapshot_preview1: wasi });
481 mem = instance.exports.memory;
482 xp = instance.exports;
483
484 const chanSabs = d.chanSabs || [], argBytes = d.argBytes || new Uint8Array(0);
485 const argPtr = argBytes.length > 0 ? xp.__alloc(argBytes.length) : 0;
486 if (argBytes.length > 0) new Uint8Array(mem.buffer, argPtr, argBytes.length).set(argBytes);
487 const nChans = chanSabs.length;
488 const chanHandlesPtr = nChans > 0 ? xp.__alloc(nChans * 4) : 0;
489 if (nChans > 0) {
490 const dv = new DataView(mem.buffer);
491 chanSabs.forEach((sab, idx) => {
492 const handle = _sabSeqRef.value++;
493 _sabTable.set(handle, sab);
494 dv.setInt32(chanHandlesPtr + idx * 4, handle, true);
495 });
496 }
497 xp.__spawn_entry(d.fnIdx, argPtr, argBytes.length, chanHandlesPtr, nChans);
498 self.postMessage({ type: 'exit' });
499 } catch (err) {
500 self.postMessage({ type: 'error', fatal: true, message: String(err) });
501 }
502 self.close();
503 }
504 };
505