PageSourceSearch

https://inboxero.ai/sw.js

js inboxero.ai collected 2026-10-05 14:56:18 UTC 5,684 bytes, 124 lines download raw bytes

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.