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