atagen

malice

public
Star0
Watch0
20fe182f3204e4d4923607816147c3fa0ae4cccc
2
ssh://brewery@pls.lobotomise.me:33/atagen/malice.git
web/src/sw.tsTypescript91 lines3.0 KB
1
import { AVATAR_CACHE, handleMediaFetch } from './sw-media';
2
3
declare const __MALICE_DEVICE__: boolean;
4
5
// Prepended by vite.config.ts's precacheManifestPlugin: every emitted chunk,
6
// asset, and the wasm, as root-relative URLs.
7
const sw = self as unknown as ServiceWorkerGlobalScope & { __MALICE_PRECACHE__: string[] };
8
9
const CACHE_NAME = 'malice-shell-v1';
10
11
sw.addEventListener('install', (event) => {
12
  event.waitUntil(
13
    (async () => {
14
      const cache = await caches.open(CACHE_NAME);
15
      await cache.addAll(['/', '/index.html', ...sw.__MALICE_PRECACHE__]);
16
    })()
17
  );
18
});
19
20
sw.addEventListener('message', (event) => {
21
  if (event.data?.type === 'SKIP_WAITING') void sw.skipWaiting();
22
});
23
24
sw.addEventListener('activate', (event) => {
25
  event.waitUntil(
26
    (async () => {
27
      const keys = await caches.keys();
28
      await Promise.all(keys.filter((key) => key !== CACHE_NAME && key !== AVATAR_CACHE).map((key) => caches.delete(key)));
29
      const shell = await caches.open(CACHE_NAME);
30
      const keep = new Set(['/', '/index.html', ...sw.__MALICE_PRECACHE__]);
31
      for (const request of await shell.keys()) {
32
        const path = new URL(request.url).pathname;
33
        if (path.startsWith('/assets/') && !keep.has(path)) await shell.delete(request);
34
      }
35
      await sw.clients.claim();
36
    })()
37
  );
38
});
39
40
async function cacheFirst(request: Request): Promise<Response> {
41
  const cached = await caches.match(request);
42
  if (cached) return cached;
43
  const response = await fetch(request);
44
  if (response.ok) {
45
    const cache = await caches.open(CACHE_NAME);
46
    void cache.put(request, response.clone());
47
  }
48
  return response;
49
}
50
51
async function networkFirstNavigation(request: Request): Promise<Response> {
52
  try {
53
    return await fetch(request);
54
  } catch {
55
    const cached = await caches.match('/index.html');
56
    if (cached) return cached;
57
    throw new Error('offline and no cached shell');
58
  }
59
}
60
61
sw.addEventListener('fetch', (event) => {
62
  const url = new URL(event.request.url);
63
64
  if (__MALICE_DEVICE__ && handleMediaFetch(event)) return;
65
  if (url.pathname.startsWith('/api/')) return;
66
  if (url.pathname.startsWith('/assets/') || url.pathname.startsWith('/twemoji/')) {
67
    event.respondWith(cacheFirst(event.request));
68
    return;
69
  }
70
  if (event.request.mode === 'navigate') {
71
    event.respondWith(networkFirstNavigation(event.request));
72
  }
73
});
74
75
sw.addEventListener('notificationclick', (event) => {
76
  event.notification.close();
77
  const roomId = (event.notification.data as { roomId?: unknown } | null)?.roomId;
78
  if (typeof roomId !== 'string') return;
79
  event.waitUntil(
80
    (async () => {
81
      const windows = await sw.clients.matchAll({ type: 'window', includeUncontrolled: true });
82
      const existing = windows.find((client) => client.visibilityState === 'visible') ?? windows[0];
83
      if (!existing) {
84
        await sw.clients.openWindow(`/?room=${encodeURIComponent(roomId)}`);
85
        return;
86
      }
87
      existing.postMessage({ type: 'malice-open-room', roomId });
88
      await existing.focus();
89
    })()
90
  );
91
});