1// Service Worker for Isthmian Football League 2// Minimal, cache-free worker. Only purpose: keep the site an installable PWA. 3// Caches NOTHING â the network + content-hashed immutable assets + no-store HTML 4// are the single source of freshness. It deletes any caches a previous worker 5// created on activate, so nothing it once stored can pin stale code in a browser. 6const SW_VERSION = 'v7-push'; 7 8self.addEventListener('install', () => self.skipWaiting()); 9 10self.addEventListener('activate', (event) => { 11 event.waitUntil( 12 (async () => { 13 // One-time heal: purge every cache any previous worker created on this origin. 14 const names = await caches.keys(); 15 await Promise.all(names.map((name) => caches.delete(name))); 16 await self.clients.claim(); 17 })() 18 ); 19}); 20 21// Empty fetch handler â present only so the browser treats the app as installable. 22// It does not intercept requests, so every request uses the default network path 23// (and the normal HTTP/CDN cache, which is correct for content-hashed assets). 24self.addEventListener('fetch', () => {}); 25 26// Goal alert push notifications. The payload is parsed defensively â a push with no 27// body, or a body that isn't JSON, must not throw inside the event handler and drop 28// the notification silently uncaught. 29self.addEventListener('push', (event) => { 30 let payload = {}; 31 if (event.data) { 32 try { 33 payload = event.data.json(); 34 } catch (err) { 35 payload = {}; 36 } 37 } 38 39 const title = payload.title || "Isthmian Football League"; 40 const options = { 41 body: payload.body || '', 42 icon: '/pwa-192x192.png', 43 badge: '/pwa-192x192.png', 44 data: { url: payload.url }, 45 }; 46 // renotify requires a non-empty tag (the Notifications spec throws otherwise), so it 47 // is only set alongside one rather than unconditionally. 48 if (payload.tag) { 49 options.tag = payload.tag; 50 options.renotify = true; 51 } 52 53 event.waitUntil(self.registration.showNotification(title, options)); 54}); 55 56// Focus an already-open same-origin tab (navigating it to the goal's match, when the 57// browser supports WindowClient#navigate) rather than piling up a new tab per goal; 58// falls back to opening one when none is open. 59self.addEventListener('notificationclick', (event) => { 60 event.notification.close(); 61 // Defense in depth: only ever navigate/open a same-origin relative path. Only the VAPID key 62 // holder can send a payload today and the server always sends '/match/<id>', but a leaked key 63 // (or a future caller putting an absolute URL in it) must not turn a notification into an 64 // open redirect or phishing link. 65 const rawUrl = event.notification.data && event.notification.data.url; 66 const url = typeof rawUrl === 'string' && rawUrl.startsWith('/') && !rawUrl.startsWith('//') ? rawUrl : '/'; 67 68 event.waitUntil( 69 (async () => { 70 const windowClients = await self.clients.matchAll({ type: 'window', includeUncontrolled: true }); 71 const existing = windowClients.find((client) => 'focus' in client); 72 if (existing) { 73 await existing.focus(); 74 if ('navigate' in existing) { 75 try { 76 await existing.navigate(url); 77 } catch (err) { 78 // Cross-origin or unsupported navigate â the focused tab stays where it was. 79 } 80 } 81 return; 82 } 83 await self.clients.openWindow(url); 84 })() 85 ); 86});
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.