PageSourceSearch

https://grimoires.online/hub/sw.js?v=0.1.061

js grimoires.online collected 2026-10-02 02:03:20 UTC 9,694 bytes, 231 lines download raw bytes

1const GRIMOIRES_SCOPE = '/hub/';
2const ASSET_VERSION = '0.1.061';
3const FORCE_ACTIVATE_VERSION = '0.1.020';
4const SHELL_CACHE = `grimoires-shell-${ASSET_VERSION}`;
5const SHELL_URL = '/hub/';
6const VERSION_URL = '/hub/version.json';
7
8// The production Gradle task replaces this list after webpack has produced the
9// hashed Wasm filenames. Keeping a small source fallback makes local/dev serving safe.
10const GRIMOIRES_PRECACHE = Object.freeze([
11    "/hub/",
12    "/hub/284a3fcef66a1adb6321.wasm",
13    "/hub/6e23e5428398b92da386.wasm",
14    "/hub/assets/auth_grimoireshub_logo.png",
15    "/hub/assets/auth_grimoireshub_logo_dark.png",
16    "/hub/assets/push/badge.png",
17    "/hub/assets/push/grimoireshub.png",
18    "/hub/assets/push/guild_placeholder.png",
19    "/hub/assets/push/mention.png",
20    "/hub/assets/push/reply.png",
21    "/hub/assets/pwa/apple-touch-icon-180.png",
22    "/hub/assets/pwa/icon-192.png",
23    "/hub/assets/pwa/icon-512.png",
24    "/hub/assets/pwa/icon-maskable-512.png",
25    "/hub/config.js",
26    "/hub/grimoiresHub.js",
27    "/hub/manifest.webmanifest",
28    "/hub/replay-bridge.js"
29]);
30
31const PRECACHE_PATHS = new Set(
32    GRIMOIRES_PRECACHE.map(value => new URL(value, self.location.origin).pathname)
33);
34const ASSETS = Object.freeze({
35    app: `/hub/assets/push/grimoireshub.png?v=${ASSET_VERSION}`,
36    badge: `/hub/assets/push/badge.png?v=${ASSET_VERSION}`,
37    mention: `/hub/assets/push/mention.png?v=${ASSET_VERSION}`,
38    reply: `/hub/assets/push/reply.png?v=${ASSET_VERSION}`,
39    guild: `/hub/assets/push/guild_placeholder.png?v=${ASSET_VERSION}`
40});
41
42async function fetchForPrecache(path) {
43    const request = new Request(path, {
44        method: 'GET',
45        cache: 'reload',
46        credentials: 'same-origin'
47    });
48    const response = await fetch(request);
49    if (!response || !response.ok) {
50        throw new Error(`Precache failed for ${path}: ${response?.status || 'network'}`);
51    }
52    return response;
53}
54
55self.addEventListener('install', event => {
56    // A new worker is allowed to wait only after it owns one complete, coherent
57    // HTML + JS + Wasm shell. One missing binary aborts installation instead of
58    // activating the familiar white rectangle of despair.
59    event.waitUntil((async () => {
60        const cache = await caches.open(SHELL_CACHE);
61        await Promise.all(GRIMOIRES_PRECACHE.map(async path => {
62            const response = await fetchForPrecache(path);
63            await cache.put(new Request(path), response);
64        }));
65        if (ASSET_VERSION === FORCE_ACTIVATE_VERSION) await self.skipWaiting();
66    })());
67});
68
69self.addEventListener('activate', event => {
70    event.waitUntil((async () => {
71        const names = await caches.keys();
72        await Promise.all(names
73            .filter(name => name.startsWith('grimoires-shell-') && name !== SHELL_CACHE)
74            .map(name => caches.delete(name)));
75        await self.clients.claim();
76        if (ASSET_VERSION === FORCE_ACTIVATE_VERSION) {
77            const windows = await self.clients.matchAll({ type: 'window', includeUncontrolled: true });
78            await Promise.all(windows.map(async client => {
79                if (!('navigate' in client)) return;
80                try { await client.navigate(client.url); } catch (_) {}
81            }));
82        }
83    })());
84});
85
86self.addEventListener('message', event => {
87    if (event.data?.type === 'SKIP_WAITING') self.skipWaiting();
88});
89
90async function cachedShellResponse(request) {
91    const cache = await caches.open(SHELL_CACHE);
92    return (await cache.match(request, { ignoreSearch: true }))
93        || (await cache.match(SHELL_URL, { ignoreSearch: true }));
94}
95
96async function networkResponseWithTimeout(request, timeoutMs = 5000) {
97    const controller = new AbortController();
98    const timer = setTimeout(() => controller.abort(), timeoutMs);
99    try {
100        return await fetch(request, { signal: controller.signal });
101    } finally {
102        clearTimeout(timer);
103    }
104}
105
106self.addEventListener('fetch', event => {
107    const request = event.request;
108    if (request.method !== 'GET') return;
109
110    const url = new URL(request.url);
111    if (url.origin !== self.location.origin) return;
112
113    // version.json is deliberately never served from the application cache. It is
114    // the authority for the manual update notice in the menu.
115    if (url.pathname === VERSION_URL) return;
116
117    if (request.mode === 'navigate' && url.pathname.startsWith('/hub/')) {
118        event.respondWith((async () => {
119            const cached = await cachedShellResponse(request);
120            if (cached) return cached;
121            try {
122                const response = await networkResponseWithTimeout(request);
123                if (response && response.ok) {
124                    const cache = await caches.open(SHELL_CACHE);
125                    await cache.put(SHELL_URL, response.clone()).catch(() => {});
126                    return response;
127                }
128                return response || Response.error();
129            } catch (_) {
130                return (await cachedShellResponse(request)) || Response.error();
131            }
132        })());
133        return;
134    }
135
136    if (!PRECACHE_PATHS.has(url.pathname)) return;
137    event.respondWith((async () => {
138        const cache = await caches.open(SHELL_CACHE);
139        const cached = await cache.match(request, { ignoreSearch: true });
140        if (cached) return cached;
141        try {
142            const response = await networkResponseWithTimeout(request);
143            if (response && response.ok) {
144                await cache.put(new Request(url.pathname), response.clone()).catch(() => {});
145            }
146            return response || Response.error();
147        } catch (_) {
148            return Response.error();
149        }
150    })());
151});
152
153function stringValue(value) {
154    return String(value == null ? '' : value).trim();
155}
156
157function notificationArtwork(payload, data) {
158    const eventType = stringValue(data.event_type || data.eventType || payload.event_type || payload.type || data.kind || payload.kind).toLowerCase();
159    const conversationType = stringValue(data.conversation_type || data.conversationType || payload.conversation_type).toLowerCase();
160    if (eventType.includes('mention')) return { icon: ASSETS.mention, badge: ASSETS.badge };
161    if (eventType.includes('reply')) return { icon: ASSETS.reply, badge: ASSETS.badge };
162    if (conversationType === 'direct') {
163        return {
164            icon: stringValue(data.sender_avatar_url || data.senderAvatarUrl || payload.sender_avatar_url) || ASSETS.app,
165            badge: ASSETS.badge
166        };
167    }
168    if (conversationType === 'guild' || conversationType === 'group' || data.topic_id || data.topicId) {
169        return {
170            icon: stringValue(data.guild_avatar_url || data.guildAvatarUrl || payload.guild_avatar_url) || ASSETS.guild,
171            badge: ASSETS.badge
172        };
173    }
174    return { icon: stringValue(payload.icon) || ASSETS.app, badge: stringValue(payload.badge) || ASSETS.badge };
175}
176
177self.addEventListener('push', event => {
178    let payload = {};
179    try {
180        payload = event.data ? event.data.json() : {};
181    } catch (_) {
182        payload = { body: event.data ? event.data.text() : '' };
183    }
184
185    const nestedData = payload && typeof payload.data === 'object' ? payload.data : {};
186    const title = payload.title || nestedData.title || 'GrimoiresHub';
187    const targetUrl = payload.url || payload.route || nestedData.url || nestedData.route || GRIMOIRES_SCOPE;
188    const artwork = notificationArtwork(payload, nestedData);
189    const options = {
190        body: payload.body || payload.message || nestedData.body || nestedData.message || '',
191        tag: payload.tag || payload.type || nestedData.tag || nestedData.event_type || 'grimoires-update',
192        data: { ...nestedData, url: targetUrl },
193        icon: artwork.icon,
194        badge: artwork.badge,
195        renotify: payload.renotify !== false,
196        silent: Boolean(payload.silent),
197        requireInteraction: Boolean(payload.requireInteraction)
198    };
199
200    event.waitUntil((async () => {
201        const windows = await self.clients.matchAll({ type: 'window', includeUncontrolled: true });
202        // Suppress a duplicate system notification only while the user is actively
203        // focused on GrimoiresHub. A merely visible but unfocused desktop window
204        // must still receive real notifications.
205        const activeWindow = windows.find(client => client.focused === true);
206        const notificationKind = stringValue(nestedData.kind || payload.kind).toLowerCase();
207        const forceSystemNotification = notificationKind === 'notification_test' || payload.force_show === true || nestedData.force_show === true;
208        if (activeWindow) {
209            try { activeWindow.postMessage({ type: 'grimoires-push', payload: { ...payload, data: options.data } }); } catch (_) {}
210            if (!forceSystemNotification) return;
211        }
212        await self.registration.showNotification(title, options);
213    })());
214});
215
216self.addEventListener('notificationclick', event => {
217    event.notification.close();
218    const rawUrl = event.notification?.data?.url || GRIMOIRES_SCOPE;
219    const targetUrl = new URL(rawUrl, self.registration.scope).href;
220    event.waitUntil((async () => {
221        const windows = await self.clients.matchAll({ type: 'window', includeUncontrolled: true });
222        for (const client of windows) {
223            if (!('focus' in client)) continue;
224            try {
225                if ('navigate' in client) await client.navigate(targetUrl);
226            } catch (_) {}
227            return client.focus();
228        }
229        return self.clients.openWindow(targetUrl);
230    })());
231});

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.