atagen

malice

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