const CACHE_VERSION = 'nexus-timer-cache-v10'; const APP_SHELL_URLS = [ // '/', // Let NetworkFirst handle '/' '/manifest.json', '/favicon.ico', '/icons/icon-192x192.png', '/icons/icon-512x512.png', '/icons/maskable-icon-192x192.png', '/icons/maskable-icon-512x512.png', '/icons/shortcut-setup-96x96.png', '/icons/shortcut-info-96x96.png', // Add Vite output paths? Usually hard due to hashing. Rely on dynamic caching. ]; self.addEventListener('install', event => { console.log(`[SW ${CACHE_VERSION}] Install`); event.waitUntil( caches.open(CACHE_VERSION) .then(cache => { console.log(`[SW ${CACHE_VERSION}] Caching app shell essentials`); return cache.addAll(APP_SHELL_URLS); // Cache core static assets }) .then(() => self.skipWaiting()) ); }); self.addEventListener('activate', event => { console.log(`[SW ${CACHE_VERSION}] Activate`); event.waitUntil( caches.keys().then(cacheNames => { return Promise.all( cacheNames.map(cacheName => { if (cacheName !== CACHE_VERSION) { console.log(`[SW ${CACHE_VERSION}] Deleting old cache: ${cacheName}`); return caches.delete(cacheName); } }) ); }).then(() => self.clients.claim()) ); }); self.addEventListener('fetch', event => { const { request } = event; const url = new URL(request.url); // Ignore non-GET requests & non-http protocols if (request.method !== 'GET' || !url.protocol.startsWith('http')) { return; } // --- Strategy 1: Network First for HTML --- if (request.mode === 'navigate' || (request.destination === 'document' || url.pathname === '/')) { event.respondWith( fetch(request) .then(response => { // If fetch is successful, cache it and return it if (response.ok) { const responseClone = response.clone(); caches.open(CACHE_VERSION).then(cache => cache.put(request, responseClone)); } return response; }) .catch(async () => { // If fetch fails, try cache const cachedResponse = await caches.match(request); if (cachedResponse) { return cachedResponse; } // If specific request not in cache, try root '/' as SPA fallback const rootCache = await caches.match('/'); if (rootCache) { return rootCache; } // Optional: return a proper offline fallback page if available // return caches.match('/offline.html'); // Or just let the browser show its offline error return new Response('Network error and no cache found.', { status: 404, statusText: 'Not Found' }); }) ); return; } // --- Strategy 2: Stale-While-Revalidate for assets --- if (request.destination === 'style' || request.destination === 'script' || request.destination === 'worker' || request.destination === 'image' || request.destination === 'font') { event.respondWith( caches.open(CACHE_VERSION).then(cache => { // Open cache first return cache.match(request).then(cachedResponse => { // Fetch in parallel (don't wait for it here) const fetchPromise = fetch(request).then(networkResponse => { // Check if fetch was successful if (networkResponse.ok) { // Clone before caching const responseToCache = networkResponse.clone(); // Update the cache with the network response cache.put(request, responseToCache); } else { console.warn(`[SW ${CACHE_VERSION}] Fetch for ${request.url} failed with status ${networkResponse.status}`); } // Return the original network response for the fetch promise resolution // This isn't directly used for the *initial* response unless cache misses. return networkResponse; }).catch(err => { console.warn(`[SW ${CACHE_VERSION}] Fetch error for ${request.url}:`, err); // If fetch fails, we just rely on the cache if it existed. // Re-throw error so if cache also missed, browser knows resource failed. throw err; }); // Return the cached response immediately if available. // If not cached, this strategy requires waiting for the fetch. // The common implementation returns cache THEN fetches, but if cache misses, // the user waits for the network. Let's return fetchPromise if no cache. return cachedResponse || fetchPromise; }).catch(err => { // Error during cache match or subsequent fetch console.error(`[SW ${CACHE_VERSION}] Error handling fetch for ${request.url}:`, err); // Fallback to network just in case cache interaction failed? Or let browser handle. // Let's try network as a last resort if cache fails. return fetch(request); }); }) ); return; } // --- Strategy 3: Cache First for others --- // (e.g., manifest, potentially icons not pre-cached) event.respondWith( caches.match(request) .then(response => { return response || fetch(request).then(networkResponse => { if(networkResponse.ok) { const responseClone = networkResponse.clone(); caches.open(CACHE_VERSION).then(cache => cache.put(request, responseClone)); } return networkResponse; }); }) ); }); // Listener for skipWaiting message self.addEventListener('message', event => { if (event.data && event.data.action === 'skipWaiting') { console.log('[SW] Received skipWaiting message, activating new SW.'); self.skipWaiting(); } });