// Service worker: precache the app shell, serve network-first with a // cache fallback so updates show up immediately when online and the app // still works fully offline. Bump VERSION when the file list changes. const VERSION = 'picker-v2'; const SHELL = [ './', './index.html', './manifest.webmanifest', './css/app.css', './js/main.js', './js/game.js', './js/modes.js', './js/random.js', './js/renderer.js', './js/reveals.js', './js/input.js', './js/audio.js', './js/device.js', './js/settings.js', './js/i18n.js', './js/themes.js', './js/ui.js', './icons/icon.svg', './icons/icon-192.png', './icons/icon-512.png', './icons/icon-maskable-512.png', './icons/apple-touch-icon.png', ]; const NETWORK_TIMEOUT_MS = 2500; self.addEventListener('install', (event) => { event.waitUntil( caches .open(VERSION) .then((cache) => cache.addAll(SHELL.map((u) => new Request(u, { cache: 'reload' })))) .then(() => self.skipWaiting()), ); }); self.addEventListener('activate', (event) => { event.waitUntil( caches .keys() .then((keys) => Promise.all(keys.filter((k) => k !== VERSION).map((k) => caches.delete(k)))) .then(() => self.clients.claim()), ); }); self.addEventListener('fetch', (event) => { const req = event.request; if (req.method !== 'GET' || new URL(req.url).origin !== self.location.origin) return; event.respondWith(networkFirst(req, event)); }); async function networkFirst(req, event) { const cache = await caches.open(VERSION); try { const res = await withTimeout(fetch(req), NETWORK_TIMEOUT_MS); if (res.ok) event.waitUntil(cache.put(req, res.clone())); return res; } catch { // ignoreSearch so ?debug=1 etc. still resolve offline const hit = (await cache.match(req, { ignoreSearch: true })) || (req.mode === 'navigate' && (await cache.match('./index.html'))); return hit || Response.error(); } } function withTimeout(promise, ms) { return new Promise((resolve, reject) => { const t = setTimeout(() => reject(new Error('timeout')), ms); promise.then( (v) => { clearTimeout(t); resolve(v); }, (e) => { clearTimeout(t); reject(e); }, ); }); }