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.