const CACHE_VERSION = typeof __APP_CACHE_VERSION__ !== 'undefined' ? __APP_CACHE_VERSION__ : 'nexus-timer-cache-fallback-dev-vManual'; const APP_SHELL_URLS = [ // Precache the root (index.html) explicitly for better offline fallback '/', '/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', ]; 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); }) .then(() => { console.log(`[SW ${CACHE_VERSION}] Skip waiting on install.`); return 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(() => { console.log(`[SW ${CACHE_VERSION}] Clients claimed.`); return self.clients.claim(); }) ); }); self.addEventListener('fetch', event => { const { request } = event; const url = new URL(request.url); if (request.method !== 'GET' || !url.protocol.startsWith('http')) { return; } // Strategy 1: Network First, then Cache for Navigation/HTML requests if (request.mode === 'navigate' || request.destination === 'document' || url.pathname === '/') { // console.log(`[SW ${CACHE_VERSION}] NetworkFirst for navigation/document: ${request.url}`); event.respondWith( fetch(request) .then(networkResponse => { // If successful, cache the response and return it if (networkResponse.ok) { const responseToCache = networkResponse.clone(); caches.open(CACHE_VERSION).then(cache => { // For navigations, it's often best to cache the specific URL requested // as well as potentially updating the '/' cache if this is the root. cache.put(request, responseToCache); if (url.pathname === '/') { // Also update root cache if it's the index const rootResponseClone = networkResponse.clone(); // Need another clone cache.put('/', rootResponseClone); } }); } return networkResponse; }) .catch(async () => { // Network failed. Try to serve from cache. // console.warn(`[SW ${CACHE_VERSION}] Network fetch failed for ${request.url}. Attempting cache.`); // 1. Try matching the specific request first (e.g. /info, /game) const cachedResponse = await caches.match(request); if (cachedResponse) { // console.log(`[SW ${CACHE_VERSION}] Serving from cache (specific request): ${request.url}`); return cachedResponse; } // 2. If specific request not found, try serving the app shell ('/') // This is crucial for SPAs to work offline. const appShellResponse = await caches.match('/'); if (appShellResponse) { // console.log(`[SW ${CACHE_VERSION}] Serving app shell ('/') from cache for: ${request.url}`); return appShellResponse; } // 3. If even the app shell is not in cache (shouldn't happen if install was successful) console.error(`[SW ${CACHE_VERSION}] CRITICAL: Network and cache miss for navigation AND app shell ('/') for: ${request.url}`); // Return a very basic offline message, but ideally this state is avoided. return new Response( `
The application is currently offline and the requested page could not be loaded from the cache. Please check your connection.
`, { headers: { 'Content-Type': 'text/html' } } ); }) ); return; } // Strategy 2: Stale-While-Revalidate for assets (CSS, JS, images, fonts, workers) if (request.destination === 'style' || request.destination === 'script' || request.destination === 'worker' || request.destination === 'image' || request.destination === 'font') { // console.log(`[SW ${CACHE_VERSION}] StaleWhileRevalidate for asset: ${request.url}`); event.respondWith( caches.open(CACHE_VERSION).then(cache => { return cache.match(request).then(cachedResponse => { const fetchPromise = fetch(request).then(networkResponse => { if (networkResponse.ok) { const responseToCache = networkResponse.clone(); cache.put(request, responseToCache); } return networkResponse; }).catch(err => { // If fetch fails, and we served from cache, it's fine. // If cache also missed, this error will propagate. // console.warn(`[SW ${CACHE_VERSION}] SWR: Network fetch error for ${request.url}`, err); throw err; }); return cachedResponse || fetchPromise; }).catch(() => { // Fallback to network if cache.match fails // console.warn(`[SW ${CACHE_VERSION}] SWR: Cache match error for ${request.url}, trying network directly.`); return fetch(request); }); }) ); return; } // Strategy 3: Cache First for other types of requests (e.g., manifest.json if not in APP_SHELL_URLS) // console.log(`[SW ${CACHE_VERSION}] CacheFirst for: ${request.url}`); 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; }); }) ); }); self.addEventListener('message', event => { if (event.data && event.data.action === 'skipWaiting') { console.log(`[SW ${CACHE_VERSION}] Received skipWaiting message, activating new SW.`); self.skipWaiting(); } });