Repository navigation
Expand file tree
/
Copy pathsw.js
More file actions
109 lines (101 loc) · 3.52 KB
/
Copy pathsw.js
File metadata and controls
109 lines (101 loc) · 3.52 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
// SUNDOWNER service worker — app-shell + static asset caching.
//
// Strategy:
// navigation (HTML) -> network-first, fall back to cached shell when offline.
// HTML is NEVER cache-first, so a new deploy is always
// picked up immediately (avoids the stale-shell black screen).
// /js/ /css/ static assets -> stale-while-revalidate. These are content-addressed by
// ?v= or a webpack content hash, so cache is safe to serve
// instantly while refreshing in the background.
// /api/ /file/ /dav/ /upload, cross-origin, and non-GET -> bypass (network only).
// /file already has a worker edge cache + ETag/immutable,
// and media must not bloat the SW cache.
//
// To ship a change to this file: bump CACHE_VERSION AND the sw.js?v= in index.html.
// Emergency kill-switch: deploy a sw.js whose body is just
// self.addEventListener('install',()=>self.skipWaiting());
// self.addEventListener('activate',e=>e.waitUntil(caches.keys().then(k=>Promise.all(k.map(c=>caches.delete(c)))).then(()=>self.registration.unregister())));
// which self-unregisters and clears all caches on the next load.
const CACHE_VERSION = 'v1';
const SHELL_CACHE = `sundowner-shell-${CACHE_VERSION}`;
const SHELL_FALLBACK = '/index.html';
self.addEventListener('install', () => {
self.skipWaiting();
});
self.addEventListener('activate', (event) => {
event.waitUntil((async () => {
const keys = await caches.keys();
await Promise.all(
keys
.filter((key) => key.startsWith('sundowner-shell-') && key !== SHELL_CACHE)
.map((key) => caches.delete(key))
);
await self.clients.claim();
})());
});
self.addEventListener('fetch', (event) => {
const req = event.request;
if (req.method !== 'GET') {
return;
}
let url;
try {
url = new URL(req.url);
} catch (err) {
return;
}
if (url.origin !== self.location.origin) {
return;
}
// Never intercept dynamic data, media, WebDAV, or uploads.
if (
url.pathname.startsWith('/api/') ||
url.pathname.startsWith('/file/') ||
url.pathname.startsWith('/dav/') ||
url.pathname.startsWith('/upload')
) {
return;
}
// HTML navigations: network-first, cache the latest shell, fall back when offline.
if (req.mode === 'navigate') {
event.respondWith((async () => {
try {
const fresh = await fetch(req);
if (fresh && fresh.ok) {
const cache = await caches.open(SHELL_CACHE);
cache.put(SHELL_FALLBACK, fresh.clone());
}
return fresh;
} catch (err) {
const cache = await caches.open(SHELL_CACHE);
const cached = await cache.match(SHELL_FALLBACK);
if (cached) {
return cached;
}
throw err;
}
})());
return;
}
// Static JS/CSS: stale-while-revalidate.
if (url.pathname.startsWith('/js/') || url.pathname.startsWith('/css/')) {
event.respondWith((async () => {
const cache = await caches.open(SHELL_CACHE);
const cached = await cache.match(req);
const networkFetch = fetch(req)
.then((res) => {
if (res && res.ok) {
cache.put(req, res.clone());
}
return res;
})
.catch(() => null);
if (cached) {
event.waitUntil(networkFetch);
return cached;
}
const res = await networkFetch;
return res || Response.error();
})());
}
});