|
| 1 | +// Trip PWA service worker — offline shell + map tiles + self-update. |
| 2 | +// Ships UNENCRYPTED next to the HTML (must NOT be staticrypt-wrapped). |
| 3 | +// |
| 4 | +// Self-update: skipWaiting + clients.claim means a newly-published sw.js takes over immediately; |
| 5 | +// the page reloads once on controllerchange to pick up the fresh shell. Navigation is network-first, |
| 6 | +// so an online open always fetches the latest app; the cache is only a fallback for dead zones. |
| 7 | +// |
| 8 | +// Bump VERSION to force-drop every cached tile / Leaflet / font (old caches are deleted on activate). |
| 9 | +const VERSION='v1'; // fresh cache namespace for the /static/ URL |
| 10 | +const CACHE='trip-'+VERSION, MAX=3000, TRIM=200, NET_TIMEOUT=4000; |
| 11 | + |
| 12 | +self.addEventListener('install', ()=>self.skipWaiting()); |
| 13 | +self.addEventListener('activate', e=>e.waitUntil((async()=>{ |
| 14 | + const names=await caches.keys(); |
| 15 | + await Promise.all(names.filter(n=>n.startsWith('trip-') && n!==CACHE).map(n=>caches.delete(n))); |
| 16 | + await self.clients.claim(); |
| 17 | +})())); |
| 18 | + |
| 19 | +self.addEventListener('fetch', e=>{ |
| 20 | + const req=e.request; |
| 21 | + if(req.method!=='GET') return; |
| 22 | + |
| 23 | + // App shell / HTML: network-first so an updated site shows on the next open. Falls back to cache |
| 24 | + // (and keeps refreshing it in the background) when the signal is slow or gone — Big Sur / Lassen have none. |
| 25 | + if(req.mode==='navigate'){ |
| 26 | + e.respondWith((async()=>{ |
| 27 | + const cache=await caches.open(CACHE); |
| 28 | + const net=fetch(req).then(r=>{ cache.put(req, r.clone()); return r; }); |
| 29 | + const cached=await cache.match(req); |
| 30 | + if(!cached) return net.catch(()=>Response.error()); // first-ever load must reach the network |
| 31 | + // ponytail: 4s ceiling — prefer fresh, don't hang on flaky signal; net still updates the cache for next open. |
| 32 | + return Promise.race([ net.catch(()=>cached), new Promise(res=>setTimeout(()=>res(cached), NET_TIMEOUT)) ]); |
| 33 | + })()); |
| 34 | + return; |
| 35 | + } |
| 36 | + |
| 37 | + // Tiles, Leaflet, fonts: cache-first (immutable, URL-versioned). Cached on first fetch. |
| 38 | + // Only cache genuine 200s — never an opaque/error response, or a failed tile would poison the |
| 39 | + // cache-first path forever. Everything we need offline is CORS (tiles crossOrigin, Leaflet SRI) or |
| 40 | + // same-origin (fonts, HTML), so res.ok is always observable here. |
| 41 | + e.respondWith(caches.open(CACHE).then(async cache=>{ |
| 42 | + const hit=await cache.match(req); |
| 43 | + if(hit) return hit; |
| 44 | + const res=await fetch(req); |
| 45 | + if(res && res.ok){ |
| 46 | + cache.put(req, res.clone()); |
| 47 | + // ponytail: crude FIFO trim, LRU if it ever matters. ~3000 tiles ≈ 60 MB, within iOS quota. |
| 48 | + cache.keys().then(keys=>{ if(keys.length>MAX) for(let i=0;i<TRIM;i++) cache.delete(keys[i]); }); |
| 49 | + } |
| 50 | + return res; |
| 51 | + }).catch(()=>caches.match(req))); |
| 52 | +}); |
0 commit comments