1// Generated by app/sw.js/route.ts â do not edit a copy of this by hand. 2const VERSION = "83f6c25d1354"; 3const STATIC = 'bmb-sw-static-' + VERSION; 4const PAGES = 'bmb-sw-pages-' + VERSION; 5const MINE = [STATIC, PAGES]; 6 7// The last resort, and it should be unreachable: install precaches '/'. Kept 8// because the alternative at this point is rejecting, and a navigation that 9// rejects is the error screen this whole branch exists to avoid. 10const OFFLINE_HTML = '<!doctype html><meta charset=utf-8>' 11 + '<meta name=viewport content="width=device-width,initial-scale=1">' 12 + '<title>Offline</title>' 13 + '<body style="margin:0;display:grid;place-items:center;height:100vh;' 14 + 'font:16px system-ui;background:#0b0b0d;color:#f4efe6">' 15 + '<p>No connection, and this page was not saved.</p>'; 16 17self.addEventListener('install', (event) => { 18 // TWO documents are precached, and no more. 19 // 20 // Everything else still fills from real traffic. This exists because the 21 // FIRST load of a page is fetched BEFORE this worker controls it, so it never 22 // passes through the fetch handler and never enters PAGES. A visitor who 23 // installs the app and then loses signal without loading a second time had no 24 // shell at all â measured on an iPhone, and it did not degrade, it produced 25 // "FetchEvent.respondWith received an error: TypeError: Load failed". 26 // 27 // It does not weaken network-first. '/' is still fetched fresh on every load 28 // that has a connection; this copy is only ever reached when the fetch 29 // rejects. And it cannot go stale across a deploy: PAGES carries the build id, 30 // so a new worker precaches its own '/' and activate() deletes the old one. 31 event.waitUntil((async () => { 32 // '/downloads' is here for its own reason, not for symmetry: it is the one 33 // route whose entire PURPOSE is to work without a network, and a dock tab 34 // that does nothing is the worst way to find that out. A client-side route 35 // change fetches an RSC payload rather than a document, so when that fetch 36 // fails the router falls back to a real navigation â and that navigation 37 // has to land on the route's OWN document. Without this it landed on the 38 // shell, which is the home page's HTML under a /downloads URL. 39 // 40 // Reported from an iPhone: the tab did not open in airplane mode and was 41 // fine on wifi. It does not reproduce in headless Chrome, which prefetches 42 // every dock <Link> on the first load and so always has what the tap needs. 43 const staticCache = await caches.open(STATIC); 44 for (const path of ['/', '/downloads']) { 45 try { 46 const res = await fetch(path); 47 if (!res.ok) continue; 48 const html = await res.clone().text(); 49 const cache = await caches.open(PAGES); 50 await cache.put(path, res); 51 // A precached page whose script deps are not cached breaks on the 52 // next deploy: activate deletes the old STATIC cache while the new 53 // page references new hashes. Collect same-origin /_next/static/ 54 // URLs and cache them now. 55 const re = /(?:src|href)=["']([^"']*[/]_next[/]static[/][^"']+)["']/g; 56 const deps = [...html.matchAll(re)].map(m => m[1]); 57 await Promise.all(deps.map(u => staticCache.add(u).catch(() => {}))); 58 } catch (e) { 59 // An install must never fail on this. Without it the app simply has no 60 // offline shell until the next load, which is where this started. 61 } 62 } 63 await self.skipWaiting(); 64 })()); 65}); 66 67self.addEventListener('activate', (event) => { 68 event.waitUntil((async () => { 69 // Delete every cache this worker owns from an older build. The prefix test 70 // is deliberately narrow: the DOWNLOADS buckets (bmb-downloads-*) are the 71 // user's own files and must never be touched here. 72 const names = await caches.keys(); 73 await Promise.all( 74 names 75 .filter((n) => n.startsWith('bmb-sw-') && !MINE.includes(n)) 76 .map((n) => caches.delete(n)), 77 ); 78 await self.clients.claim(); 79 })()); 80}); 81 82/** Four things never enter a cache, and each would be a real bug. */ 83function cacheable(request, url) { 84 // A cached POST is not a thing. 85 if (request.method !== 'GET') return false;
86 // Enclosure audio, relay traffic, third-party artwork. Not ours to hold, and 87 // downloads have their own buckets with their own lifetime. 88 if (url.origin !== self.location.origin) return false; 89 // A cached /api/feed serves a stale episode list, and /api/live-status would 90 // report a finished show as live. 91 if (url.pathname.startsWith('/api/')) return false; 92 return true; 93} 94 95async function putIfOk(cacheName, request, response) { 96 // A cached 404 or 502 outlives the outage that produced it. An opaque or 97 // cross-origin response cannot be read back usefully either. 98 if (!response || !response.ok || response.type !== 'basic') return; 99 if ((response.headers.get('cache-control') || '').includes('no-store')) return; 100 // A 206 is ok and Cache.put REJECTS it â and every same-origin media element 101 // asks with a Range header, the boost sound included. A partial body is not 102 // the resource anyway. 103 if (response.status === 206 || request.headers.has('range')) return; 104 // THE CACHE WRITE MUST NEVER FAIL THE RESPONSE. The caller awaits this before 105 // returning a response the network already delivered, so a throw here (a full 106 // quota once downloads have used it, a private-mode refusal) turned a good 107 // answer into a rejection: a chunk failed to load while online. 108 try { 109 const cache = await caches.open(cacheName); 110 await cache.put(request, response.clone()); 111 } catch (e) { 112 // Not caching is always a safe answer; failing the fetch is not. 113 } 114} 115 116self.addEventListener('fetch', (event) => { 117 const request = event.request; 118 const url = new URL(request.url); 119 if (!cacheable(request, url)) return; // straight to the network, as before 120 121 // /_next/static/* is CONTENT-HASHED, so a given URL's bytes never change and 122 // a cached copy cannot go stale. Cache-first here is what makes the rule 123 // below work offline: the last document's chunks are still present. 124 if (url.pathname.startsWith('/_next/static/')) { 125 event.respondWith((async () => { 126 const hit = await caches.match(request); 127 if (hit) return hit; 128 const res = await fetch(request); 129 await putIfOk(STATIC, request, res); 130 return res; 131 })()); 132 return; 133 } 134 135 // EVERYTHING ELSE IS NETWORK-FIRST, and the cache is used ONLY when the fetch 136 // rejects. A reader with a connection is therefore never served a stale 137 // document, so a stale document can never reference dead chunk hashes while 138 // online â which is the failure the old public/sw.js comment warned about. 139 event.respondWith((async () => { 140 try { 141 const res = await fetch(request); 142 await putIfOk(PAGES, request, res); 143 return res; 144 } catch (err) { 145 const hit = await caches.match(request); 146 if (hit) return hit; 147 148 // A NAVIGATION MUST NEVER REJECT HERE. Rejecting inside respondWith does 149 // not fall back to the browser's own offline page â it replaces it with 150 // "FetchEvent.respondWith received an error: TypeError: Load failed", 151 // which is a worse screen than having no service worker at all. So a 152 // navigation that missed its exact URL falls back to the shell: the app 153 // reads its own deep-link parameters off window.location on mount, so 154 // '/?podcast=â¦' served the shell still lands where the reader meant. 155 if (request.mode === 'navigate') { 156 const shell = await caches.match('/'); 157 if (shell) return shell; 158 return new Response(OFFLINE_HTML, { 159 status: 503, 160 headers: { 'Content-Type': 'text/html; charset=utf-8' }, 161 }); 162 } 163 164 // A SUBRESOURCE is the opposite: rejecting is exactly what would happen 165 // with no worker installed, and it is what the app's own catch blocks are 166 // written against. Synthesising a Response here would turn a network 167 // failure into a non-ok ANSWER, and lib/downloads/downloads-cache.ts's 168 // fetchDoc reads the difference â it falls back to a downloaded copy only 169 // on a rejection. 170 throw err; 171 } 172 })()); 173});
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.