offscreen-supervisor.mjs raw
1 const WASM_URL = new URL('signer.wasm', import.meta.url).href;
2 const MAX_RESTARTS = 5;
3
4 let worker = null, buildHash = null, ready = false, readyWaiters = [];
5 let restartCount = 0, stableTimer = null;
6 const pendingResolvers = new Map();
7 let nextId = 0;
8
9 // Firefox provides chrome.* as a callback-based compat alias. All storage calls
10 // use callbacks so they work in both browsers unchanged.
11 // sendMessage does not return a Promise in older Firefox - wrap with Promise.resolve.
12 function _sendStatus(msg) {
13 try { Promise.resolve(chrome.runtime.sendMessage(msg)).catch(() => {}); } catch(_) {}
14 }
15
16 function onReady() {
17 ready = true;
18 const w = readyWaiters.splice(0);
19 for (const fn of w) fn();
20 }
21
22 function awaitReady() {
23 if (ready) return Promise.resolve();
24 return new Promise(resolve => readyWaiters.push(resolve));
25 }
26
27 function startWorker() {
28 if (stableTimer) { clearTimeout(stableTimer); stableTimer = null; }
29 ready = false;
30 worker = new Worker(new URL('wasm-worker-host.mjs', import.meta.url), { type: 'module' });
31 worker.postMessage({ type: 'init', mode: 'root', wasmUrl: WASM_URL });
32 stableTimer = setTimeout(() => { restartCount = 0; stableTimer = null; }, 30000);
33
34 worker.onmessage = function(e) {
35 const d = e.data;
36 if (d.type === 'hello') { buildHash = d.buildHash; return; }
37 if (d.type === 'ready') { onReady(); return; }
38 if (d.type === 'exit') { if (worker) { worker.terminate(); worker = null; } return; }
39 if (d.type === 'storage-get') {
40 chrome.storage.local.get(d.key, r => {
41 worker && worker.postMessage({ type: 'storage-result', msgId: d.msgId, value: r[d.key] || '' });
42 });
43 return;
44 }
45 if (d.type === 'storage-set') { chrome.storage.local.set({ [d.key]: d.value }); return; }
46 if (d.type === 'storage-remove') { chrome.storage.local.remove(d.key); return; }
47 // signer response
48 const r = pendingResolvers.get(d.id);
49 if (r) { pendingResolvers.delete(d.id); r(d.result, d.error); }
50 };
51 worker.onerror = function(e) {
52 const detail = e.message + ' at ' + e.filename + ':' + e.lineno +
53 (e.error && e.error.stack ? '\n' + e.error.stack : '');
54 handleWorkerDeath(detail);
55 };
56 worker.onmessageerror = () => handleWorkerDeath('message deserialize error');
57 }
58
59 function handleWorkerDeath(detail) {
60 if (stableTimer) { clearTimeout(stableTimer); stableTimer = null; }
61 ready = false;
62 worker = null;
63 const drainedWaiters = readyWaiters.splice(0);
64 for (const fn of drainedWaiters) fn();
65 chrome.storage.local.get(['signerErrors'], r => {
66 const errs = (r.signerErrors || []).slice(-19);
67 errs.push({ ts: Date.now(), error: detail });
68 chrome.storage.local.set({ signerErrors: errs });
69 });
70 _sendStatus({ target: 'signer-status', status: 'degraded', error: detail });
71 for (const [, resolve] of pendingResolvers) resolve(null, 'signer worker died');
72 pendingResolvers.clear();
73 if (restartCount++ < MAX_RESTARTS) {
74 setTimeout(startWorker, 1000 * restartCount);
75 } else {
76 _sendStatus({ target: 'signer-status', status: 'dead' });
77 }
78 }
79
80 chrome.runtime.onMessage.addListener(function(msg, _sender, respond) {
81 // Accept messages routed via the Chrome SW (target='offscreen') OR sent
82 // directly from Firefox content scripts (no target field).
83 // Reject any message with a different target (signer-status broadcasts etc).
84 if (msg.target && msg.target !== 'offscreen') return false;
85 if (!worker) { respond({ error: 'signer unavailable' }); return true; }
86 const id = nextId++;
87 pendingResolvers.set(id, function(result, error) {
88 if (error) { respond({ error }); return; }
89 // Unwrap {"result":...} / {"error":...} JSON from the signer wasm,
90 // matching the behaviour of the old ext.mjs respond() function.
91 try {
92 const parsed = JSON.parse(result);
93 if (parsed.error) { respond({ error: parsed.error }); return; }
94 respond(parsed.result !== undefined ? parsed.result : parsed);
95 } catch (_) {
96 respond(result);
97 }
98 });
99 awaitReady().then(() => {
100 if (!worker) {
101 const r = pendingResolvers.get(id);
102 if (r) { pendingResolvers.delete(id); r(null, 'signer unavailable'); }
103 return;
104 }
105 worker.postMessage({ id, method: msg.method, params: msg.params, senderTabId: msg.senderTabId });
106 });
107 return true;
108 });
109
110 startWorker();
111