1/** 2 * Kaboodu service worker. 3 * 4 * 1. Makes the site installable (a fetch handler is required). 5 * 2. Caches what makes the closet open fast on a train: the app shell, 6 * the hashed build assets, web fonts, and the item cutouts, so a 7 * second visit on a weak connection paints from cache while the 8 * network catches up. 9 * 3. Shows push notifications ("Tomorrow: Sunday, slow") and opens the 10 * right day when tapped. 11 * 12 * API calls are never cached: the app's own store decides what is fresh, 13 * and live sync keeps it that way. 14 */ 15 16const VERSION = 'kaboodu-sw-v2'; 17const SHELL = `${VERSION}-shell`; 18const ASSETS = `${VERSION}-assets`; 19const IMAGES = `${VERSION}-images`; 20const FONTS = `${VERSION}-fonts`; 21const MAX_IMAGES = 400; 22 23self.addEventListener('install', (event) => { 24 event.waitUntil( 25 caches.open(SHELL).then((c) => c.addAll(['/', '/manifest.webmanifest']).catch(() => {})) 26 ); 27 self.skipWaiting(); 28}); 29 30self.addEventListener('activate', (event) => { 31 event.waitUntil((async () => { 32 const keys = await caches.keys(); 33 await Promise.all(keys.filter((k) => !k.startsWith(VERSION)).map((k) => caches.delete(k))); 34 await self.clients.claim(); 35 })()); 36}); 37 38function isApi(url) { return url.pathname.startsWith('/api/') || /\/api\//.test(url.href); } 39function isBuildAsset(url) { return url.origin === self.location.origin && (url.pathname.startsWith('/assets/') || url.pathname.startsWith('/fonts/')); } 40function isFont(url) { return /fonts\.(googleapis|gstatic)\.com$/.test(url.hostname); } 41function isItemImage(url) { return /\/storage\/v1\/object\/public\//.test(url.pathname) || /supabase\.co$/.test(url.hostname); } 42 43async function cacheFirst(cacheName, request) { 44 const cache = await caches.open(cacheName); 45 const hit = await cache.match(request); 46 if (hit) return hit; 47 const res = await fetch(request); 48 if (res.ok) cache.put(request, res.clone()); 49 return res; 50} 51 52async function staleWhileRevalidate(cacheName, request, max) { 53 const cache = await caches.open(cacheName); 54 const hit = await cache.match(request); 55 const refresh = fetch(request).then(async (res) => { 56 if (res.ok || res.type === 'opaque') { 57 await cache.put(request, res.clone()); 58 if (max) trim(cache, max); 59 } 60 return res; 61 }).catch(() => null); 62 return hit || (await refresh) || Response.error(); 63} 64 65async function trim(cache, max) { 66 const keys = await cache.keys(); 67 if (keys.length <= max) return; 68 await Promise.all(keys.slice(0, keys.length - max).map((k) => cache.delete(k))); 69} 70 71self.addEventListener('fetch', (event) => { 72 const { request } = event; 73 if (request.method !== 'GET') return; 74 const url = new URL(request.url); 75 if (isApi(url)) return; 76 77 if (request.mode === 'navigate') { 78 // Network first, so deploys land; the cached shell when offline. 79 event.respondWith((async () => { 80 try { 81 const res = await fetch(request); 82 if (res.ok) (await caches.open(SHELL)).put('/', res.clone()); 83 return res; 84 } catch { 85 return (await caches.match('/')) || Response.error(); 86 } 87 })()); 88 return; 89 } 90 if (isBuildAsset(url)) { event.respondWith(cacheFirst(ASSETS, request)); return; } 91 if (isFont(url)) { event.respondWith(staleWhileRevalidate(FONTS, request)); return; } 92 if (isItemImage(url) && !url.pathname.includes('/auth/')) { event.respondWith(staleWhileRevalidate(IMAGES, request, MAX_IMAGES)); } 93}); 94 95// ââ Push ââââââââââââââââââââââââââââââââââââââââââââââââââââââââââââââ 96 97self.addEventListener('push', (event) => { 98 let data = {}; 99 try { data = event.data ? event.data.json() : {}; } catch { data = { title: 'Kaboodu', body: event.data?.text() || '' }; } 100 const title = data.title || 'Kaboodu'; 101 const options = { 102 body: data.body || '', 103 icon: '/icon-192.png', 104 badge: '/icon-192.png', 105 tag: data.tag || undefined, 106 data: { url: data.url || '/calendar' } 107 }; 108 event.waitUntil(self.registration.showNotification(title, options)); 109}); 110 111self.addEventListener('notificationclick', (event) => { 112 event.notification.close();
113 const target = new URL(event.notification.data?.url || '/calendar', self.location.origin).href; 114 event.waitUntil((async () => { 115 const wins = await self.clients.matchAll({ type: 'window', includeUncontrolled: true }); 116 for (const w of wins) { 117 if (new URL(w.url).origin === self.location.origin && 'focus' in w) { 118 await w.focus(); 119 if ('navigate' in w) { try { await w.navigate(target); } catch { /* noop */ } } 120 return; 121 } 122 } 123 await self.clients.openWindow(target); 124 })()); 125});
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.