PageSourceSearch

https://www.isthmian.co.uk/sw.js

js isthmian.co.uk collected 2026-10-02 11:40:51 UTC 3,398 bytes, 86 lines download raw bytes

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.