// Dumpster PWA Service Worker // Bump CACHE_VERSION on each deploy to force re-cache of hashed assets. const CACHE_VERSION = 'dumpster-v5'; const OFFLINE_URL = '/offline.html'; // Assets to pre-cache on install (shell files) const PRECACHE_URLS = [ '/', '/offline.html', '/favicon.png', '/icons/icon-192.png', '/icons/icon-512.png', ]; // Install: pre-cache shell and skip waiting self.addEventListener('install', (event) => { event.waitUntil( caches.open(CACHE_VERSION).then((cache) => cache.addAll(PRECACHE_URLS)).then(() => self.skipWaiting()) ); }); // Activate: clean old caches and claim clients self.addEventListener('activate', (event) => { event.waitUntil( caches .keys() .then((names) => Promise.all(names.filter((n) => n !== CACHE_VERSION).map((n) => caches.delete(n)))) .then(() => self.clients.claim()) ); }); // Fetch handler self.addEventListener('fetch', (event) => { if (event.request.method !== 'GET') return; const url = new URL(event.request.url); // Skip API, WebSocket, and cross-origin requests if (url.pathname.startsWith('/api/') || url.pathname.startsWith('/ws')) return; if (url.origin !== self.location.origin) return; // Navigation requests: network-first, offline fallback if (event.request.mode === 'navigate') { event.respondWith( fetch(event.request) .then((response) => { const clone = response.clone(); caches.open(CACHE_VERSION).then((c) => c.put(event.request, clone)); return response; }) .catch(() => caches.match(event.request).then((cached) => cached || caches.match(OFFLINE_URL)) ) ); return; } // Static assets (JS, CSS, images, fonts): stale-while-revalidate // Serve cached immediately, fetch fresh in background event.respondWith( caches.match(event.request).then((cached) => { const fetchPromise = fetch(event.request) .then((response) => { if (response.ok) { const clone = response.clone(); caches.open(CACHE_VERSION).then((c) => c.put(event.request, clone)); } return response; }) .catch(() => cached); return cached || fetchPromise; }) ); }); // Push notifications self.addEventListener('push', (event) => { let data = { title: 'Dumpster', body: '', url: '/', tag: 'dumpster-notification' }; try { if (event.data) { data = { ...data, ...event.data.json() }; } } catch (e) { data.body = event.data ? event.data.text() : ''; } event.waitUntil( self.registration.showNotification(data.title, { body: data.body, icon: '/icons/icon-192.png', badge: '/icons/icon-192.png', vibrate: [100, 50, 100], tag: data.tag, renotify: true, data: { url: data.url }, }) ); }); // Notification click: focus or open window self.addEventListener('notificationclick', (event) => { event.notification.close(); event.waitUntil( clients.matchAll({ type: 'window', includeUncontrolled: true }).then((list) => { for (const client of list) { if (client.url.includes(event.notification.data.url) && 'focus' in client) { return client.focus(); } } return clients.openWindow(event.notification.data.url); }) ); });