fix: bump SW version and switch to network-first caching

VERSION was stuck at raycer-v1 so mobile PWAs never picked up
the streak fix (cache-first served the stale stats.js forever).

- Bump to raycer-v2 → triggers install, purges old caches
- Switch same-origin assets from cache-first to network-first
  so future deploys propagate immediately; cache is only used
  as an offline fallback
- Simplify to a single SHELL_CACHE (no separate runtime cache)
- Cross-origin vendor assets (Dexie CDN) stay cache-first
This commit is contained in:
Spencer Flagg 2026-05-03 14:00:59 +02:00
parent 392bd6416e
commit e410f09c93

View file

@ -1,14 +1,13 @@
/* raycer service worker /* raycer service worker
* - app shell: cache-first * - app shell: network-first (so deploys propagate immediately), cache fallback for offline
* - /api/*: network-first with no cache fallback (data lives in Dexie) * - /api/*: network-only (data lives in Dexie)
* - dexie CDN module: cache-first (so the app boots offline) * - cross-origin (Dexie CDN): cache-first (immutable vendor assets)
*/ */
const VERSION = 'raycer-v1'; const VERSION = 'raycer-v2';
const SHELL_CACHE = `${VERSION}-shell`; const SHELL_CACHE = `${VERSION}-shell`;
const RUNTIME_CACHE = `${VERSION}-runtime`;
const SHELL = [ const PRECACHE = [
'/', '/',
'/index.html', '/index.html',
'/manifest.webmanifest', '/manifest.webmanifest',
@ -28,7 +27,7 @@ self.addEventListener('install', (event) => {
event.waitUntil( event.waitUntil(
(async () => { (async () => {
const cache = await caches.open(SHELL_CACHE); const cache = await caches.open(SHELL_CACHE);
await cache.addAll(SHELL); await cache.addAll(PRECACHE);
self.skipWaiting(); self.skipWaiting();
})() })()
); );
@ -40,7 +39,7 @@ self.addEventListener('activate', (event) => {
const keys = await caches.keys(); const keys = await caches.keys();
await Promise.all( await Promise.all(
keys keys
.filter((k) => !k.startsWith(VERSION)) .filter((k) => k !== SHELL_CACHE)
.map((k) => caches.delete(k)) .map((k) => caches.delete(k))
); );
await self.clients.claim(); await self.clients.claim();
@ -54,7 +53,7 @@ self.addEventListener('fetch', (event) => {
const url = new URL(req.url); const url = new URL(req.url);
// API: network-first, never cache // API: network-only, never cache
if (url.pathname.startsWith('/api/')) { if (url.pathname.startsWith('/api/')) {
event.respondWith( event.respondWith(
fetch(req).catch( fetch(req).catch(
@ -67,39 +66,24 @@ self.addEventListener('fetch', (event) => {
return; return;
} }
// Same-origin SPA navigations: serve cached shell, fall back to network // Same-origin (app shell + assets): network-first, cache fallback for offline
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) { if (url.origin === self.location.origin) {
event.respondWith( event.respondWith(
(async () => { (async () => {
const cached = await caches.match(req);
if (cached) return cached;
try { try {
const fresh = await fetch(req); const fresh = await fetch(req);
if (fresh.ok) { if (fresh.ok) {
const cache = await caches.open(RUNTIME_CACHE); const cache = await caches.open(SHELL_CACHE);
cache.put(req, fresh.clone()).catch(() => {}); cache.put(req, fresh.clone()).catch(() => {});
} }
return fresh; return fresh;
} catch { } catch {
const cached = await caches.match(req);
if (cached) return cached;
if (req.mode === 'navigate') {
const shell = await caches.match('/index.html');
if (shell) return shell;
}
return new Response('offline', { status: 503 }); return new Response('offline', { status: 503 });
} }
})() })()
@ -107,7 +91,7 @@ self.addEventListener('fetch', (event) => {
return; return;
} }
// Cross-origin (e.g. Dexie CDN): cache-first // Cross-origin (e.g. Dexie CDN): cache-first (immutable vendor assets)
event.respondWith( event.respondWith(
(async () => { (async () => {
const cached = await caches.match(req); const cached = await caches.match(req);
@ -115,12 +99,12 @@ self.addEventListener('fetch', (event) => {
try { try {
const fresh = await fetch(req); const fresh = await fetch(req);
if (fresh.ok) { if (fresh.ok) {
const cache = await caches.open(RUNTIME_CACHE); const cache = await caches.open(SHELL_CACHE);
cache.put(req, fresh.clone()).catch(() => {}); cache.put(req, fresh.clone()).catch(() => {});
} }
return fresh; return fresh;
} catch { } catch {
return cached || new Response('offline', { status: 503 }); return new Response('offline', { status: 503 });
} }
})() })()
); );