/** * Service Worker for justin.deal * Provides caching and offline support */ const CACHE_NAME = 'justin-deal-v1'; // Assets to cache immediately on service worker install const PRECACHE_ASSETS = [ '/', '/index.html', '/favicon.svg', '/site.webmanifest', '/favicons/favicon.png', '/favicons/apple-touch-icon.png', '/favicons/favicon-16x16.png', '/favicons/favicon-32x32.png' ]; // Cache strategies const CACHE_STRATEGIES = { // Cache first, falling back to network CACHE_FIRST: 'cache-first', // Network first, falling back to cache NETWORK_FIRST: 'network-first', // Cache only (no network request) CACHE_ONLY: 'cache-only', // Network only (no cache lookup) NETWORK_ONLY: 'network-only', // Stale while revalidate (serve from cache, update cache in background) STALE_WHILE_REVALIDATE: 'stale-while-revalidate' }; // Route patterns and their corresponding cache strategies const ROUTE_STRATEGIES = [ // HTML pages - network first { pattern: /\.html$|\/$/, strategy: CACHE_STRATEGIES.NETWORK_FIRST }, // CSS and JS - stale while revalidate { pattern: /\.(css|js)$/, strategy: CACHE_STRATEGIES.STALE_WHILE_REVALIDATE }, // Images - cache first { pattern: /\.(jpe?g|png|gif|svg|webp|avif)$/, strategy: CACHE_STRATEGIES.CACHE_FIRST }, // Fonts - cache first { pattern: /\.(woff2?|ttf|otf|eot)$/, strategy: CACHE_STRATEGIES.CACHE_FIRST }, // API requests - network first { pattern: /\/api\//, strategy: CACHE_STRATEGIES.NETWORK_FIRST } ]; // Determine cache strategy for a given URL function getStrategyForUrl(url) { const matchedRoute = ROUTE_STRATEGIES.find(route => route.pattern.test(url)); return matchedRoute ? matchedRoute.strategy : CACHE_STRATEGIES.NETWORK_FIRST; } // Install event - precache critical assets self.addEventListener('install', event => { event.waitUntil( caches.open(CACHE_NAME) .then(cache => cache.addAll(PRECACHE_ASSETS)) .then(() => self.skipWaiting()) ); }); // Activate event - clean up old caches self.addEventListener('activate', event => { event.waitUntil( caches.keys().then(cacheNames => { return Promise.all( cacheNames .filter(cacheName => cacheName !== CACHE_NAME) .map(cacheName => caches.delete(cacheName)) ); }).then(() => self.clients.claim()) ); }); // Helper function to handle network-first strategy async function networkFirstStrategy(request) { try { // Try network first const networkResponse = await fetch(request); // If successful, clone and cache the response if (networkResponse.ok) { const cache = await caches.open(CACHE_NAME); cache.put(request, networkResponse.clone()); } return networkResponse; } catch (error) { // If network fails, try cache const cachedResponse = await caches.match(request); if (cachedResponse) { return cachedResponse; } // If no cache, throw error throw error; } } // Helper function to handle cache-first strategy async function cacheFirstStrategy(request) { // Try cache first const cachedResponse = await caches.match(request); if (cachedResponse) { return cachedResponse; } // If not in cache, get from network const networkResponse = await fetch(request); // Cache the response for future if (networkResponse.ok) { const cache = await caches.open(CACHE_NAME); cache.put(request, networkResponse.clone()); } return networkResponse; } // Helper function to handle stale-while-revalidate strategy async function staleWhileRevalidateStrategy(request) { // Try to get from cache const cachedResponse = await caches.match(request); // Fetch from network to update cache (don't await) const fetchPromise = fetch(request) .then(networkResponse => { if (networkResponse.ok) { const cache = caches.open(CACHE_NAME); cache.then(cache => cache.put(request, networkResponse.clone())); } return networkResponse; }) .catch(error => { console.error('Failed to fetch and update cache:', error); }); // Return cached response immediately if available return cachedResponse || fetchPromise; } // Fetch event - handle all fetch requests self.addEventListener('fetch', event => { // Skip non-GET requests and browser extensions if (event.request.method !== 'GET' || event.request.url.startsWith('chrome-extension://') || event.request.url.includes('extension://')) { return; } // Get the appropriate strategy for this URL const strategy = getStrategyForUrl(event.request.url); // Apply the selected strategy switch (strategy) { case CACHE_STRATEGIES.NETWORK_FIRST: event.respondWith(networkFirstStrategy(event.request)); break; case CACHE_STRATEGIES.CACHE_FIRST: event.respondWith(cacheFirstStrategy(event.request)); break; case CACHE_STRATEGIES.STALE_WHILE_REVALIDATE: event.respondWith(staleWhileRevalidateStrategy(event.request)); break; case CACHE_STRATEGIES.CACHE_ONLY: event.respondWith(caches.match(event.request)); break; case CACHE_STRATEGIES.NETWORK_ONLY: event.respondWith(fetch(event.request)); break; default: // Default to network first event.respondWith(networkFirstStrategy(event.request)); } }); // Handle offline fallback self.addEventListener('fetch', event => { // Only handle HTML navigation requests that fail if (event.request.mode === 'navigate') { event.respondWith( fetch(event.request) .catch(() => { // If fetch fails, return the offline page return caches.match('/offline.html') .then(response => { return response || new Response( '
You are currently offline.
', { headers: { 'Content-Type': 'text/html' } } ); }); }) ); } }); // Listen for messages from the client self.addEventListener('message', event => { if (event.data && event.data.type === 'SKIP_WAITING') { self.skipWaiting(); } });