// MindSift service worker: keeps the app shell so the PWA also starts without network. // - Page loads (navigation): network first, so a new release is picked up right away; offline the // last loaded index.html is used. // - Built bundles (/assets/*, file names contain a content hash): from the cache, loaded once. // - Icons and manifest: from the cache, refreshed in the background. // - API requests are never touched; offline data comes from the app's own cache (lib/offline.ts). // Raise VERSION when this file changes in a way that needs fresh caches. const VERSION = 'v2'; const SHELL_CACHE = `mindsift-shell-${VERSION}`; const ASSET_CACHE = `mindsift-assets-${VERSION}`; const MAX_ASSETS = 80; const SHELL_URLS = [ '/', '/index.html', '/manifest.json', '/favicon.ico', '/icon.svg', '/icon-192x192.png', '/icon-512x512.png', '/apple-touch-icon.png', ]; self.addEventListener('install', (event) => { event.waitUntil( caches .open(SHELL_CACHE) .then((cache) => Promise.all(SHELL_URLS.map((url) => cache.add(new Request(url, { cache: 'reload' })).catch(() => {})))) .then(() => self.skipWaiting()) ); }); self.addEventListener('activate', (event) => { event.waitUntil( caches .keys() .then((keys) => Promise.all(keys.filter((k) => k !== SHELL_CACHE && k !== ASSET_CACHE).map((k) => caches.delete(k)))) .then(() => self.clients.claim()) ); }); // Old bundles pile up with every release; keep only the newest entries async function trimAssets() { const cache = await caches.open(ASSET_CACHE); const keys = await cache.keys(); await Promise.all(keys.slice(0, Math.max(0, keys.length - MAX_ASSETS)).map((k) => cache.delete(k))); } async function networkFirstPage(request) { const cache = await caches.open(SHELL_CACHE); try { const response = await fetch(request, { cache: 'no-store' }); if (response.ok && response.type === 'basic') cache.put('/index.html', response.clone()); return response; } catch { return (await cache.match('/index.html')) || (await cache.match('/')) || Response.error(); } } async function cacheFirstAsset(request) { const cache = await caches.open(ASSET_CACHE); const hit = await cache.match(request); if (hit) return hit; const response = await fetch(request); if (response.ok && response.type === 'basic') { await cache.put(request, response.clone()); trimAssets(); } return response; } async function staleWhileRevalidate(request) { const cache = await caches.open(SHELL_CACHE); const hit = await cache.match(request); const refresh = fetch(request) .then((response) => { if (response.ok && response.type === 'basic') cache.put(request, response.clone()); return response; }) .catch(() => hit || Response.error()); return hit || refresh; } self.addEventListener('fetch', (event) => { const { request } = event; if (request.method !== 'GET') return; const url = new URL(request.url); if (url.origin !== self.location.origin || url.pathname.startsWith('/api/')) return; if (request.mode === 'navigate') { event.respondWith(networkFirstPage(request)); } else if (url.pathname.startsWith('/assets/')) { event.respondWith(cacheFirstAsset(request)); } else if (SHELL_URLS.includes(url.pathname)) { event.respondWith(staleWhileRevalidate(request)); } });