/* * SkateSpots push service worker. * * Receives a Web Push payload from `push-service/` — which is CONTENT-FREE * ({ title, body, wrapId, url }) because the server never decrypts anything — * and shows an OS notification. Tapping it focuses/opens the app, where the real * message is fetched by wrapId and decrypted locally. * * It also acts as the tiny app-shell cache: hashed assets are cache-first, * navigations are network-first with an index fallback. No API, DM or model * request is ever stored; only same-origin static bytes. */ const SHELL_CACHE = 'skate-spots-shell-v2'; const SHELL_ASSETS = ['/', '/index.html']; self.addEventListener('install', (event) => { event.waitUntil( caches.open(SHELL_CACHE).then((cache) => cache.addAll(SHELL_ASSETS)).catch(() => undefined), ); self.skipWaiting(); }); self.addEventListener('activate', (event) => { event.waitUntil( caches.keys().then((keys) => Promise.all( keys .filter((key) => key.startsWith('skate-spots-shell-') && key !== SHELL_CACHE) .map((key) => caches.delete(key)), ), ).then(() => self.clients.claim()), ); }); self.addEventListener('fetch', (event) => { const request = event.request; if (request.method !== 'GET') return; const url = new URL(request.url); if (url.origin !== self.location.origin) return; if (url.pathname.startsWith('/api/') || url.pathname.startsWith('/.netlify/')) return; if (request.mode === 'navigate') { event.respondWith( fetch(request) .then((response) => { if (response.ok) { const copy = response.clone(); caches.open(SHELL_CACHE).then((cache) => cache.put('/index.html', copy)).catch(() => undefined); } return response; }) .catch(() => caches.match('/index.html').then((cached) => cached || Response.error())), ); return; } const isStatic = url.pathname.startsWith('/assets/') || url.pathname.startsWith('/images/') || url.pathname.startsWith('/flags/') || url.pathname.startsWith('/ort/') || /\.(?:js|mjs|css|woff2?|ttf|wasm|png|jpe?g|webp|svg|ico)$/i.test(url.pathname); if (!isStatic) return; event.respondWith( caches.match(request).then((cached) => { if (cached) return cached; return fetch(request).then((response) => { if (response.ok) { const copy = response.clone(); caches.open(SHELL_CACHE).then((cache) => cache.put(request, copy)).catch(() => undefined); } return response; }); }), ); }); self.addEventListener('push', (event) => { let data = {}; try { data = event.data ? event.data.json() : {}; } catch { /* non-JSON payload — fall back to defaults */ } const title = data.title || 'SkateSpots'; const body = data.body || 'You have a new notification'; event.waitUntil( self.registration.showNotification(title, { body, tag: data.wrapId || undefined, data: { url: data.url || '/' }, }), ); }); self.addEventListener('notificationclick', (event) => { event.notification.close(); const target = (event.notification.data && event.notification.data.url) || '/'; event.waitUntil( self.clients.matchAll({ type: 'window', includeUncontrolled: true }).then((list) => { for (const client of list) { if ('focus' in client) return client.focus(); } return self.clients.openWindow(target); }), ); });