sw.mjs raw

   1  // TinyJS Runtime — Service Worker Bridge
   2  // Provides Go-callable Service Worker, Cache, Fetch, and SSE operations.
   3  
   4  // --- Internal state ---
   5  
   6  const _events = new Map();
   7  let _nextEventId = 1;
   8  const _caches = new Map();
   9  let _nextCacheId = 1;
  10  const _responses = new Map();
  11  let _nextRespId = 1;
  12  const _clients = new Map();
  13  let _nextClientId = 1;
  14  const _sseConns = new Map();
  15  let _nextSseId = 1;
  16  
  17  function _storeEvent(ev) {
  18    const id = _nextEventId++;
  19    _events.set(id, ev);
  20    return id;
  21  }
  22  
  23  function _storeResponse(resp) {
  24    if (!resp) return 0;
  25    const id = _nextRespId++;
  26    _responses.set(id, resp);
  27    return id;
  28  }
  29  
  30  function _storeClient(client) {
  31    const id = _nextClientId++;
  32    _clients.set(id, client);
  33    return id;
  34  }
  35  
  36  // --- Lifecycle ---
  37  
  38  export function OnInstall(fn) {
  39    self.addEventListener('install', (event) => {
  40      fn(_storeEvent(event));
  41    });
  42  }
  43  
  44  export function OnActivate(fn) {
  45    self.addEventListener('activate', (event) => {
  46      fn(_storeEvent(event));
  47    });
  48  }
  49  
  50  // Fetch events use a deferred promise pattern:
  51  // Go calls RespondWithCache or RespondWithNetwork synchronously to pick strategy.
  52  // The runtime creates the promise and calls event.respondWith() synchronously.
  53  const _fetchResolvers = new Map();
  54  
  55  export function OnFetch(fn) {
  56    self.addEventListener('fetch', (event) => {
  57      const id = _storeEvent(event);
  58      fn(id);
  59      // Check if Go code set up a response strategy.
  60      const resolver = _fetchResolvers.get(id);
  61      if (resolver) {
  62        event.respondWith(resolver);
  63        _fetchResolvers.delete(id);
  64      }
  65      // If no resolver was set, browser handles the fetch normally.
  66    });
  67  }
  68  
  69  export function OnMessage(fn) {
  70    self.addEventListener('message', (event) => {
  71      fn(_storeEvent(event));
  72    });
  73  }
  74  
  75  // --- Event methods ---
  76  
  77  export function WaitUntil(eventId, fn) {
  78    const ev = _events.get(eventId);
  79    if (!ev) return;
  80    ev.waitUntil(new Promise((resolve) => {
  81      fn(resolve);
  82    }));
  83  }
  84  
  85  export function RespondWith(eventId, respId) {
  86    const resp = _responses.get(respId);
  87    if (resp) {
  88      _fetchResolvers.set(eventId, Promise.resolve(resp));
  89    }
  90  }
  91  
  92  export function RespondWithNetwork(eventId) {
  93    // Don't set a resolver — browser handles the fetch.
  94  }
  95  
  96  // RespondWithCacheFirst tries cache, falls back to network.
  97  // This is the common pattern and must be called synchronously from onFetch.
  98  export function RespondWithCacheFirst(eventId) {
  99    const ev = _events.get(eventId);
 100    if (!ev) return;
 101    _fetchResolvers.set(eventId,
 102      caches.match(ev.request).then(cached => cached || fetch(ev.request))
 103    );
 104  }
 105  
 106  export function GetRequestURL(eventId) {
 107    const ev = _events.get(eventId);
 108    return ev ? ev.request.url : '';
 109  }
 110  
 111  export function GetRequestPath(eventId) {
 112    const ev = _events.get(eventId);
 113    if (!ev) return '';
 114    return new URL(ev.request.url).pathname;
 115  }
 116  
 117  export function GetMessageData(eventId) {
 118    const ev = _events.get(eventId);
 119    if (!ev) return '';
 120    const d = ev.data;
 121    return typeof d === 'string' ? d : JSON.stringify(d);
 122  }
 123  
 124  // --- SW globals ---
 125  
 126  export function SkipWaiting() {
 127    self.skipWaiting();
 128  }
 129  
 130  export function ClaimClients(done) {
 131    self.clients.claim().then(() => { if (done) done(); });
 132  }
 133  
 134  export function MatchClients(fn) {
 135    self.clients.matchAll({ type: 'window' }).then((all) => {
 136      for (const c of all) {
 137        fn(_storeClient(c));
 138      }
 139    });
 140  }
 141  
 142  export function PostMessage(clientId, msg) {
 143    const c = _clients.get(clientId);
 144    if (c) c.postMessage(msg);
 145  }
 146  
 147  export function PostMessageJSON(clientId, json) {
 148    const c = _clients.get(clientId);
 149    if (c) c.postMessage(json);
 150  }
 151  
 152  export function Navigate(clientId, url) {
 153    const c = _clients.get(clientId);
 154    if (c) c.navigate(url || c.url);
 155  }
 156  
 157  // --- Cache ---
 158  
 159  export function CacheOpen(name, fn) {
 160    caches.open(name).then((cache) => {
 161      const id = _nextCacheId++;
 162      _caches.set(id, cache);
 163      fn(id);
 164    });
 165  }
 166  
 167  export function CacheAddAll(cacheId, urls, done) {
 168    const cache = _caches.get(cacheId);
 169    if (!cache) { if (done) done(); return; }
 170    cache.addAll(urls).then(() => { if (done) done(); });
 171  }
 172  
 173  export function CachePut(cacheId, url, respId, done) {
 174    const cache = _caches.get(cacheId);
 175    const resp = _responses.get(respId);
 176    if (!cache || !resp) { if (done) done(); return; }
 177    cache.put(new Request(url), resp).then(() => { if (done) done(); });
 178  }
 179  
 180  export function CacheMatch(url, fn) {
 181    caches.match(new Request(url)).then((resp) => {
 182      fn(_storeResponse(resp));
 183    });
 184  }
 185  
 186  export function CacheDelete(name, done) {
 187    caches.delete(name).then(() => { if (done) done(); });
 188  }
 189  
 190  // --- Fetch ---
 191  
 192  export function Fetch(url, fn) {
 193    fetch(url).then(
 194      (resp) => fn(_storeResponse(resp), true),
 195      () => fn(0, false)
 196    );
 197  }
 198  
 199  export function ResponseOK(respId) {
 200    const resp = _responses.get(respId);
 201    return resp ? resp.ok : false;
 202  }
 203  
 204  // --- SSE ---
 205  
 206  export function SSEConnect(url, onMessage) {
 207    const id = _nextSseId++;
 208    const es = new EventSource(url);
 209    _sseConns.set(id, es);
 210    es.onmessage = (event) => {
 211      if (onMessage) onMessage(event.data);
 212    };
 213    return id;
 214  }
 215  
 216  export function SSEClose(sseId) {
 217    const es = _sseConns.get(sseId);
 218    if (es) {
 219      es.close();
 220      _sseConns.delete(sseId);
 221    }
 222  }
 223  
 224  // --- Logging ---
 225  
 226  export function Log(msg) {
 227    console.log('sw:', msg);
 228  }
 229