// Service Worker with automatic versioning and cache invalidation // Version is updated on each build to force cache refresh const BUILD_TIMESTAMP = '__BUILD_TIMESTAMP__'; // Replaced during build const CACHE_NAME = `puffin-calculator-${BUILD_TIMESTAMP}`; const MAX_CACHE_AGE = 24 * 60 * 60 * 1000; // 24 hours in milliseconds const urlsToCache = [ '/', '/mobile-app', '/puffinOffset.webp', '/manifest.json' ]; // Install event - cache resources self.addEventListener('install', (event) => { console.log('[SW] Installing service worker version:', BUILD_TIMESTAMP); event.waitUntil( caches.open(CACHE_NAME) .then((cache) => { console.log('[SW] Caching app shell'); return cache.addAll(urlsToCache); }) .then(() => { // Force the waiting service worker to become the active service worker return self.skipWaiting(); }) .catch((error) => { console.error('[SW] Cache install failed:', error); }) ); }); // Activate event - clear old caches and claim clients self.addEventListener('activate', (event) => { console.log('[SW] Activating service worker version:', BUILD_TIMESTAMP); event.waitUntil( Promise.all([ // Clear old caches caches.keys().then((cacheNames) => { return Promise.all( cacheNames.map((cacheName) => { if (cacheName !== CACHE_NAME) { console.log('[SW] Clearing old cache:', cacheName); return caches.delete(cacheName); } }) ); }), // Claim all clients immediately self.clients.claim() ]) ); }); // Fetch event - Network first, fall back to cache self.addEventListener('fetch', (event) => { const { request } = event; const url = new URL(request.url); // Skip cross-origin requests if (url.origin !== location.origin) { return; } event.respondWith( // Try network first fetch(request) .then((response) => { // Don't cache if not a success response if (!response || response.status !== 200 || response.type !== 'basic') { return response; } // Clone the response const responseToCache = response.clone(); // Cache the fetched resource caches.open(CACHE_NAME).then((cache) => { cache.put(request, responseToCache); }); return response; }) .catch(() => { // Network failed, try cache return caches.match(request).then((cachedResponse) => { if (cachedResponse) { // Check cache age for HTML documents if (request.destination === 'document') { const cacheDate = cachedResponse.headers.get('sw-cache-date'); if (cacheDate) { const age = Date.now() - parseInt(cacheDate, 10); if (age > MAX_CACHE_AGE) { console.log('[SW] Cached HTML is too old, returning without cache'); return new Response('Cache expired. Please connect to the internet.', { status: 503, statusText: 'Service Unavailable' }); } } } return cachedResponse; } // Not in cache and network failed return new Response('Offline and not cached', { status: 503, statusText: 'Service Unavailable' }); }); }) ); }); // Listen for skip waiting message self.addEventListener('message', (event) => { if (event.data && event.data.type === 'SKIP_WAITING') { console.log('[SW] Received SKIP_WAITING message'); self.skipWaiting(); } });