/* Build injects a content-derived version and an explicit public-asset allowlist. */ const CONFIG = JSON.parse("__PWA_CONFIG__"); const PREFIX = "mina-habits-static-"; const CACHE = PREFIX + CONFIG.version; const ASSETS = new Set(CONFIG.assets); self.addEventListener("install", event => { event.waitUntil((async () => { const cache = await caches.open(CACHE); // addAll is atomic: a failed download cannot activate a partial release. await cache.addAll(CONFIG.assets.map(url => new Request(url, { cache: "reload", credentials: "omit" }))); })()); }); self.addEventListener("activate", event => { event.waitUntil((async () => { // A user can accept an update while other windows still hold older code. // Preserve their lazy chunks until a later activation with no open windows. const windows = await self.clients.matchAll({ type: "window", includeUncontrolled: true }); if (windows.length === 0) { await Promise.all((await caches.keys()).filter(key => key.startsWith(PREFIX) && key !== CACHE).map(key => caches.delete(key))); } await self.clients.claim(); })()); }); self.addEventListener("message", event => { if (event.data?.type === "ACTIVATE_UPDATE") event.waitUntil(self.skipWaiting()); }); async function cachedAsset(request) { const current = await caches.open(CACHE); const cached = await current.match(request); if (cached) return cached; // Old windows can still request fingerprinted chunks after an accepted update. for (const key of await caches.keys()) { if (key.startsWith(PREFIX) && key !== CACHE) { const response = await (await caches.open(key)).match(request); if (response) return response; } } return fetch(request); } async function navigate(request) { const controller = new AbortController(); const timeout = setTimeout(() => controller.abort(), 4000); try { const response = await fetch(request, { signal: controller.signal }); if (response.ok) return response; } catch { /* Unreachable server or offline: use the public shell. */ } finally { clearTimeout(timeout); } return (await (await caches.open(CACHE)).match(CONFIG.shell)) || Response.error(); } self.addEventListener("fetch", event => { const request = event.request; const url = new URL(request.url); // In particular: no account data, OAuth callbacks, exports, writes or third-party responses. if (request.method !== "GET" || url.origin !== self.location.origin || /^\/api(?:\/|$)/.test(url.pathname)) return; if (request.mode === "navigate") { event.respondWith(navigate(request)); } else if (ASSETS.has(url.pathname) || /^\/chunk-[\w-]+\.js$/.test(url.pathname)) { event.respondWith(cachedAsset(request)); } });