// TouchBase service worker — minimal, hand-rolled. // // Strategy: // - HTML pages (text/html): network-first → cache fallback for offline. // - Static assets (/_next/static, /icon.svg, fonts): stale-while-revalidate. // - API + auth + booking server actions: pass-through, NEVER cached. // Stale appointment data is dangerous; better to fail than to lie. // // Cache name is versioned. Bump CACHE_VERSION on any deploy that should // invalidate the cache for returning users. const CACHE_VERSION = "v1"; const HTML_CACHE = `tb-html-${CACHE_VERSION}`; const STATIC_CACHE = `tb-static-${CACHE_VERSION}`; const ALL_CACHES = [HTML_CACHE, STATIC_CACHE]; self.addEventListener("install", () => { // Activate the new SW immediately; users get the new caching rules on next nav. self.skipWaiting(); }); self.addEventListener("activate", (event) => { event.waitUntil( (async () => { const names = await caches.keys(); await Promise.all( names .filter((n) => n.startsWith("tb-") && !ALL_CACHES.includes(n)) .map((n) => caches.delete(n)), ); await self.clients.claim(); })(), ); }); self.addEventListener("fetch", (event) => { const req = event.request; // Only handle GET. Server actions (POST), auth callbacks, etc. always pass through. if (req.method !== "GET") return; const url = new URL(req.url); // Same-origin only — don't cache cross-origin (analytics, fonts CDN, etc.). if (url.origin !== self.location.origin) return; // Never cache API, auth, or anything in the booking flow that could go stale. // Stale slot data could let a customer "see" an already-taken slot. if ( url.pathname.startsWith("/api/") || url.pathname.startsWith("/_next/data/") || url.pathname.startsWith("/admin/") || url.pathname.startsWith("/account/") || url.pathname.startsWith("/therapist/") || url.pathname.startsWith("/book") ) { return; // pass through } // Static assets → stale-while-revalidate if ( url.pathname.startsWith("/_next/static/") || url.pathname === "/icon.svg" || url.pathname === "/icon-mask.svg" || url.pathname === "/favicon.ico" || url.pathname === "/manifest.webmanifest" ) { event.respondWith(staleWhileRevalidate(req, STATIC_CACHE)); return; } // HTML navigation → network-first → cache fallback if (req.mode === "navigate" || req.headers.get("accept")?.includes("text/html")) { event.respondWith(networkFirst(req, HTML_CACHE)); return; } }); async function networkFirst(request, cacheName) { const cache = await caches.open(cacheName); try { const fresh = await fetch(request); if (fresh && fresh.ok) { cache.put(request, fresh.clone()); } return fresh; } catch (err) { const cached = await cache.match(request); if (cached) return cached; throw err; } } async function staleWhileRevalidate(request, cacheName) { const cache = await caches.open(cacheName); const cached = await cache.match(request); const fetchPromise = fetch(request) .then((response) => { if (response && response.ok) { cache.put(request, response.clone()); } return response; }) .catch(() => undefined); return cached ?? (await fetchPromise) ?? new Response("offline", { status: 503 }); }