// This global constant __APP_CACHE_VERSION__ will be replaced by Vite // during the build process due to the `define` config in vite.config.js. const CACHE_VERSION = typeof __APP_CACHE_VERSION__ !== 'undefined' ? __APP_CACHE_VERSION__ : 'nexus-timer-cache-fallback-dev-vManual'; // Fallback for dev or if define fails const APP_SHELL_URLS = [ // Note: '/' (index.html) is handled by NetworkFirst strategy, no need to precache explicitly here. '/manifest.json', // Will be served from public, copied to dist root '/favicon.ico', // Will be served from public, copied to dist root // Icons from public/icons, will be copied to dist/icons '/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', // Any other critical static assets from the public folder that should be part of the app shell ]; 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')) { // console.log(`[SW ${CACHE_VERSION}] Ignoring non-GET or non-http(s) request: ${request.url}`); return; } // Strategy 1: Network First for HTML (navigations or direct / request) if (request.mode === 'navigate' || (request.destination === 'document' || url.pathname === '/')) { // console.log(`[SW ${CACHE_VERSION}] NetworkFirst for: ${request.url}`); event.respondWith( fetch(request) .then(response => { if (response.ok) { const responseClone = response.clone(); caches.open(CACHE_VERSION).then(cache => cache.put(request, responseClone)); } return response; }) .catch(async () => { // console.warn(`[SW ${CACHE_VERSION}] Network fetch failed for ${request.url}, trying cache.`); const cachedResponse = await caches.match(request); if (cachedResponse) return cachedResponse; // Fallback to root /index.html from cache if specific page not found offline const rootCache = await caches.match('/'); if (rootCache) return rootCache; // console.error(`[SW ${CACHE_VERSION}] Network and cache miss for navigation: ${request.url}`); return new Response('Network error: You are offline and this page is not cached.', { status: 404, statusText: 'Not Found', headers: { 'Content-Type': 'text/html' } // Important for SPA offline fallback }); }) ); return; } // Strategy 2: Stale-While-Revalidate for assets (CSS, JS, images, fonts) if (request.destination === 'style' || request.destination === 'script' || request.destination === 'worker' || request.destination === 'image' || request.destination === 'font') { // console.log(`[SW ${CACHE_VERSION}] StaleWhileRevalidate for: ${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); } else { // console.warn(`[SW ${CACHE_VERSION}] StaleWhileRevalidate: Network fetch for ${request.url} failed with status ${networkResponse.status}`); } return networkResponse; }).catch(err => { // console.warn(`[SW ${CACHE_VERSION}] StaleWhileRevalidate: Network fetch error for ${request.url}:`, err); // If fetch fails, and we already served from cache, that's okay. // If cache also missed (i.e., cachedResponse was null), then this error will propagate. throw err; }); return cachedResponse || fetchPromise; }).catch(err => { // This catch block handles errors from cache.match() or if fetchPromise was returned and rejected // console.error(`[SW ${CACHE_VERSION}] StaleWhileRevalidate: Error for ${request.url}. Trying network fallback.`, err); return fetch(request); // Final fallback to network if cache interactions fail }); }) ); 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(); } });