1/* 2 * Model Diplomat service worker â offline resilience for flaky mobile networks. 3 * 4 * Strategy (deliberately conservative so we never serve stale authed content): 5 * - Navigations: network-first â cache â branded /offline page. 6 * - Static assets (_next/static, fonts, images): stale-while-revalidate. 7 * - API / streaming / analytics traffic: never intercepted. 8 * 9 * Bump VERSION to invalidate old caches on deploy. 10 */ 11const VERSION = "v1"; 12const STATIC_CACHE = `md-static-${VERSION}`; 13const OFFLINE_URL = "/offline"; 14 15// A real request the SW never intercepts (see the /api/ guard in `fetch`), so it 16// reflects genuine reachability. We probe this instead of trusting 17// `self.navigator.onLine`, which can stay stuck at `false` after a missed 18// `online` event and strand the user on the branded offline page. 19const PROBE_URL = "/api/ping"; 20const PROBE_TIMEOUT_MS = 4000; 21 22// Actively confirm connectivity. Resolves true only when the network genuinely 23// answers; any failure (including the timeout) resolves false. 24async function probeOnline() { 25 const controller = new AbortController(); 26 const timeout = setTimeout(() => controller.abort(), PROBE_TIMEOUT_MS); 27 try { 28 const res = await fetch(PROBE_URL, { 29 method: "HEAD", 30 cache: "no-store", 31 signal: controller.signal, 32 }); 33 return res.ok || res.status === 204; 34 } catch { 35 return false; 36 } finally { 37 clearTimeout(timeout); 38 } 39} 40 41const PRECACHE = [OFFLINE_URL, "/favicon.svg", "/web-app-manifest-192x192.png"]; 42 43self.addEventListener("install", (event) => { 44 event.waitUntil( 45 caches 46 .open(STATIC_CACHE) 47 .then((cache) => cache.addAll(PRECACHE)) 48 .then(() => self.skipWaiting()), 49 ); 50}); 51 52self.addEventListener("activate", (event) => { 53 event.waitUntil( 54 caches 55 .keys() 56 .then((keys) => 57 Promise.all(keys.filter((key) => key !== STATIC_CACHE).map((key) => caches.delete(key))), 58 ) 59 .then(() => self.clients.claim()), 60 ); 61}); 62 63// Notifications shown via `registration.showNotification` (the Android Chrome 64// path in run-watcher.tsx) never reach the page's `onclick`, so the click is 65// handled here: focus a tab already on the thread, else open one. 66self.addEventListener("notificationclick", (event) => { 67 event.notification.close(); 68 const url = new URL(event.notification.data?.url || "/", self.location.origin).href; 69 event.waitUntil( 70 self.clients.matchAll({ type: "window", includeUncontrolled: true }).then((windows) => { 71 const open = windows.find((client) => client.url === url); 72 return open ? open.focus() : self.clients.openWindow(url); 73 }), 74 ); 75}); 76 77function isCacheableStatic(url) { 78 return ( 79 url.pathname.startsWith("/_next/static/") || 80 url.pathname.startsWith("/fonts/") || 81 url.pathname.startsWith("/brands/") || 82 /\.(?:css|js|woff2?|png|jpe?g|webp|svg|ico)$/.test(url.pathname) 83 ); 84} 85 86// Network-first navigation with a single retry and an offline-only fallback. 87async function handleNavigation(request) { 88 try { 89 return await fetch(request); 90 } catch (firstError) { 91 // Actively confirm reachability rather than trusting `navigator.onLine` 92 // (which can stay stuck at `false` after a missed `online` event). When the 93 // probe succeeds the network is genuinely up, so this was a transient 94 // failure (cold SW, dropped request, server hiccup, or an in-flight 95 // redirect) â retry once before considering any fallback. 96 const online = await probeOnline(); 97 if (online) { 98 try { 99 return await fetch(request); 100 } catch { 101 // fall through to cache / offline handling below 102 } 103 } 104 105 // Prefer a cached copy of the exact page if we have one. 106 const cached = await caches.match(request); 107 if (cached) return cached; 108 109 // Only show the branded offline page when an active probe confirms we are 110 // offline. Otherwise, propagate the error so a transient single failure no 111 // longer renders the full-screen blocker. 112 if (!online) { 113 const offline = await caches.match(OFFLINE_URL); 114 if (offline) return offline; 115 } 116 117 throw firstError; 118 } 119} 120 121self.addEventListener("fetch", (event) => { 122 const { request } = event; 123 if (request.method !== "GET") return; 124 125 const url = new URL(request.url); 126 if (url.origin !== self.location.origin) return; 127 128 // Never touch dynamic / streaming / analytics traffic â these must always hit 129 // the network (and the SSE search stream must never be buffered by a cache). 130 if ( 131 url.pathname.startsWith("/api/") || 132 url.pathname.startsWith("/ingest") || 133 url.pathname.startsWith("/monitoring") 134 ) { 135 return; 136 } 137 138 // Navigations: keep content fresh (network-first). A single transient failure 139 // (cold SW activation, a dropped request, a server hiccup, an in-flight 140 // client-side redirect) must NOT render the branded full-screen offline 141 // blocker â that produces false positives on the highest-traffic surfaces. 142 // So we retry once, fall back to any cached copy, and only serve the offline 143 // page when the browser actually reports itself offline. Otherwise we let the 144 // genuine network error propagate so the browser/app handles it normally. 145 if (request.mode === "navigate") { 146 event.respondWith(handleNavigation(request)); 147 return; 148 } 149 150 // Static assets: serve from cache immediately, refresh in the background. 151 if (isCacheableStatic(url)) { 152 event.respondWith( 153 caches.open(STATIC_CACHE).then(async (cache) => { 154 const cached = await cache.match(request); 155 const network = fetch(request) 156 .then((response) => { 157 if (response && response.status === 200) { 158 cache.put(request, response.clone()); 159 } 160 return response; 161 }) 162 .catch(() => cached); 163 return cached || network; 164 }), 165 ); 166 } 167});
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.