/* raycer service worker * - app shell: cache-first * - /api/*: network-first with no cache fallback (data lives in Dexie) * - dexie CDN module: cache-first (so the app boots offline) */ const VERSION = 'raycer-v1'; const SHELL_CACHE = `${VERSION}-shell`; const RUNTIME_CACHE = `${VERSION}-runtime`; const SHELL = [ '/', '/index.html', '/manifest.webmanifest', '/css/styles.css', '/js/app.js', '/js/api.js', '/js/db.js', '/js/theme.js', '/js/stats.js', '/js/calendar.js', '/icons/favicon.svg', '/icons/icon-192.png', '/icons/icon-512.png', ]; self.addEventListener('install', (event) => { event.waitUntil( (async () => { const cache = await caches.open(SHELL_CACHE); await cache.addAll(SHELL); self.skipWaiting(); })() ); }); self.addEventListener('activate', (event) => { event.waitUntil( (async () => { const keys = await caches.keys(); await Promise.all( keys .filter((k) => !k.startsWith(VERSION)) .map((k) => caches.delete(k)) ); await self.clients.claim(); })() ); }); self.addEventListener('fetch', (event) => { const req = event.request; if (req.method !== 'GET') return; const url = new URL(req.url); // API: network-first, never cache if (url.pathname.startsWith('/api/')) { event.respondWith( fetch(req).catch( () => new Response(JSON.stringify({ offline: true }), { status: 503, headers: { 'content-type': 'application/json' }, }) ) ); return; } // Same-origin SPA navigations: serve cached shell, fall back to network if (req.mode === 'navigate') { event.respondWith( (async () => { try { const fresh = await fetch(req); const cache = await caches.open(SHELL_CACHE); cache.put('/index.html', fresh.clone()).catch(() => {}); return fresh; } catch { const cached = await caches.match('/index.html'); if (cached) return cached; return new Response('offline', { status: 503 }); } })() ); return; } // Same-origin assets: cache-first if (url.origin === self.location.origin) { event.respondWith( (async () => { const cached = await caches.match(req); if (cached) return cached; try { const fresh = await fetch(req); if (fresh.ok) { const cache = await caches.open(RUNTIME_CACHE); cache.put(req, fresh.clone()).catch(() => {}); } return fresh; } catch { return new Response('offline', { status: 503 }); } })() ); return; } // Cross-origin (e.g. Dexie CDN): cache-first event.respondWith( (async () => { const cached = await caches.match(req); if (cached) return cached; try { const fresh = await fetch(req); if (fresh.ok) { const cache = await caches.open(RUNTIME_CACHE); cache.put(req, fresh.clone()).catch(() => {}); } return fresh; } catch { return cached || new Response('offline', { status: 503 }); } })() ); });