PageSourceSearch

https://grigsi.com/sendi/sendi-sw.js

js grigsi.com collected 2026-09-24 14:46:03 UTC 6,023 bytes, 191 lines download raw bytes

1"use strict";
2
3/** Space scope SW — offline shell (2b) + Web Push (Phase 7). */
4const CACHE = "grigsi-space-pwa-78";
5
6const PRECACHE_URLS = [
7  "/sendi/sendi-shell-deps.min.js",
8  "/style.css",
9  "/js/sendi-secret.js",
10  "/js/connect-spacebook.js",
11  "/js/sendi-connect-local.js",
12  "/js/sendi-code-parse.js",
13  "/js/connect-deep-link.js",
14  "/js/connect-lan-hint.js",
15  "/js/connect-offline-status.js",
16  "/js/connect-cap-policy.js",
17  "/js/grigsi-sendi-media-lazy.js",
18  "/js/sendi-server-channel.js",
19  "/connect/connect-visual-identity.js",
20  "/connect/icons/connect-icon-192.png",
21];
22
23const OFFLINE_JS_PREFIXES = [
24  "/js/connect-",
25  "/js/sendi-secret.js",
26  "/js/sendi-connect-local.js",
27  "/js/sendi-code-parse.js",
28  "/js/grigsi-sendi-media-lazy.js",
29  "/js/sendi-server-channel.js",
30  "/connect/peerjs-",
31];
32
33self.addEventListener("install", (event) => {
34  event.waitUntil(
35    (async () => {
36      const cache = await caches.open(CACHE);
37      for (const url of PRECACHE_URLS) {
38        try {
39          await cache.add(url);
40        } catch (_) {}
41      }
42      self.skipWaiting();
43    })()
44  );
45});
46
47self.addEventListener("activate", (event) => {
48  event.waitUntil(
49    (async () => {
50      const keys = await caches.keys();
51      const stale = keys.filter((k) => k.startsWith("grigsi-space-pwa-") && k !== CACHE);
52      await Promise.all(stale.map((k) => caches.delete(k)));
53      self.clients.claim();
54    })()
55  );
56});
57
58function shouldHandlePath(pathname) {
59  if (pathname.startsWith("/SENDi!/")) return true;
60  if (pathname === "/style.css") return true;
61  // isNetworkFirstAsset() below special-cases sendi.js/sendi.css/etc., but this gate
62  // never let those paths through to it — they were never precached, runtime-cached,
63  // or given an offline fallback at all, despite the code clearly intending to.
64  if (isNetworkFirstAsset(pathname)) return true;
65  for (const p of OFFLINE_JS_PREFIXES) {
66    if (pathname === p || pathname.startsWith(p)) return true;
67  }
68  if (pathname.startsWith("/connect/connect-visual-identity")) return true;
69  return false;
70}
71
72function isNetworkFirstAsset(pathname) {
73  if (pathname === "/sendi/sendi.css" || pathname === "/sendi/sendi.js") return true;
74  if (pathname.startsWith("/sendi/sendi.css?") || pathname.startsWith("/sendi/sendi.js?")) return true;
75  if (pathname === "/sendi/sendi-compose-sanity.js" || pathname.startsWith("/sendi/sendi-compose-sanity.js?")) return true;
76  if (pathname === "/sendi/sendi-shell-deps.min.js" || pathname.startsWith("/sendi/sendi-shell-deps.min.js?")) return true;
77  if (pathname === "/js/sendi-secret.js" || pathname.startsWith("/js/sendi-secret.js?")) return true;
78  return false;
79}
80
81self.addEventListener("fetch", (event) => {
82  const req = event.request;
83  if (req.method !== "GET") return;
84  let url;
85  try {
86    url = new URL(req.url);
87  } catch (_) {
88    return;
89  }
90  if (url.origin !== self.location.origin) return;
91  if (url.pathname.startsWith("/api/")) return;
92  if (!shouldHandlePath(url.pathname)) return;
93
94  const accept = req.headers.get("accept") || "";
95  const isNav = req.mode === "navigate" || accept.includes("text/html");
96
97  // Never intercept SENDi! HTML — a cached shell kept ?space= in the address bar and froze Chrome.
98  if (isNav) return;
99
100  if (isNetworkFirstAsset(url.pathname)) {
101    event.respondWith(
102      fetch(req, { cache: "no-cache" })
103        .then((res) => {
104          if (res.ok && res.type !== "opaqueredirect") {
105            const copy = res.clone();
106            caches.open(CACHE).then((c) => c.put(req, copy));
107          }
108          return res;
109        })
110        .catch(() =>
111          caches.match(req, { ignoreSearch: false }).then((hit) => {
112            if (hit) return hit;
113            return new Response("", { status: 503, statusText: "Offline" });
114          })
115        )
116    );
117    return;
118  }
119
120  event.respondWith(
121    fetch(req)
122      .then((res) => {
123        if (res.ok && res.type !== "opaqueredirect") {
124          const copy = res.clone();
125          caches.open(CACHE).then((c) => c.put(req, copy));
126        }
127        return res;
128      })
129      .catch(() =>
130        caches.match(req, { ignoreSearch: false }).then((hit) => {
131          if (hit) return hit;
132          return new Response("", { status: 503, statusText: "Offline" });
133        })
134      )
135  );
136});
137
138self.addEventListener("push", (event) => {
139  let data = {};
140  try {
141    data = event.data ? event.data.json() : {};
142  } catch (_) {}
143  const title = data.title || "SENDi!";
144  const options = {
145    body: data.body || "Activity in your Space.",
146    data: data.data || {},
147    icon: "/connect/icons/connect-icon-192.png",
148  };
149  event.waitUntil(
150    (async () => {
151      await self.registration.showNotification(title, options);
152      const payload = data.data || {};
153      const clients = await self.clients.matchAll({ type: "window", includeUncontrolled: true });
154      clients.forEach((c) => {
155        try {
156          c.postMessage({
157            type: "grigsi_space_alert",
158            kind: payload.kind || payload.type || "ping",
159            space_code: payload.space_code || payload.spaceCode || "",
160            source: "push",
161            ts: Date.now(),
162          });
163        } catch (_) {}
164      });
165    })()
166  );
167});
168
169self.addEventListener("notificationclick", (event) => {
170  event.notification.close();
171  const d = (event.notification && event.notification.data) || {};
172  let url = d.url || "/SENDi!/";
173  if (d.space_code) {
174    const q = url.indexOf("?") >= 0 ? "&" : "?";
175    url = url + q + "sendi=" + encodeURIComponent(d.space_code);
176  }
177  if (!url) return;
178  event.waitUntil(
179    self.clients.matchAll({ type: "window", includeUncontrolled: true }).then((list) => {
180      const abs = new URL(url, self.location.origin).href;
181      for (const c of list) {
182        if ("focus" in c) {
183          return c.focus().then(() => {
184            if (c.navigate) return c.navigate(abs);
185          });
186        }
187      }
188      if (self.clients.openWindow) return self.clients.openWindow(abs);
189    })
190  );
191});

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.