// Author: Simon-Pierre Boucher // Contact: contact@spboucher.ai // Project: chat.spboucher.ai // Service worker: cache the app shell + static assets for instant cold starts. // Conversation data stays network-first — the server DB is the single source of truth. const CACHE = "spb-shell-v1"; const SHELL = ["/manifest.webmanifest", "/icons/icon.svg"]; self.addEventListener("install", (event) => { event.waitUntil(caches.open(CACHE).then((c) => c.addAll(SHELL))); self.skipWaiting(); }); self.addEventListener("activate", (event) => { event.waitUntil( caches.keys().then((keys) => Promise.all(keys.filter((k) => k !== CACHE).map((k) => caches.delete(k)))) ); self.clients.claim(); }); self.addEventListener("fetch", (event) => { const url = new URL(event.request.url); if (event.request.method !== "GET") return; // Never cache API traffic or streams. if (url.pathname.startsWith("/api/")) return; // Static assets: cache-first (immutable Next chunks + icons). if (url.pathname.startsWith("/_next/static/") || url.pathname.startsWith("/icons/")) { event.respondWith( caches.match(event.request).then( (hit) => hit || fetch(event.request).then((res) => { const copy = res.clone(); caches.open(CACHE).then((c) => c.put(event.request, copy)); return res; }) ) ); return; } // Pages: network-first with cache fallback so the shell opens offline. event.respondWith( fetch(event.request) .then((res) => { if (res.ok && (url.pathname === "/" || url.pathname === "/login")) { const copy = res.clone(); caches.open(CACHE).then((c) => c.put(event.request, copy)); } return res; }) .catch(() => caches.match(event.request).then((hit) => hit || caches.match("/"))) ); });