// BUILD is substituted per-deploy by the server with a hash of the static // assets (see serveSW in server/main.go), so the cache name — and therefore the // bytes of this file — change whenever any asset changes. That byte difference // is what makes the browser install a new worker and surface the update prompt. // Served unsubstituted (dev / a plain static host) it stays a valid constant. const BUILD = "__BUILD_HASH__"; const CACHE = `puppy-tracker-${BUILD}`; const PHOTO_CACHE = "puppy-tracker-photos-v1"; const ASSETS = [ "./", "./index.html", "./style.css", "./app.js", "./manifest.json", "./icon.svg", "./changelog.json", ]; self.addEventListener("install", (event) => { // cache: "reload" bypasses the browser's HTTP cache, so a new build always // caches assets fetched fresh from the server. Without it, addAll could mix // a fresh index.html with a heuristically-cached stale app.js and install a // build whose markup references listeners the old script never registers. event.waitUntil( caches.open(CACHE).then((cache) => cache.addAll(ASSETS.map((u) => new Request(u, { cache: "reload" }))) ) ); // No skipWaiting() here: a new worker stays in "waiting" while an old one is // controlling a tab, so the page can prompt before swapping assets out from // under it. The page tells us to activate via a SKIP_WAITING message. }); self.addEventListener("message", (event) => { if (event.data && event.data.type === "SKIP_WAITING") self.skipWaiting(); }); self.addEventListener("activate", (event) => { event.waitUntil( caches.keys().then((keys) => Promise.all( keys .filter((k) => k !== CACHE && k !== PHOTO_CACHE) .map((k) => caches.delete(k)) ) ) ); self.clients.claim(); }); self.addEventListener("fetch", (event) => { const req = event.request; if (req.method !== "GET") return; const url = new URL(req.url); // Photos are immutable per UUID — cache-first forever and stuff every // successful fetch into a dedicated cache so they survive between visits // and are available offline once seen. if (url.pathname.includes("/api/photos/")) { event.respondWith( caches.open(PHOTO_CACHE).then(async (cache) => { const cached = await cache.match(req); if (cached) return cached; try { const res = await fetch(req); if (res && res.ok) cache.put(req, res.clone()); return res; } catch (err) { return cached || Response.error(); } }) ); return; } // Other API calls: never cache — sync must reflect live server state. if (url.pathname.includes("/api/")) return; event.respondWith( caches.match(req).then((cached) => { if (cached) return cached; return fetch(req) .then((res) => { if (res && res.ok && url.origin === self.location.origin) { const clone = res.clone(); caches.open(CACHE).then((c) => c.put(req, clone)); } return res; }) .catch(() => caches.match("./index.html")); }) ); });