PageSourceSearch

https://oneclickclaw.io/sw.js

js oneclickclaw.io collected 2026-10-04 08:01:31 UTC 7,706 bytes, 213 lines download raw bytes

1// Maintenance-page service worker.
2//
3// History:
4//   v1-v3: Cache /offline-maintenance.html and serve it as a 503 fallback
5//          when the backend is unreachable or returns a non-HTML 5xx.
6//
7//   v4 (Luigi 2026-05-14): Self-recovery. After yesterday's deploy outage,
8//          some browsers held onto the SW state from when the backend was
9//          looping on a failed Alembic migration. The backend recovered
10//          but the SW kept presenting the cached maintenance page in those
11//          tabs (cached page + stale tab on /maintenance URL = persistent
12//          "stuck" feeling even though prod is healthy). Two changes:
13//            1) Bump CACHE_NAME so the activate handler purges every old
14//               cache the moment this SW takes over.
15//            2) Whenever we DO serve the cached fallback, fire a background
16//               probe to /healthz; on a 200 response, unregister this SW
17//               so the next reload bypasses it entirely. Recovery is then
18//               at most one extra reload away, with no manual DevTools
19//               steps required from the customer.
20var CACHE_NAME = "occ-maintenance-v4";
21var MAINTENANCE_ASSETS = [
22  "/offline-maintenance.html",
23  "/logos/logo-wide-light.png",
24  "/images/mascot-thumbsup.webp",
25];
26
27// Self-recovery probe state. Module-scoped so we don't spam /healthz on
28// every cached response within the same SW lifetime.
29var recoveryProbeInflight = false;
30var recoveryProbeLastAt = 0;
31var RECOVERY_PROBE_MIN_INTERVAL_MS = 15000;
32
33self.addEventListener("install", function (event) {
34  event.waitUntil(
35    caches.open(CACHE_NAME).then(function (cache) {
36      return Promise.allSettled(
37        MAINTENANCE_ASSETS.map(function (url) {
38          return cache.add(url);
39        })
40      );
41    })
42  );
43  self.skipWaiting();
44});
45
46self.addEventListener("activate", function (event) {
47  event.waitUntil(
48    caches.keys().then(function (keys) {
49      return Promise.all(
50        keys
51          .filter(function (k) {
52            return k !== CACHE_NAME;
53          })
54          .map(function (k) {
55            return caches.delete(k);
56          })
57      );
58    })
59  );
60  self.clients.claim();
61});
62
63// Background self-recovery: when we just served a cached 503, probe the
64// real backend. If it's healthy, unregister so the customer's next
65// navigation bypasses the SW entirely and gets the real page. Bounded
66// by RECOVERY_PROBE_MIN_INTERVAL_MS so a flurry of cached responses
67// only triggers one network probe per window. Returns a promise that
68// the caller MUST attach to event.waitUntil() so the worker is kept
69// alive until recovery completes -- otherwise the browser is free to
70// terminate the SW between serving the cached response and finishing
71// the probe/unregister, which is exactly the "still stuck" scenario
72// we are trying to eliminate.
73function maybeSelfRecover() {
74  var now = Date.now();
75  if (recoveryProbeInflight) return Promise.resolve();
76  if (now - recoveryProbeLastAt < RECOVERY_PROBE_MIN_INTERVAL_MS) {
77    return Promise.resolve();
78  }
79  recoveryProbeInflight = true;
80  recoveryProbeLastAt = now;
81  return fetch("/healthz", { cache: "no-store", credentials: "omit" })
82    .then(function (res) {
83      if (!res || !res.ok) return null;
84      return self.registration.unregister().then(function () {
85        // Force any open clients to refresh so they pick up the real
86        // page instead of staying on the cached 503. Clients that are
87        // currently on /maintenance are special-cased: reloading them
88        // in place would just land them back on /maintenance (the
89        // backend doesn't redirect away from that route on its own),
90        // so we send them to "/" instead, which the now-healthy
91        // middleware will resolve to the real destination.
92        return self.clients
93          .matchAll({ type: "window" })
94          .then(function (cs) {
95            return Promise.allSettled(
96              cs.map(function (c) {
97                try {
98                  var target = c.url;
99                  try {
100                    var u = new URL(c.url);
101                    if (u.pathname === "/maintenance") {
102                      u.pathname = "/";
103                      u.search = "";
104                      target = u.toString();
105                    }
106                  } catch (_pe) {
107                    // Malformed client URL; fall back to in-place reload.
108                  }
109                  return c.navigate(target);
110                } catch (_e) {
111                  // Cross-origin or detached client; ignore.
112                  return Promise.resolve();
113                }
114              })
115            );
116          });
117      });
118    })
119    .catch(function () {
120      // Backend still down or probe blocked -- leave the SW in place.
121    })
122    .then(function () {
123      recoveryProbeInflight = false;
124    });
125}
126
127function serve503(cached) {
128  if (!cached) {
129    return new Response(
130      "Service temporarily unavailable. Please try again shortly.",
131      { status: 503, headers: { "Content-Type": "text/plain" } }
132    );
133  }
134  return new Response(cached.body, {
135    status: 503,
136    statusText: "Service Unavailable",
137    headers: {
138      "Content-Type": "text/html; charset=utf-8",
139      "Retry-After": "30",
140      "Cache-Control": "no-store",
141    },
142  });
143}
144
145// Admin/login routes are NEVER intercepted (Luigi 2026-05-14). The
146// Next.js middleware already bypasses /oc-panel-9f4e and /login from
147// auto-maintenance so the operator can always sign in to disable
148// maintenance mode -- the SW must mirror that bypass, otherwise during
149// a full outage (frontend AND backend down at the same time) the cached
150// maintenance page would lock the admin out of the very recovery path.
151function isAdminBypass(pathname) {
152  return (
153    pathname === "/login" ||
154    pathname.indexOf("/oc-panel-9f4e") === 0
155  );
156}
157
158self.addEventListener("fetch", function (event) {
159  if (event.request.mode === "navigate") {
160    try {
161      var navUrl = new URL(event.request.url);
162      if (isAdminBypass(navUrl.pathname)) {
163        // Hand straight to the network; no cached fallback even if the
164        // backend is unreachable. Better to show the browser's native
165        // "site can't be reached" than to lock the admin out.
166        return;
167      }
168    } catch (_e) {
169      // Malformed URL -- fall through to default handling below.
170    }
171    event.respondWith(
172      fetch(event.request)
173        .then(function (response) {
174          if (response.status >= 500) {
175            var ct = response.headers.get("content-type") || "";
176            if (!ct.includes("text/html")) {
177              // Served the cached fallback -- kick off self-recovery and
178              // keep the SW alive until it finishes via waitUntil().
179              try {
180                event.waitUntil(maybeSelfRecover());
181              } catch (_e) {
182                // Defensive: never let recovery wiring affect the response.
183              }
184              return caches
185                .match("/offline-maintenance.html")
186                .then(serve503);
187            }
188          }
189          return response;
190        })
191        .catch(function () {
192          try {
193            event.waitUntil(maybeSelfRecover());
194          } catch (_e) {
195            // Defensive: never let recovery wiring affect the response.
196          }
197          return caches
198            .match("/offline-maintenance.html")
199            .then(serve503);
200        })
201    );
202    return;
203  }
204
205  var url = new URL(event.request.url);
206  if (MAINTENANCE_ASSETS.indexOf(url.pathname) !== -1) {
207    event.respondWith(
208      fetch(event.request).catch(function () {
209        return caches.match(event.request);
210      })
211    );
212  }
213});

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.