an atproto based link aggregator
1/// <reference no-default-lib="true"/>
2/// <reference lib="esnext" />
3/// <reference lib="webworker" />
4/// <reference types="@sveltejs/kit" />
5
6import { build, files, version } from '$service-worker';
7
8// This gives `self` the correct types
9const sw = /** @type {ServiceWorkerGlobalScope} */ (/** @type {unknown} */ (globalThis.self));
10
11// Create a unique cache name for this deployment
12const CACHE = `cache-${version}`;
13
14// Assets to cache immediately (app shell)
15const ASSETS = [
16 ...build, // the app itself
17 ...files // everything in static
18];
19
20// Install: cache app shell
21sw.addEventListener('install', (event) => {
22 async function addFilesToCache() {
23 const cache = await caches.open(CACHE);
24 // In dev mode, ASSETS is empty - skip caching
25 if (ASSETS.length > 0) {
26 await cache.addAll(ASSETS);
27 }
28 }
29
30 event.waitUntil(addFilesToCache());
31});
32
33// Activate: clean up old caches
34sw.addEventListener('activate', (event) => {
35 async function deleteOldCaches() {
36 for (const key of await caches.keys()) {
37 if (key !== CACHE) await caches.delete(key);
38 }
39 }
40
41 event.waitUntil(deleteOldCaches());
42});
43
44// Fetch: network-first with cache fallback for pages, cache-first for assets
45sw.addEventListener('fetch', (event) => {
46 // Only handle GET requests
47 if (event.request.method !== 'GET') return;
48
49 const url = new URL(event.request.url);
50
51 // Skip cross-origin requests
52 if (url.origin !== sw.location.origin) return;
53
54 // Skip API routes - always go to network
55 if (url.pathname.startsWith('/api/')) return;
56
57 async function respond() {
58 const cache = await caches.open(CACHE);
59
60 // For static assets (build/files), serve from cache first
61 if (ASSETS.includes(url.pathname)) {
62 const cached = await cache.match(url.pathname);
63 if (cached) return cached;
64 }
65
66 // For pages, try network first, fall back to cache
67 try {
68 const response = await fetch(event.request);
69
70 // if we're offline, fetch can return a value that is not a Response
71 if (!(response instanceof Response)) {
72 throw new Error('invalid response from fetch');
73 }
74
75 // Cache successful responses
76 if (response.status === 200) {
77 cache.put(event.request, response.clone());
78 }
79
80 return response;
81 } catch (err) {
82 // Offline: try to serve from cache
83 const cached = await cache.match(event.request);
84 if (cached) return cached;
85
86 // If no cache and it's a navigation request, show offline page
87 if (event.request.mode === 'navigate') {
88 const offlinePage = await cache.match('/');
89 if (offlinePage) return offlinePage;
90 }
91
92 throw err;
93 }
94 }
95
96 event.respondWith(respond());
97});