PageSourceSearch

https://modeldiplomat.com/sw.js

js modeldiplomat.com collected 2026-10-02 16:44:50 UTC 5,889 bytes, 167 lines download raw bytes

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.