// Service Worker for Ultimate Lists App // Implements caching strategies for offline functionality const CACHE_NAME = 'ultimate-lists-v5'; const RUNTIME_CACHE = 'ultimate-lists-runtime-v5'; // Static assets to cache on install const STATIC_ASSETS = [ '/', '/offline', '/auth/login', '/auth/register', '/auth/error', '/manifest.json', '/icon-192.svg', '/icon-512.svg', ]; // Install event - cache static assets self.addEventListener('install', (event) => { event.waitUntil( caches.open(CACHE_NAME).then((cache) => { return cache.addAll(STATIC_ASSETS).catch((error) => { console.error('Failed to cache static assets:', error); // Continue even if some assets fail to cache return Promise.resolve(); }); }) ); // Activate immediately self.skipWaiting(); }); // Activate event - clean up old caches self.addEventListener('activate', (event) => { event.waitUntil( caches.keys().then((cacheNames) => { return Promise.all( cacheNames .filter((name) => name !== CACHE_NAME && name !== RUNTIME_CACHE) .map((name) => caches.delete(name)) ); }) ); // Take control of all pages immediately return self.clients.claim(); }); // Fetch event - implement caching strategies self.addEventListener('fetch', (event) => { const { request } = event; const url = new URL(request.url); // Skip non-GET requests if (request.method !== 'GET') { return; } // Skip chrome extensions and other protocols if (!url.protocol.startsWith('http')) { return; } // Skip cross-origin requests for performance if (url.origin !== self.location.origin) { return; } // Handle authentication-related requests if (isAuthRequest(url)) { event.respondWith(handleAuthRequest(request)); return; } // Cache-first strategy for static assets if (isStaticAsset(url)) { event.respondWith(cacheFirst(request)); return; } // Stale-while-revalidate for API responses and pages if (isApiRequest(url) || isPageRequest(url)) { event.respondWith(staleWhileRevalidate(request)); return; } // Network-first for everything else event.respondWith(networkFirst(request)); }); // Helper: Check if request is for a static asset function isStaticAsset(url) { const staticExtensions = ['.js', '.css', '.png', '.jpg', '.jpeg', '.svg', '.ico', '.woff', '.woff2']; return staticExtensions.some((ext) => url.pathname.endsWith(ext)); } // Helper: Check if request is an API request function isApiRequest(url) { return url.pathname.startsWith('/api/'); } // Helper: Check if request is a page request function isPageRequest(url) { return url.origin === self.location.origin && !url.pathname.includes('.'); } // Helper: Check if request is authentication-related function isAuthRequest(url) { return url.pathname.startsWith('/auth/') || url.pathname.startsWith('/api/auth/'); } // Strategy: Handle authentication requests async function handleAuthRequest(request) { const url = new URL(request.url); // For authentication API calls, always try network first if (url.pathname.startsWith('/api/auth/')) { try { const response = await fetch(request); return response; } catch (error) { console.error('Authentication API request failed (offline):', error); // Return a JSON error response for API calls return new Response( JSON.stringify({ error: 'Authentication requires an internet connection. Please check your connection and try again.' }), { status: 503, statusText: 'Service Unavailable', headers: { 'Content-Type': 'application/json' } } ); } } // For authentication pages, use cache-first with network fallback const cache = await caches.open(CACHE_NAME); const cached = await cache.match(request); try { const response = await fetch(request); if (response.ok) { cache.put(request, response.clone()); } return response; } catch (error) { console.error('Authentication page request failed:', error); // Return cached version if available if (cached) { return cached; } // Otherwise redirect to offline page return caches.match('/offline'); } } // Strategy: Cache-first (for static assets) async function cacheFirst(request) { const cache = await caches.open(CACHE_NAME); const cached = await cache.match(request); if (cached) { return cached; } try { const response = await fetch(request); if (response.ok) { cache.put(request, response.clone()); } return response; } catch (error) { console.error('Cache-first fetch failed:', error); return new Response('Network error', { status: 408 }); } } // Strategy: Stale-while-revalidate (for API and pages) async function staleWhileRevalidate(request) { const cache = await caches.open(RUNTIME_CACHE); const cached = await cache.match(request); const fetchPromise = fetch(request).then((response) => { // Handle authentication errors (401, 403) if (response.status === 401 || response.status === 403) { // Don't cache authentication errors return response; } if (response.ok) { cache.put(request, response.clone()); } return response; }).catch((error) => { console.error('Stale-while-revalidate fetch failed:', error); // If we have a cached version, return it on network error if (cached) { return cached; } // Otherwise, return offline page for navigation requests if (request.mode === 'navigate') { return caches.match('/offline'); } throw error; }); return cached || fetchPromise; } // Strategy: Network-first (fallback) async function networkFirst(request) { try { const response = await fetch(request); // Handle authentication errors (401, 403) if (response.status === 401 || response.status === 403) { // Don't cache authentication errors return response; } if (response.ok) { const cache = await caches.open(RUNTIME_CACHE); cache.put(request, response.clone()); } return response; } catch (error) { const cache = await caches.open(RUNTIME_CACHE); const cached = await cache.match(request); if (cached) { return cached; } // Return offline page for navigation requests if (request.mode === 'navigate') { return caches.match('/offline'); } throw error; } } // Background Sync - sync pending mutations when connection is restored self.addEventListener('sync', (event) => { if (event.tag === 'sync-mutations') { event.waitUntil(syncMutations()); } }); // Sync mutations from the queue async function syncMutations() { try { // Notify all clients to trigger sync const clients = await self.clients.matchAll(); clients.forEach((client) => { client.postMessage({ type: 'SYNC_MUTATIONS', }); }); } catch (error) { console.error('Background sync failed:', error); throw error; // Re-throw to retry later } } // Listen for messages from clients self.addEventListener('message', (event) => { if (event.data && event.data.type === 'SKIP_WAITING') { self.skipWaiting(); } if (event.data && event.data.type === 'REGISTER_SYNC') { // Register background sync if (self.registration.sync) { self.registration.sync.register('sync-mutations').catch((error) => { console.error('Failed to register background sync:', error); }); } } });