// PackIt service worker. // // A packing list is read where there is no signal, so anything already visited // has to keep working offline. Navigations are network-first (the list must // reflect the latest admin changes when we can reach the server) with the // cached copy as the fallback; static assets are cache-first. // // Bump CACHE_VERSION to retire every previously cached response. const CACHE_VERSION = 'packit-v1'; const OFFLINE_URL = '/offline.html'; // Small and always needed. index.php is added opportunistically because it // depends on the database and must not be allowed to fail the install. const PRECACHE = [ OFFLINE_URL, '/manifest.webmanifest', '/img/icon-192.png', '/img/icon-512.png', ]; self.addEventListener('install', (event) => { event.waitUntil((async () => { const cache = await caches.open(CACHE_VERSION); await cache.addAll(PRECACHE); await cache.add('/index.php').catch(() => {}); self.skipWaiting(); })()); }); self.addEventListener('activate', (event) => { event.waitUntil((async () => { const keys = await caches.keys(); await Promise.all(keys.filter((k) => k !== CACHE_VERSION).map((k) => caches.delete(k))); await self.clients.claim(); })()); }); async function networkFirst(request) { const cache = await caches.open(CACHE_VERSION); try { const response = await fetch(request); // A redirected response (list.php -> packing.php) cannot be replayed from // the cache for a navigation, so only store final ones. if (response.ok && !response.redirected) { cache.put(request, response.clone()); } return response; } catch (err) { const cached = await cache.match(request); return cached || (await cache.match(OFFLINE_URL)) || Response.error(); } } async function cacheFirst(request) { const cache = await caches.open(CACHE_VERSION); const cached = await cache.match(request); if (cached) return cached; const response = await fetch(request); if (response.ok || response.type === 'opaque') { cache.put(request, response.clone()); } return response; } async function staleWhileRevalidate(request) { const cache = await caches.open(CACHE_VERSION); const cached = await cache.match(request); const network = fetch(request).then((response) => { if (response.ok || response.type === 'opaque') cache.put(request, response.clone()); return response; }).catch(() => cached); return cached || network; } self.addEventListener('fetch', (event) => { const request = event.request; if (request.method !== 'GET') return; const url = new URL(request.url); // Fonts live on Google's CDN; keep them usable offline. if (url.origin === 'https://fonts.googleapis.com' || url.origin === 'https://fonts.gstatic.com') { event.respondWith(staleWhileRevalidate(request)); return; } if (url.origin !== self.location.origin) return; // The admin panel is session-protected and always changing – never cache it, // and never serve a stale page to someone editing content. if (url.pathname.startsWith('/admin/')) return; if (request.mode === 'navigate') { event.respondWith(networkFirst(request)); return; } event.respondWith(cacheFirst(request)); });