1/** 2 * Recipe Pantry service worker (WO-28). 3 * 4 * History: the previous worker was a cleanup-only script (June 2026) that 5 * deleted every cache and unregistered itself, shipped to undo an aggressive 6 * app-shell precache that served stale HTML after deploys. This worker is the 7 * replacement, built so that failure mode cannot recur: 8 * 9 * - HTML/navigations: NETWORK-FIRST. The network copy always wins; the cache 10 * is only a fallback when the user is offline. Stale HTML is never served 11 * while online. 12 * - Hashed build assets (/assets/*): cache-first â content-hashed filenames 13 * are immutable by construction. 14 * - Images and recipe JSON (/images/*, /api/recipes/*, /onesheets/*): 15 * network-first so replaced recipe data and photos show immediately. 16 * 17 * CACHE_VERSION must be bumped when cache semantics change; old caches are 18 * deleted on activate. sw.js itself is served no-store (vercel.json), so a new 19 * deploy always picks up the new worker promptly. 20 */ 21const CACHE_VERSION = 'pantry-v3-2026-07-31'; 22const HTML_CACHE = `${CACHE_VERSION}-html`; 23const ASSET_CACHE = `${CACHE_VERSION}-assets`; 24const DATA_CACHE = `${CACHE_VERSION}-data`; 25const KNOWN_CACHES = [HTML_CACHE, ASSET_CACHE, DATA_CACHE]; 26 27const SELF_DESTRUCT_HOSTS = new Set([ 28 'localhost', 29 '127.0.0.1', 30 'pantry.bakinggreatbread.com', 31]); 32 33const shouldSelfDestruct = 34 SELF_DESTRUCT_HOSTS.has(self.location.hostname) || 35 self.location.hostname.includes('id-preview--') || 36 self.location.hostname.includes('lovableproject.com'); 37 38async function deleteAllCaches() { 39 const names = await caches.keys(); 40 await Promise.allSettled(names.map((name) => caches.delete(name))); 41} 42 43async function unregisterAndReloadClients() { 44 await deleteAllCaches(); 45 await self.clients.claim(); 46 await self.registration.unregister(); 47 const clients = await self.clients.matchAll({ type: 'window' }); 48 await Promise.allSettled( 49 clients.map((client) => client.navigate(client.url)) 50 ); 51} 52 53self.addEventListener('install', (event) => { 54 if (shouldSelfDestruct) { 55 event.waitUntil(deleteAllCaches().then(() => self.skipWaiting())); 56 return; 57 } 58 59 event.waitUntil( 60 caches.open(HTML_CACHE).then((cache) => cache.add('/').catch(() => undefined)) 61 ); 62 self.skipWaiting(); 63}); 64 65self.addEventListener('activate', (event) => { 66 if (shouldSelfDestruct) { 67 event.waitUntil(unregisterAndReloadClients()); 68 return; 69 } 70 71 event.waitUntil( 72 (async () => { 73 const names = await caches.keys(); 74 await Promise.allSettled( 75 names.filter((n) => !KNOWN_CACHES.includes(n)).map((n) => caches.delete(n)) 76 ); 77 await self.clients.claim(); 78 })() 79 ); 80}); 81 82async function networkFirst(request, cacheName) { 83 const cache = await caches.open(cacheName); 84 try { 85 const response = await fetch(request); 86 if (response.ok) cache.put(request, response.clone()); 87 return response; 88 } catch (err) { 89 const cached = await cache.match(request, { ignoreSearch: request.mode === 'navigate' }); 90 if (cached) return cached; 91 if (request.mode === 'navigate') { 92 const home = await cache.match('/'); 93 if (home) return home; 94 } 95 throw err; 96 } 97} 98 99async function cacheFirst(request, cacheName) { 100 const cache = await caches.open(cacheName); 101 const cached = await cache.match(request); 102 if (cached) return cached; 103 const response = await fetch(request); 104 if (response.ok) cache.put(request, response.clone()); 105 return response; 106} 107 108async function staleWhileRevalidate(request, cacheName) { 109 const cache = await caches.open(cacheName); 110 const cached = await cache.match(request); 111 const network = fetch(request) 112 .then((response) => { 113 if (response.ok) cache.put(request, response.clone()); 114 return response; 115 }) 116 .catch(() => undefined); 117 return cached || (await network) || fetch(request); 118} 119 120self.addEventListener('fetch', (event) => { 121 if (shouldSelfDestruct) return; 122 123 const { request } = event; 124 if (request.method !== 'GET') return; 125 126 const url = new URL(request.url); 127 if (url.origin !== self.location.origin) return; 128 129 // Never intercept the worker itself. 130 if (url.pathname === '/sw.js' || url.pathname === '/service-worker.js') return; 131 132 if (request.mode === 'navigate') { 133 event.respondWith(networkFirst(request, HTML_CACHE)); 134 return; 135 } 136 137 if (url.pathname.startsWith('/assets/')) { 138 event.respondWith(cacheFirst(request, ASSET_CACHE)); 139 return; 140 } 141 142 if (url.pathname.startsWith('/api/recipes/')) { 143 event.respondWith(networkFirst(request, DATA_CACHE)); 144 return; 145 } 146 147 if ( 148 url.pathname.startsWith('/images/') || 149 url.pathname.startsWith('/icons/') || 150 url.pathname.startsWith('/onesheets/') 151 ) { 152 event.respondWith(networkFirst(request, DATA_CACHE)); 153 } 154});
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.