1// public/sw.js - Service Worker for caching and offline support 2// Version: 2.1.0 - Fixed caching strategy to prevent stale CSS/JS 3 4const CACHE_VERSION = '2.1.0'; 5const STATIC_CACHE = 'cc-merch-static-v' + CACHE_VERSION; 6const API_CACHE = 'cc-merch-api-v' + CACHE_VERSION; 7 8// Only cache truly static assets on install (not HTML pages) 9const PRECACHE_FILES = [ 10 '/manifest.json', 11 '/favicon.ico', 12 '/icon-192.png', 13 '/icon-96.png' 14]; 15 16// Install event - cache static assets only 17self.addEventListener('install', (event) => { 18 console.log('[SW] Installing service worker v' + CACHE_VERSION); 19 event.waitUntil( 20 caches.open(STATIC_CACHE) 21 .then((cache) => { 22 console.log('[SW] Caching precache files'); 23 return cache.addAll(PRECACHE_FILES); 24 }) 25 .then(() => { 26 console.log('[SW] Precache complete'); 27 return self.skipWaiting(); 28 }) 29 .catch((error) => { 30 console.error('[SW] Failed to cache precache files:', error); 31 }) 32 ); 33}); 34 35// Activate event - clean up ALL old caches 36self.addEventListener('activate', (event) => { 37 console.log('[SW] Activating service worker v' + CACHE_VERSION); 38 event.waitUntil( 39 caches.keys() 40 .then((cacheNames) => { 41 return Promise.all( 42 cacheNames.map((cacheName) => { 43 // Delete any cache that doesn't match current version 44 if (cacheName !== STATIC_CACHE && cacheName !== API_CACHE) { 45 console.log('[SW] Deleting old cache:', cacheName); 46 return caches.delete(cacheName); 47 } 48 }) 49 ); 50 }) 51 .then(() => { 52 console.log('[SW] Service worker activated'); 53 return self.clients.claim(); 54 }) 55 ); 56}); 57 58// Fetch event - route requests to appropriate handler 59self.addEventListener('fetch', (event) => { 60 const { request } = event; 61 const url = new URL(request.url); 62 63 // Only handle same-origin requests 64 if (url.origin !== location.origin) return; 65 66 // The Cache API can only store GET responses, so anything else (the 67 // sendBeacon POSTs from AnalyticsTracker, form submissions, mutations) 68 // goes straight to the network untouched. 69 if (request.method !== 'GET') return; 70 71 // Handle API requests - network-first 72 if (url.pathname.startsWith('/api/')) { 73 event.respondWith(handleApiRequest(request)); 74 return; 75 } 76 77 // Handle navigation requests (HTML pages) - ALWAYS network-first 78 if (request.mode === 'navigate') { 79 event.respondWith(handleNavigationRequest(request)); 80 return; 81 } 82 83 // Handle immutable Next.js static assets - cache-first (these have content hashes) 84 if (url.pathname.startsWith('/_next/static/')) { 85 event.respondWith(handleImmutableStaticRequest(request)); 86 return; 87 } 88 89 // All other GET requests - network-first with cache fallback 90 event.respondWith(handleNetworkFirstRequest(request)); 91}); 92 93// Navigation requests (HTML pages) - ALWAYS network-first 94// This is critical: ensures fresh HTML that references correct CSS/JS chunk URLs 95async function handleNavigationRequest(request) { 96 try { 97 const networkResponse = await fetch(request); 98 return networkResponse; 99 } catch (error) { 100 console.log('[SW] Navigation failed, serving offline page'); 101 const cachedOffline = await caches.match('/offline'); 102 if (cachedOffline) return cachedOffline; 103 104 return new Response('Offline - Please check your connection', { 105 status: 503, 106 headers: { 'Content-Type': 'text/html' } 107 }); 108 } 109} 110 111// Immutable static assets (/_next/static/*) - cache-first 112// These files have content hashes in their URLs, so cache-first is safe 113async function handleImmutableStaticRequest(request) { 114 const cachedResponse = await caches.match(request); 115 if (cachedResponse) { 116 return cachedResponse; 117 } 118 119 try { 120 const networkResponse = await fetch(request); 121 if (networkResponse.ok) { 122 const cache = await caches.open(STATIC_CACHE); 123 cache.put(request, networkResponse.clone()); 124 } 125 return networkResponse; 126 } catch (error) { 127 console.log('[SW] Failed to fetch static asset:', request.url); 128 throw error; 129 } 130} 131 132// Generic network-first handler for other static files 133async function handleNetworkFirstRequest(request) { 134 try { 135 const networkResponse = await fetch(request); 136 if (networkResponse.ok) { 137 const cache = await caches.open(STATIC_CACHE); 138 cache.put(request, networkResponse.clone()); 139 } 140 return networkResponse; 141 } catch (error) { 142 const cachedResponse = await caches.match(request); 143 if (cachedResponse) { 144 console.log('[SW] Serving from cache:', request.url); 145 return cachedResponse; 146 } 147 throw error; 148 } 149} 150 151// Handle API requests with network-first strategy 152async function handleApiRequest(request) { 153 try { 154 const networkResponse = await fetch(request); 155 156 if (networkResponse.ok) { 157 const cache = await caches.open(API_CACHE); 158 cache.put(request, networkResponse.clone()); 159 return networkResponse; 160 } 161 162 const cachedResponse = await caches.match(request); 163 if (cachedResponse) { 164 console.log('[SW] Serving API from cache:', request.url); 165 return cachedResponse; 166 } 167 168 return networkResponse; 169 } catch (error) {
170 console.log('[SW] Network failed, trying cache for:', request.url); 171 172 const cachedResponse = await caches.match(request); 173 if (cachedResponse) { 174 return cachedResponse; 175 } 176 177 return new Response( 178 JSON.stringify({ 179 message: 'Offline - Please check your connection', 180 offers: [], 181 currentPage: 1, 182 totalPages: 1, 183 totalResults: 0 184 }), 185 { 186 status: 503, 187 statusText: 'Service Unavailable', 188 headers: { 189 'Content-Type': 'application/json', 190 'Cache-Control': 'no-cache' 191 } 192 } 193 ); 194 } 195} 196 197// Background sync for offline actions 198self.addEventListener('sync', (event) => { 199 if (event.tag === 'background-sync') { 200 console.log('[SW] Background sync triggered'); 201 event.waitUntil(doBackgroundSync()); 202 } 203}); 204 205async function doBackgroundSync() { 206 try { 207 console.log('[SW] Background sync completed'); 208 } catch (error) { 209 console.error('[SW] Background sync failed:', error); 210 } 211} 212 213// Push notification handling 214self.addEventListener('push', (event) => { 215 if (event.data) { 216 const data = event.data.json(); 217 const options = { 218 body: data.body || 'New offers available!', 219 icon: '/icon-192.png', 220 badge: '/icon-96.png', 221 vibrate: [100, 50, 100], 222 data: { 223 dateOfArrival: Date.now(), 224 primaryKey: 1 225 }, 226 actions: [ 227 { 228 action: 'explore', 229 title: 'View Offers', 230 icon: '/icon-96.png' 231 }, 232 { 233 action: 'close', 234 title: 'Close', 235 icon: '/icon-96.png' 236 } 237 ] 238 }; 239 240 event.waitUntil( 241 self.registration.showNotification(data.title || 'CC Merch Offers', options) 242 ); 243 } 244}); 245 246// Notification click handling 247self.addEventListener('notificationclick', (event) => { 248 event.notification.close(); 249 250 if (event.action === 'explore') { 251 event.waitUntil( 252 clients.openWindow('/') 253 ); 254 } 255}); 256 257// Message handling for communication with main thread 258self.addEventListener('message', (event) => { 259 if (event.data && event.data.type === 'SKIP_WAITING') { 260 self.skipWaiting(); 261 } 262});
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.