1const CACHE_NAME = 'kich-v5'; 2const IMAGE_CACHE_NAME = 'kich-images-v1'; 3const IMAGE_CACHE_MAX = 500; // Max cached images â ~250MB at ~500KB each 4 5self.addEventListener('install', event => { 6 // Pre-cache the app shell immediately 7 event.waitUntil( 8 caches.open(CACHE_NAME).then(cache => { 9 return cache.addAll([ 10 '/', 11 '/index.html', 12 '/manifest.json', 13 '/icon.svg', 14 '/icon-192.png', 15 '/icon-512.png' 16 ]); 17 }) 18 ); 19 self.skipWaiting(); 20}); 21 22self.addEventListener('activate', event => { 23 // Clean old caches (keep current app + image cache) 24 event.waitUntil( 25 caches.keys().then(keys => 26 Promise.all(keys.filter(k => k !== CACHE_NAME && k !== IMAGE_CACHE_NAME).map(k => caches.delete(k))) 27 ) 28 ); 29 self.clients.claim(); 30}); 31 32self.addEventListener('fetch', event => { 33 const url = new URL(event.request.url); 34 35 // Skip non-GET 36 if (event.request.method !== 'GET') return; 37 38 // Skip non-http(s) schemes (chrome-extension://, etc.) 39 if (!url.protocol.startsWith('http')) return; 40 41 // Skip API endpoints â always network 42 if (url.pathname.startsWith('/api/')) return; 43 44 // Skip admin page â always load fresh from network 45 if (url.pathname.startsWith('/admin')) return; 46 47 // Firebase Storage images â cache-first (images don't change once uploaded) 48 if (url.hostname.includes('firebasestorage.googleapis.com') || url.hostname.includes('firebasestorage.app')) { 49 event.respondWith( 50 caches.open(IMAGE_CACHE_NAME).then(cache => 51 cache.match(event.request).then(cached => { 52 if (cached) return cached; 53 return fetch(event.request).then(response => { 54 if (response.status === 200) { 55 const clone = response.clone(); 56 // Cache the image, then trim if over limit 57 cache.put(event.request, clone).then(() => { 58 cache.keys().then(keys => { 59 if (keys.length > IMAGE_CACHE_MAX) { 60 const toDelete = keys.length - IMAGE_CACHE_MAX; 61 for (let i = 0; i < toDelete; i++) { 62 cache.delete(keys[i]); 63 } 64 } 65 }); 66 }); 67 } 68 return response; 69 }).catch(() => { 70 // Offline and not cached â return transparent SVG placeholder 71 return new Response( 72 '<svg xmlns="http://www.w3.org/2000/svg" width="1" height="1"/>', 73 { headers: { 'Content-Type': 'image/svg+xml' } } 74 ); 75 }); 76 }) 77 ) 78 ); 79 return; 80 } 81 82 // Skip other Firebase and external APIs â always network 83 if (url.hostname.includes('firebaseio.com') || 84 url.hostname.includes('firebaseapp.com') || 85 url.hostname.includes('googleapis.com') || 86 url.hostname.includes('google.com') || 87 url.hostname.includes('fcm.googleapis.com') || 88 url.hostname.includes('anthropic.com') || 89 url.hostname.includes('qrserver.com')) { 90 return; 91 } 92 93 // Navigation requests (page loads) â network first, fall back to cached index.html 94 if (event.request.mode === 'navigate') { 95 event.respondWith( 96 fetch(event.request).then(response => { 97 const clone = response.clone(); 98 caches.open(CACHE_NAME).then(cache => { 99 try { cache.put(event.request, clone); } catch (e) {} 100 }); 101 return response; 102 }).catch(() => { 103 return caches.match('/index.html').then(cached => { 104 return cached || new Response('Offline â please connect to the internet and reload.', { 105 status: 503, 106 headers: { 'Content-Type': 'text/plain' } 107 }); 108 }); 109 }) 110 ); 111 return; 112 } 113 114 // All other requests â stale-while-revalidate 115 // Serve from cache immediately, update cache in background 116 event.respondWith( 117 caches.match(event.request).then(cached => { 118 const fetchPromise = fetch(event.request).then(response => { 119 if (response.status === 200) { 120 const clone = response.clone(); 121 caches.open(CACHE_NAME).then(cache => { 122 try { cache.put(event.request, clone); } catch (e) {} 123 }); 124 } 125 return response; 126 }).catch(() => cached || new Response('', { status: 408 })); 127 128 return cached || fetchPromise; 129 }) 130 ); 131}); 132 133// Handle notification clicks 134self.addEventListener('notificationclick', event => { 135 event.notification.close(); 136 event.waitUntil( 137 clients.matchAll({ type: 'window', includeUncontrolled: true }).then(clientList => { 138 for (const client of clientList) {
139 if (client.url.includes(self.location.origin) && 'focus' in client) { 140 return client.focus(); 141 } 142 } 143 return clients.openWindow('/'); 144 }) 145 ); 146});
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.