1// Inboxero â Service Worker 2// Cache strategy: cache-first for GET assets, network-only for Blazor and API 3// Push: shows notification when app is in background, updates badge via Badging API 4 5const CACHE_NAME = 'app-shell-v2'; 6 7// ââ install ââââââââââââââââââââââââââââââââââââââââââââââââââââââââââââââââââ 8// Skip waiting so the new SW activates immediately on update. 9// No pre-caching: Blazor fingerprints asset URLs at build time (e.g. /app.css?v=abc123) 10// so bare paths would never match cached responses. Cache is populated lazily on first load. 11self.addEventListener('install', (event) => { 12 self.skipWaiting(); 13}); 14 15// ââ activate ââââââââââââââââââââââââââââââââââââââââââââââââââââââââââââââââââ 16// Delete any caches from old SW versions, then claim all open clients. 17self.addEventListener('activate', (event) => { 18 event.waitUntil( 19 caches.keys().then((names) => 20 Promise.all( 21 names 22 .filter((name) => name !== CACHE_NAME) 23 .map((name) => caches.delete(name)) 24 ) 25 ).then(() => self.clients.claim()) 26 ); 27}); 28 29// ââ fetch ââââââââââââââââââââââââââââââââââââââââââââââââââââââââââââââââââââââ 30// Pass through: non-GET, navigation (HTML pages), /_blazor/*, /api/* 31// Cache-first: static assets only (JS, CSS, images, fonts, manifests) 32self.addEventListener('fetch', (event) => { 33 const url = new URL(event.request.url); 34 const isGet = event.request.method === 'GET'; 35 const isNavigation = event.request.mode === 'navigate'; // HTML document requests 36 const isBlazor = url.pathname.startsWith('/_blazor'); 37 const isApi = url.pathname.startsWith('/api'); 38 39 if (!isGet || isNavigation || isBlazor || isApi) { 40 // Network-only passthrough â never cache HTML pages or Blazor/API traffic 41 return; 42 } 43 44 event.respondWith( 45 caches.open(CACHE_NAME).then(async (cache) => { 46 const cached = await cache.match(event.request); 47 if (cached) return cached; 48 49 const response = await fetch(event.request); 50 if (response && response.status === 200) { 51 cache.put(event.request, response.clone()); 52 } 53 return response; 54 }).catch(() => { 55 // Offline fallback: return cached response if available, otherwise propagate 56 return caches.match(event.request); 57 }) 58 ); 59}); 60 61// ââ push ââââââââââââââââââââââââââââââââââââââââââââââââââââââââââââââââââââââ 62// Parses the push payload and shows a notification unless a focused window is already open. 63// Always updates the app badge regardless of whether the notification is shown.
64self.addEventListener('push', (event) => { 65 event.waitUntil((async () => { 66 let data = {}; 67 try { 68 data = event.data ? event.data.json() : {}; 69 } catch { 70 data = {}; 71 } 72 73 const senderName = data.senderName || ''; 74 const senderEmail = data.senderEmail || ''; 75 const subject = data.subject || '(No subject)'; 76 const emailId = data.emailId || ''; 77 const attentionQueueCount = data.attentionQueueCount || 0; 78 const deepLink = data.deepLink || '/dashboard'; 79 80 // Update app icon badge (Badging API â W3C, available on self.navigator inside SW) 81 if (attentionQueueCount > 0) { 82 await self.navigator?.setAppBadge?.(attentionQueueCount); 83 } else { 84 await self.navigator?.clearAppBadge?.(); 85 } 86 87 // Check if there is already a focused window â if so, skip the notification 88 // so the user isn't double-notified while actively using the app. 89 const clients = await self.clients.matchAll({ type: 'window', includeUncontrolled: true }); 90 const hasFocusedClient = clients.some((c) => c.focused); 91 if (hasFocusedClient) return; 92 93 await self.registration.showNotification(senderName || senderEmail, { 94 body: subject, 95 icon: '/icons/icon-192.png', 96 badge: '/icons/icon-192.png', 97 tag: 'email-' + emailId, 98 data: { deepLink } 99 }); 100 })()); 101}); 102 103// ââ notificationclick ââââââââââââââââââââââââââââââââââââââââââââââââââââââââââ 104// Focus an existing window and navigate it to the deepLink, or open a new tab. 105self.addEventListener('notificationclick', (event) => { 106 event.notification.close(); 107 const deepLink = event.notification.data?.deepLink ?? '/dashboard'; 108 109 event.waitUntil( 110 self.clients.matchAll({ type: 'window' }).then((clients) => { 111 for (const client of clients) { 112 if ('focus' in client) { 113 client.focus(); 114 client.navigate(deepLink); 115 return; 116 } 117 } 118 // No existing window â open a new one 119 if (self.clients.openWindow) { 120 return self.clients.openWindow(deepLink); 121 } 122 }) 123 ); 124});
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.