Skip to content
File

Blob: src/client/service-worker.ts

typescript84 lines
1/// <reference lib="webworker" />
2 
3import type { PrecacheEntry, RouteMatchCallbackOptions, SerwistGlobalConfig } from "serwist";
4import {
5 CacheableResponsePlugin,
6 ExpirationPlugin,
7 NetworkFirst,
8 NetworkOnly,
9 PrecacheFallbackPlugin,
10 Serwist,
11} from "serwist";
12 
13declare const self: ServiceWorkerGlobalScope &
14 SerwistGlobalConfig & {
15 __SW_MANIFEST: (PrecacheEntry | string)[] | undefined;
16 };
17 
18const SHELL_PRECACHE_URL = "/__pwa-shell";
19const UPLOAD_CACHE_NAME = "bland-uploads-v1";
20const APP_SHELL_EXCLUDED_PATH_PREFIXES = ["/api/", "/uploads/", "/parties/"];
21 
22function isAppShellExcludedPath(pathname: string): boolean {
23 return APP_SHELL_EXCLUDED_PATH_PREFIXES.some((prefix) => pathname.startsWith(prefix));
24}
25 
26function isAppNavigation({ url, request, sameOrigin }: RouteMatchCallbackOptions): boolean {
27 if (!sameOrigin) return false;
28 if (request.mode !== "navigate") return false;
29 return !isAppShellExcludedPath(url.pathname);
30}
31 
32function isNetworkOnlyNavigation({ url, request, sameOrigin }: RouteMatchCallbackOptions): boolean {
33 return sameOrigin && request.mode === "navigate" && isAppShellExcludedPath(url.pathname);
34}
35 
36function isCacheableUpload({ url, request, sameOrigin }: RouteMatchCallbackOptions): boolean {
37 return (
38 sameOrigin && url.pathname.startsWith("/uploads/") && request.method === "GET" && !url.pathname.endsWith("/data")
39 );
40}
41 
42const serwist = new Serwist({
43 cacheId: "bland",
44 clientsClaim: true,
45 navigationPreload: true,
46 precacheEntries: self.__SW_MANIFEST,
47 precacheOptions: {
48 cleanupOutdatedCaches: true,
49 },
50});
51 
52// Same-origin app navigations only. API, uploads, and parties are excluded so
53// direct browser navigation to those paths cannot receive the app shell.
54serwist.registerCapture(
55 isAppNavigation,
56 new NetworkOnly({
57 plugins: [new PrecacheFallbackPlugin({ fallbackUrls: [SHELL_PRECACHE_URL], serwist })],
58 }),
59);
60 
61serwist.registerCapture(
62 isCacheableUpload,
63 new NetworkFirst({
64 cacheName: UPLOAD_CACHE_NAME,
65 networkTimeoutSeconds: 3,
66 plugins: [
67 // TTL intentionally matches the Worker's `Cache-Control: private,
68 // max-age=300, must-revalidate` on page-scoped uploads
69 // (`src/worker/routes/uploads.ts`) so share-link revocation takes effect
70 // within the same window for SW-cached media.
71 new ExpirationPlugin({ maxEntries: 100, maxAgeSeconds: 300 }),
72 new CacheableResponsePlugin({ statuses: [200] }),
73 ],
74 }),
75);
76 
77// Navigation preload starts before Serwist chooses a route. Handle excluded
78// navigations explicitly so Serwist consumes that response instead of letting
79// the browser issue a second request. Keep this after the upload route so
80// cacheable upload GETs continue to use NetworkFirst.
81serwist.registerCapture(isNetworkOnlyNavigation, new NetworkOnly());
82 
83serwist.addEventListeners();