1// public/sw.js - Service Worker for caching and offline support 2// Version: 2.2.0 - Per-user API responses and page data are no longer cached. 3// Bumping the version makes activate delete caches written by earlier workers. 4 5const CACHE_VERSION = '2.2.0'; 6const STATIC_CACHE = 'cc-merch-static-v' + CACHE_VERSION; 7const API_CACHE = 'cc-merch-api-v' + CACHE_VERSION; 8 9// Only cache truly static assets on install (not HTML pages) 10const PRECACHE_FILES = [ 11 '/manifest.json', 12 '/favicon.ico', 13 '/icon-192.png', 14 '/icon-96.png' 15]; 16 17// Install event - cache static assets only 18self.addEventListener('install', (event) => { 19 console.log('[SW] Installing service worker v' + CACHE_VERSION); 20 event.waitUntil( 21 caches.open(STATIC_CACHE) 22 .then((cache) => { 23 console.log('[SW] Caching precache files'); 24 return cache.addAll(PRECACHE_FILES); 25 }) 26 .then(() => { 27 console.log('[SW] Precache complete'); 28 return self.skipWaiting(); 29 }) 30 .catch((error) => { 31 console.error('[SW] Failed to cache precache files:', error); 32 }) 33 ); 34}); 35 36// Activate event - clean up ALL old caches 37self.addEventListener('activate', (event) => { 38 console.log('[SW] Activating service worker v' + CACHE_VERSION); 39 event.waitUntil( 40 caches.keys() 41 .then((cacheNames) => { 42 return Promise.all( 43 cacheNames.map((cacheName) => { 44 // Delete any cache that doesn't match current version 45 if (cacheName !== STATIC_CACHE && cacheName !== API_CACHE) { 46 console.log('[SW] Deleting old cache:', cacheName); 47 return caches.delete(cacheName); 48 } 49 }) 50 ); 51 }) 52 .then(() => { 53 console.log('[SW] Service worker activated'); 54 return self.clients.claim(); 55 }) 56 ); 57}); 58 59// Fetch event - route requests to appropriate handler 60self.addEventListener('fetch', (event) => { 61 const { request } = event; 62 const url = new URL(request.url); 63 64 // Only handle same-origin requests 65 if (url.origin !== location.origin) return; 66 67 // The Cache API can only store GET responses, so anything else (the 68 // sendBeacon POSTs from AnalyticsTracker, form submissions, mutations) 69 // goes straight to the network untouched. 70 if (request.method !== 'GET') return; 71 72 // Handle API requests - network-first 73 if (url.pathname.startsWith('/api/')) { 74 event.respondWith(handleApiRequest(request)); 75 return; 76 } 77 78 // Handle navigation requests (HTML pages) - ALWAYS network-first 79 if (request.mode === 'navigate') { 80 event.respondWith(handleNavigationRequest(request)); 81 return; 82 } 83 84 // Handle immutable Next.js static assets - cache-first (these have content hashes) 85 if (url.pathname.startsWith('/_next/static/')) { 86 event.respondWith(handleImmutableStaticRequest(request)); 87 return; 88 } 89 90 // Page data the client router fetches on a navigation (?_rsc=, RSC: 1) 91 // carries the signed-in user's view of the page, and next.config.mjs marks 92 // it public, so no header says to keep it out. Leave it to the network. 93 if (url.searchParams.has('_rsc') || request.headers.get('RSC') === '1') return; 94 95 // All other GET requests - network-first with cache fallback 96 event.respondWith(handleNetworkFirstRequest(request)); 97}); 98 99// Navigation requests (HTML pages) - ALWAYS network-first 100// This is critical: ensures fresh HTML that references correct CSS/JS chunk URLs 101async function handleNavigationRequest(request) { 102 try { 103 const networkResponse = await fetch(request); 104 return networkResponse; 105 } catch (error) { 106 console.log('[SW] Navigation failed, serving offline page'); 107 const cachedOffline = await caches.match('/offline'); 108 if (cachedOffline) return cachedOffline; 109 110 return new Response('Offline - Please check your connection', { 111 status: 503, 112 headers: { 'Content-Type': 'text/html' } 113 }); 114 } 115} 116 117// Immutable static assets (/_next/static/*) - cache-first 118// These files have content hashes in their URLs, so cache-first is safe 119async function handleImmutableStaticRequest(request) { 120 const cachedResponse = await caches.match(request); 121 if (cachedResponse) { 122 return cachedResponse; 123 } 124 125 try { 126 const networkResponse = await fetch(request); 127 if (networkResponse.ok) { 128 const cache = await caches.open(STATIC_CACHE); 129 cache.put(request, networkResponse.clone()); 130 } 131 return networkResponse; 132 } catch (error) { 133 console.log('[SW] Failed to fetch static asset:', request.url); 134 throw error; 135 } 136} 137 138// Generic network-first handler for other static files 139async function handleNetworkFirstRequest(request) { 140 try { 141 const networkResponse = await fetch(request); 142 if (networkResponse.ok) { 143 const cache = await caches.open(STATIC_CACHE); 144 cache.put(request, networkResponse.clone()); 145 } 146 return networkResponse; 147 } catch (error) { 148 const cachedResponse = await caches.match(request); 149 if (cachedResponse) { 150 console.log('[SW] Serving from cache:', request.url); 151 return cachedResponse; 152 } 153 throw error; 154 } 155} 156 157// Only responses that are the same for every visitor may be kept. The Cache
158// API cannot tell signed-in users apart (cookies are not part of the stored 159// request), so a per-user response cached here would be served to whoever 160// uses this browser next - for example when a sign-out makes the network 161// answer 401 and the fallback below reaches for the cache. 162function isSharedResponse(response) { 163 const cacheControl = (response.headers.get('Cache-Control') || '').toLowerCase(); 164 if (/\b(private|no-store)\b/.test(cacheControl)) return false; 165 const vary = (response.headers.get('Vary') || '').toLowerCase(); 166 return !/\b(cookie|authorization)\b/.test(vary); 167} 168 169// Handle API requests with network-first strategy 170async function handleApiRequest(request) { 171 try { 172 const networkResponse = await fetch(request); 173 174 if (networkResponse.ok) { 175 if (isSharedResponse(networkResponse)) { 176 const cache = await caches.open(API_CACHE); 177 cache.put(request, networkResponse.clone()); 178 } 179 return networkResponse; 180 } 181 182 const cachedResponse = await caches.match(request); 183 if (cachedResponse) { 184 console.log('[SW] Serving API from cache:', request.url); 185 return cachedResponse; 186 } 187 188 return networkResponse; 189 } catch (error) { 190 console.log('[SW] Network failed, trying cache for:', request.url); 191 192 const cachedResponse = await caches.match(request); 193 if (cachedResponse) { 194 return cachedResponse; 195 } 196 197 return new Response( 198 JSON.stringify({ 199 message: 'Offline - Please check your connection', 200 offers: [], 201 currentPage: 1, 202 totalPages: 1, 203 totalResults: 0 204 }), 205 { 206 status: 503, 207 statusText: 'Service Unavailable', 208 headers: { 209 'Content-Type': 'application/json', 210 'Cache-Control': 'no-cache' 211 } 212 } 213 ); 214 } 215} 216 217// Message handling for communication with main thread 218self.addEventListener('message', (event) => { 219 if (event.data && event.data.type === 'SKIP_WAITING') { 220 self.skipWaiting(); 221 } 222});
Line numbers count LF bytes from the start of the resource, as the search results do. Vendor segments are library code the classifier recognised; they are stored but not indexed. Bytes are shown as Latin1 characters, one per byte.