bc.mjs raw

   1  // TinyJS Runtime — Bus Bridge (Relay SW)
   2  // Routes bus messages via MessagePort to/from the page.
   3  // Listener registered at top level to catch port before Open() runs.
   4  
   5  // Global error handler — catches errors and forwards to page via bus port.
   6  self.addEventListener('error', (e) => {
   7    console.error('relay-sw uncaught:', e.message, e.filename, e.lineno);
   8    if (self._busPort) {
   9      self._busPort.postMessage('{"from":"relay","to":"shell","msg":["LOG","relay","UNCAUGHT: ' + String(e.message).replace(/"/g, '\\"').replace(/\n/g, ' ') + ' at ' + String(e.filename).replace(/"/g, '') + ':' + e.lineno + '"]}');
  10    }
  11  });
  12  self.addEventListener('unhandledrejection', (e) => {
  13    var msg = e.reason ? (e.reason.message || String(e.reason)) : 'unknown';
  14    console.error('relay-sw unhandled rejection:', msg);
  15    if (self._busPort) {
  16      self._busPort.postMessage('{"from":"relay","to":"shell","msg":["LOG","relay","REJECTION: ' + String(msg).replace(/"/g, '\\"').replace(/\n/g, ' ') + '"]}');
  17    }
  18  });
  19  
  20  let _onMessage = null;
  21  let _queue = [];
  22  
  23  // waitUntil with a 25s promise — tells browser "I'm busy, don't terminate."
  24  // With 10s keepalive interval from page, there are always 2-3 overlapping
  25  // promises. Browser MUST keep SW alive while any waitUntil promise is pending.
  26  function _holdOpen(ev) {
  27    if (ev.waitUntil) ev.waitUntil(new Promise(r => setTimeout(r, 25000)));
  28  }
  29  
  30  // Capture port immediately — before goroutine scheduler calls Open().
  31  self.addEventListener('message', (ev) => {
  32    if (ev.data && ev.data.type === 'bus-port' && ev.ports && ev.ports[0]) {
  33      self._busPort = ev.ports[0];
  34      self._busPort.onmessage = _portHandler;
  35      // Flush queued messages.
  36      for (const m of _queue) self._busPort.postMessage(m);
  37      _queue = [];
  38      _holdOpen(ev);
  39      return;
  40    }
  41    // Keepalive from page — extend SW lifetime, nothing else needed.
  42    if (ev.data === 'keepalive') {
  43      _holdOpen(ev);
  44      return;
  45    }
  46    // Direct string message (fallback).
  47    const d = ev.data;
  48    if (typeof d === 'string' && d.length > 0 && d[0] === '{' && _onMessage) {
  49      _holdOpen(ev);
  50      try {
  51        _onMessage(d);
  52      } catch (e) {
  53        console.error('relay-sw: direct msg handler CRASH:', e.message, e.stack);
  54      }
  55    }
  56  });
  57  
  58  function _portHandler(pev) {
  59    const d = pev.data;
  60    // Health check: page sends PING, we respond PONG to prove we're alive.
  61    if (d === 'PING') {
  62      if (self._busPort) self._busPort.postMessage('PONG');
  63      return;
  64    }
  65    if (typeof d === 'string' && d.length > 0 && d[0] === '{' && _onMessage) {
  66      try {
  67        _onMessage(d);
  68      } catch (e) {
  69        console.error('relay-sw: bus handler CRASH:', e.message, e.stack);
  70        // Forward crash to shell SW via port so it reaches the page console.
  71        if (self._busPort) {
  72          self._busPort.postMessage('{"from":"relay","to":"shell","msg":["LOG","relay","BUS CRASH: ' + String(e.message).replace(/"/g, '\\"') + '"]}');
  73        }
  74      }
  75    }
  76  }
  77  
  78  export function Open(name, onMessage) {
  79    _onMessage = onMessage;
  80    // If port arrived before Open(), wire up handler now.
  81    if (self._busPort) {
  82      self._busPort.onmessage = _portHandler;
  83    }
  84    return 1;
  85  }
  86  
  87  export function Send(bcId, msg) {
  88    if (self._busPort) {
  89      try { self._busPort.postMessage(msg); }
  90      catch (e) { console.error('relay bus send failed:', e.message); }
  91    } else {
  92      _queue.push(msg);
  93    }
  94  }
  95  
  96  export function Close(bcId) {}
  97