index.html raw

   1  <!DOCTYPE html>
   2  <html lang="en">
   3  <head>
   4  <meta charset="utf-8">
   5  <meta name="viewport" content="width=device-width, initial-scale=1">
   6  <base href="/">
   7  <title>smesh</title>
   8  <link rel="icon" type="image/svg+xml" href="./favicon.svg">
   9  <style>
  10  @font-face {
  11    font-family: 'Fira Code';
  12    src: url('./fonts/FiraCode-Regular.woff2') format('woff2');
  13    font-weight: 400;
  14    font-style: normal;
  15    font-display: swap;
  16  }
  17  @font-face {
  18    font-family: 'Fira Code';
  19    src: url('./fonts/FiraCode-Bold.woff2') format('woff2');
  20    font-weight: 700;
  21    font-style: normal;
  22    font-display: swap;
  23  }
  24  html { margin: 0; padding: 0; }
  25  body {
  26    --bg: #fff;
  27    --fg: #111;
  28    --accent: #f59e0b;
  29    --muted: #888;
  30    --border: #ddd;
  31    --bg2: #f5f5f5;
  32    --c1: #f59e0b;
  33    --c2: #cc33ff;
  34    --c3: #00aabb;
  35    margin: 0;
  36    padding: 0;
  37    min-height: 100vh;
  38    background: var(--bg);
  39    color: var(--fg);
  40    font-family: 'Fira Code', emoji, system-ui, monospace;
  41  }
  42  body.dark {
  43    --bg: #000;
  44    --fg: #e0e0e0;
  45    --accent: #f59e0b;
  46    --muted: #666;
  47    --border: #333;
  48    --bg2: #000;
  49    --c1: #f59e0b;
  50    --c2: #cc33ff;
  51    --c3: #00aabb;
  52  }
  53  [stroke="#e07030"] { stroke: var(--c1); }
  54  [stroke="#8833bb"] { stroke: var(--c2); }
  55  [stroke="#00aabb"] { stroke: var(--c3); }
  56  #update-snackbar {
  57    position: fixed;
  58    bottom: 37px;
  59    left: 0;
  60    right: 0;
  61    background: var(--accent);
  62    color: #000;
  63    text-align: center;
  64    padding: 12px;
  65    font-family: 'Fira Code', emoji, system-ui, monospace;
  66    font-size: 14px;
  67    cursor: pointer;
  68    transform: translateY(calc(100% + 37px));
  69    transition: transform 0.3s ease;
  70    z-index: 9999;
  71  }
  72  #update-snackbar.visible {
  73    transform: translateY(0);
  74  }
  75  #update-snackbar:hover {
  76    filter: brightness(1.1);
  77  }
  78  </style>
  79  </head>
  80  <body>
  81  <script>{let t=localStorage.getItem('smesh-theme');if(t==='dark'||(t===null&&matchMedia('(prefers-color-scheme:dark)').matches))document.body.classList.add('dark')}</script>
  82  <div id="app-root"></div>
  83  <div id="update-snackbar">new version available — click to update</div>
  84  <script>
  85  if ('serviceWorker' in navigator) {
  86    // Queue for relay→shell messages that arrive before controller is set.
  87    // After hard refresh or deploy, controller is null until claimClients() completes.
  88    // Without this queue, READY from the relay SW is silently dropped and the shell
  89    // SW never learns the relay is alive → all messages queue forever.
  90    var _pendingBus = [];
  91  
  92    function busToShell(d) {
  93      if (navigator.serviceWorker.controller) {
  94        navigator.serviceWorker.controller.postMessage(d);
  95      } else {
  96        _pendingBus.push(d);
  97      }
  98    }
  99  
 100    navigator.serviceWorker.addEventListener('controllerchange', function() {
 101      if (_pendingBus.length && navigator.serviceWorker.controller) {
 102        for (var i = 0; i < _pendingBus.length; i++)
 103          navigator.serviceWorker.controller.postMessage(_pendingBus[i]);
 104        _pendingBus = [];
 105      }
 106    });
 107  
 108    navigator.serviceWorker.register('./$sw/$entry.mjs', { type: 'module', scope: '/', updateViaCache: 'none' }).then(reg => {
 109      if (!navigator.serviceWorker.controller && reg.active) {
 110        reg.active.postMessage('CLAIM');
 111      }
 112      // Trigger update check on every page load — catches stale SW modules.
 113      reg.update().catch(function(){});
 114    }).catch(err => {
 115      console.error('smesh: sw registration failed:', err);
 116    });
 117  
 118  
 119    navigator.serviceWorker.addEventListener('message', (event) => {
 120      const d = event.data;
 121      // Bus relay: shell SW → page → satellite SWs via _busPorts.
 122      if (typeof d === 'string' && d.length > 0 && d[0] === '{') {
 123        var ports = window._busPorts;
 124        if (ports) for (var name in ports) ports[name].postMessage(d);
 125        return;
 126      }
 127      if (typeof d === 'string' && d.startsWith('sw-error:')) {
 128        console.error('SW RUNTIME ERROR:', d.slice(9));
 129        return;
 130      }
 131      if (typeof d === 'string' && d === 'sw-started') return;
 132      if (d === 'update-available') {
 133        document.getElementById('update-snackbar').classList.add('visible');
 134      }
 135      if (d === 'update-activated') {
 136        window.location.reload();
 137      }
 138      // Version epoch: shell SW detected satellite version mismatch — force re-register.
 139      if (Array.isArray(d) && d[0] === 'FORCE_UPDATE_SW') {
 140        var dir = d[1];
 141        // Loop protection: max 3 retries per dir per page load.
 142        if (!window._fuswRetries) window._fuswRetries = {};
 143        var count = window._fuswRetries[dir] || 0;
 144        if (count >= 3) {
 145          console.error('FORCE_UPDATE_SW: giving up on ' + dir + ' after 3 retries — rebuild required');
 146          return;
 147        }
 148        window._fuswRetries[dir] = count + 1;
 149        console.warn('FORCE_UPDATE_SW: re-registering ' + dir + ' (attempt ' + (count+1) + '/3)');
 150        var isLocalDev = location.hostname === 'localhost' || /^127\.0\.0\.\d+$/.test(location.hostname);
 151        if (isLocalDev) {
 152          // Dev mode: reload the iframe to pick up new SW.
 153          var iframe = document.getElementById('sw-iframe-' + dir);
 154          if (iframe) iframe.contentWindow.location.reload();
 155        } else {
 156          navigator.serviceWorker.getRegistrations().then(function(regs) {
 157            for (var i = 0; i < regs.length; i++) {
 158              if (regs[i].scope.indexOf('/' + dir + '/') !== -1) {
 159                regs[i].unregister().then(function() { regSW(dir); }).catch(function() { regSW(dir); });
 160              }
 161            }
 162          });
 163        }
 164      }
 165    });
 166  
 167    document.getElementById('update-snackbar').addEventListener('click', () => {
 168      if (navigator.serviceWorker.controller) {
 169        navigator.serviceWorker.controller.postMessage('activate-update');
 170      }
 171    });
 172  }
 173  </script>
 174  <script>
 175  // Satellite SWs — two modes:
 176  // Production (HTTPS): same-origin path-based registration via MessagePort.
 177  // Dev (localhost): cross-origin iframe at 127.0.0.3 using loader.html.
 178  {
 179    window._busPorts = {};
 180    var isLocalDev = location.hostname === 'localhost' || /^127\.0\.0\.\d+$/.test(location.hostname);
 181  
 182    // Same-origin path-based registration (production).
 183    async function regSW(dir) {
 184      try {
 185        var reg = await navigator.serviceWorker.register('/'+dir+'/$entry.mjs',{type:'module',scope:'/'+dir+'/',updateViaCache:'none'});
 186        reg.update().catch(function(){});
 187        var _pongOk = false;
 188        function sendPort(w) {
 189          var ch = new MessageChannel();
 190          w.postMessage({type:'bus-port'},[ch.port2]);
 191          window._busPorts[dir] = ch.port1;
 192          ch.port1.onmessage = function(ev) {
 193            var d = ev.data;
 194            if(d === 'PONG') { _pongOk = true; return; }
 195            if(typeof d==='string'&&d.length>0&&d[0]==='{') busToShell(d);
 196          };
 197        }
 198        var sw = reg.active;
 199        if(sw) sendPort(sw);
 200        reg.addEventListener('updatefound',function(){
 201          var nw = reg.installing;
 202          if(nw) nw.addEventListener('statechange',function(){
 203            if(nw.state==='activated') sendPort(nw);
 204          });
 205        });
 206        if(!sw){
 207          sw = reg.installing||reg.waiting;
 208          if(sw) sw.addEventListener('statechange',function(){if(sw.state==='activated')sendPort(sw);});
 209        }
 210        // --- Layer 1: Keepalive (10s) ---
 211        // postMessage creates ExtendableMessageEvent → resets termination timer.
 212        // SW calls waitUntil(25s promise) on each → overlapping holds.
 213        // Detects SW restart (new Worker instance) → re-establishes MessagePort.
 214        var _lastPortTarget = sw || null;
 215        setInterval(function(){
 216          var w = reg.active;
 217          if(!w) return;
 218          if(w !== _lastPortTarget) {
 219            _lastPortTarget = w;
 220            sendPort(w);
 221          } else {
 222            w.postMessage('keepalive');
 223          }
 224        }, 10000);
 225        // --- Layer 2: Health check (30s) ---
 226        // PING via MessagePort, expect PONG within 3s.
 227        // On failure: re-send bus-port to recover from terminated/restarted SW.
 228        var _failCount = 0;
 229        setInterval(function(){
 230          var port = window._busPorts[dir];
 231          if(!port) return;
 232          _pongOk = false;
 233          try { port.postMessage('PING'); } catch(e) { _failCount++; return; }
 234          setTimeout(function(){
 235            if(_pongOk) { _failCount = 0; return; }
 236            _failCount++;
 237            console.warn(dir+' health check failed ('+_failCount+')');
 238            var w = reg.active;
 239            if(w) { _lastPortTarget = w; sendPort(w); }
 240          }, 3000);
 241        }, 30000);
 242      } catch(e) { console.error(dir+' SW failed:',e); }
 243    }
 244  
 245    // Cross-origin iframe registration (dev mode).
 246    // Relay SW runs on 127.0.0.3 for thread isolation.
 247    function regSWIframe(dir, ip) {
 248      var origin = 'http://'+ip+':'+location.port;
 249      var iframe = document.createElement('iframe');
 250      iframe.src = origin+'/'+dir+'/loader.html';
 251      iframe.style.display = 'none';
 252      iframe.id = 'sw-iframe-'+dir;
 253      document.body.appendChild(iframe);
 254      // Relay bus messages between iframe (satellite SW) and shell SW.
 255      window.addEventListener('message', function(ev) {
 256        if(ev.origin !== origin) return;
 257        var d = ev.data;
 258        if(typeof d==='string'&&d.length>0&&d[0]==='{') busToShell(d);
 259      });
 260      // Forward shell→satellite messages via iframe.
 261      window._busPorts[dir] = {postMessage: function(msg) { iframe.contentWindow.postMessage(msg, origin); }};
 262    }
 263  
 264    if (isLocalDev) {
 265      regSWIframe('$sw-relay', '127.0.0.3');
 266    } else {
 267      regSW('$sw-relay');
 268    }
 269  }
 270  </script>
 271  <script type="module" src="./mls-bridge.mjs"></script>
 272  <script type="module" src="./$entry.mjs"></script>
 273  </body>
 274  </html>
 275