PageSourceSearch

https://recipepantry.app/sw.js

js recipepantry.app collected 2026-10-04 14:31:56 UTC 4,788 bytes, 154 lines download raw bytes

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.