1// Webcade service worker â the minimum that makes the app installable (a PWA, 2// and therefore wrappable as the Saga/Seeker dApp-Store TWA) without ever 3// letting a stale response reach a money-relevant screen. 4// 5// The disciplines that matter here: 6// /api/* is NEVER cached â the board, prizes, duels and wallet state must 7// always be live. Versioned assets (?v=<deploy>) are immutable, so they are 8// cache-first. Everything else is network-first, falling back to cache only 9// when the device is actually offline. That keeps the SW an offline nicety, 10// not a second source of truth that could serve yesterday's board. 11const CACHE = 'webcade-v1'; 12const OFFLINE = '/offline.html'; 13 14self.addEventListener('install', (e) => { 15 e.waitUntil( 16 caches.open(CACHE) 17 .then((c) => c.addAll([OFFLINE, '/icon-192.png'])) 18 .then(() => self.skipWaiting()) 19 ); 20}); 21 22self.addEventListener('activate', (e) => { 23 e.waitUntil( 24 caches.keys() 25 .then((keys) => Promise.all(keys.filter((k) => k !== CACHE).map((k) => caches.delete(k)))) 26 .then(() => self.clients.claim()) 27 ); 28}); 29 30self.addEventListener('fetch', (e) => { 31 const req = e.request; 32 if (req.method !== 'GET') return; 33 const url = new URL(req.url); 34 if (url.origin !== location.origin) return; // CDN scripts pass straight through 35 if (url.pathname.startsWith('/api/')) return; // never cache the live endpoints 36 37 // Immutable, deploy-stamped assets: serve from cache, fill it on first miss. 38 if (url.searchParams.has('v')) { 39 e.respondWith(caches.open(CACHE).then(async (c) => { 40 const hit = await c.match(req); 41 if (hit) return hit; 42 const res = await fetch(req); 43 if (res && res.ok) c.put(req, res.clone()); 44 return res; 45 })); 46 return; 47 } 48 49 // Page loads: live first, offline shell as the fallback. 50 if (req.mode === 'navigate') { 51 e.respondWith(fetch(req).catch(() => caches.match(OFFLINE))); 52 return; 53 } 54 55 // Anything else: live, with whatever we cached as a last resort offline. 56 e.respondWith(fetch(req).catch(() => caches.match(req))); 57}); 58 59// ------------------------------------------------------------- web push 60// 61// A nudge arrives here even when the app is closed. In the dApp Store TWA 62// (built with enableNotifications) these render as the APP's own notifications 63// â our icon, our name â rather than a browser's. 64// 65// Everything is defensive: a malformed payload must still produce a 66// notification rather than a swallowed error, because a silent push is 67// indistinguishable from a broken one. 68self.addEventListener('push', (e) => { 69 let d = {}; 70 try { d = e.data ? e.data.json() : {}; } catch (_) { /* not JSON: fall through */ } 71 const title = d.title || 'WEBCADE'; 72 e.waitUntil(self.registration.showNotification(title, { 73 body: d.body || 'Your arcade is calling.', 74 icon: '/icon-192.png', 75 badge: '/icon-192.png', 76 // A tag collapses repeats: two board nudges replace each other rather 77 // than stacking into a wall the player has to swipe away. 78 tag: d.tag || 'webcade', 79 renotify: true, 80 data: { url: d.url || '/arcade' }, 81 })); 82}); 83 84// Tapping a nudge should land on the thing it was about â and reuse an open 85// window rather than piling up tabs. 86self.addEventListener('notificationclick', (e) => { 87 e.notification.close(); 88 const target = (e.notification.data && e.notification.data.url) || '/arcade'; 89 e.waitUntil((async () => { 90 const all = await clients.matchAll({ type: 'window', includeUncontrolled: true }); 91 for (const c of all) { 92 if (c.url.includes(location.origin)) { 93 await c.focus(); 94 if ('navigate' in c) { try { await c.navigate(target); } catch (_) {} } 95 return; 96 } 97 } 98 await clients.openWindow(target); 99 })()); 100});
Line numbers count LF bytes from the start of the resource, as the search results do. Vendor segments are library code the classifier recognised; they are stored but not indexed. Bytes are shown as Latin1 characters, one per byte.