/* Service worker: app-shell cache-first, API network-first met timeout en fallback op de laatste response. */ const VERSION = 'weerscherm-v12'; const SHELL = [ './', './index.html', './styles.css', './app.js', './format.js', './icons.js', './manifest.webmanifest', './icons/icon.svg', './icons/icon-180.png', './icons/icon-192.png', './icons/icon-512.png', ]; const API_PREFIX = '/api/'; const DATA_PATH = '/api/data.php'; const API_TIMEOUT_MS = 8000; self.addEventListener('install', (event) => { event.waitUntil( caches.open(VERSION).then((cache) => cache.addAll(SHELL)).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') return; const url = new URL(req.url); if (url.origin !== self.location.origin) return; if (url.pathname.includes(API_PREFIX)) { event.respondWith(networkFirst(req)); } else { event.respondWith(cacheFirst(req)); } }); async function networkFirst(req) { const cache = await caches.open(VERSION); const url = new URL(req.url); // Sleutel zonder querystring: het radarbeeld krijgt elke paar minuten een andere ?t=, en zonder // normaliseren zou de cache vollopen met varianten en zou offline nooit een treffer geven. const key = url.origin + url.pathname; const controller = new AbortController(); const timer = setTimeout(() => controller.abort(), API_TIMEOUT_MS); try { const res = await fetch(req, { signal: controller.signal, cache: 'no-store' }); if (res.ok) { cache.put(key, res.clone()); } return res; } catch (err) { const cached = await cache.match(key); if (cached) { const headers = new Headers(cached.headers); headers.set('X-From-Cache', '1'); return new Response(await cached.arrayBuffer(), { status: 200, statusText: 'OK', headers }); } if (url.pathname.endsWith(DATA_PATH)) { return new Response(JSON.stringify({ errors: ['offline'] }), { status: 503, headers: { 'Content-Type': 'application/json; charset=utf-8', 'Cache-Control': 'no-store' }, }); } return new Response('offline', { status: 503, headers: { 'Cache-Control': 'no-store' } }); } finally { clearTimeout(timer); } } async function cacheFirst(req) { const cache = await caches.open(VERSION); const cached = await cache.match(req, { ignoreSearch: true }); if (cached) return cached; const res = await fetch(req); if (res.ok) { cache.put(req, res.clone()); } return res; }