const CACHE_PREFIX = "who-need-help-static-" const CACHE = `${CACHE_PREFIX}v5` const OFFLINE_URL = "/offline.html" const SHELL = [ OFFLINE_URL, "/manifest.webmanifest", "/assets/css/app.css", "/assets/js/app.js", "/images/logo.svg", "/images/pwa-192.png", "/images/pwa-512.png", "/images/pwa-maskable-512.png" ] self.addEventListener("install", event => { event.waitUntil( caches .open(CACHE) .then(cache => cache.addAll(SHELL)) .then(() => self.skipWaiting()) ) }) self.addEventListener("activate", event => { event.waitUntil( caches .keys() .then(keys => Promise.all( keys .filter(key => key.startsWith(CACHE_PREFIX) && key !== CACHE) .map(key => caches.delete(key)) ) ) .then(() => self.clients.claim()) ) }) self.addEventListener("fetch", event => { const url = new URL(event.request.url) const sameOrigin = url.origin === self.location.origin const isStatic = sameOrigin && (url.pathname.startsWith("/assets/") || url.pathname.startsWith("/images/")) if (event.request.method !== "GET" || !sameOrigin) return if (event.request.mode === "navigate") { event.respondWith( fetch(event.request).catch(async () => { const fallback = await caches.match(OFFLINE_URL) return fallback || Response.error() }) ) return } if (isStatic) { event.respondWith(cacheFirst(event)) } }) self.addEventListener("push", event => { let payload = {} try { payload = event.data?.json() || {} } catch (_error) { payload = {title: "Who Need Help", body: "Open the app to view an update."} } const title = String(payload.title || "Who Need Help").slice(0, 120) const body = String(payload.body || "Open the app to view an update.").slice(0, 240) const path = validPath(payload.path) ? payload.path : "/notifications" event.waitUntil(self.registration.showNotification(title, { body, icon: "/images/pwa-192.png", badge: "/images/favicon-48.png", tag: String(payload.tag || "who-need-help-update").slice(0, 64), data: {path} })) }) self.addEventListener("notificationclick", event => { event.notification.close() const path = validPath(event.notification.data?.path) ? event.notification.data.path : "/notifications" event.waitUntil((async () => { const target = new URL(path, self.location.origin).href const windows = await self.clients.matchAll({type: "window", includeUncontrolled: true}) const existing = windows.find(client => new URL(client.url).origin === self.location.origin) if (existing) { await existing.navigate(target) return existing.focus() } return self.clients.openWindow(target) })()) }) function validPath(value) { return typeof value === "string" && /^\/(?!\/)[A-Za-z0-9_/?=&.#%-]*$/.test(value) } async function cacheFirst(event) { const cached = await caches.match(event.request) if (cached) return cached const response = await fetch(event.request) if (response.ok) { const copy = response.clone() event.waitUntil(caches.open(CACHE).then(cache => cache.put(event.request, copy))) } return response }