Repository navigation
Expand file tree
/
Copy pathsw.js
More file actions
215 lines (196 loc) · 7.64 KB
/
Copy pathsw.js
File metadata and controls
215 lines (196 loc) · 7.64 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
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
const CACHE = 'nym-' + '__CACHE_VERSION__';
const ASSET_RE = /\/(js|css|data|images)\//;
let PRECACHE = [];
try { PRECACHE = JSON.parse('__PRECACHE_ASSETS__'); } catch (_) { }
// Proxied media lives in its own cache that survives deploys.
const MEDIA_CACHE = 'nym-media-v1';
const MEDIA_MAX_ENTRIES = 600;
const MEDIA_MAX_BYTES = 100 * 1024 * 1024;
const MEDIA_MAX_AGE_MS = 7 * 24 * 60 * 60 * 1000;
const MEDIA_STAMP = 'x-nym-cached-at';
const MEDIA_SIZE = 'x-nym-size';
function mediaStamp(resp) {
const t = Number(resp && resp.headers && resp.headers.get(MEDIA_STAMP));
return Number.isFinite(t) && t > 0 ? t : 0;
}
function mediaFresh(resp, now) {
const t = mediaStamp(resp);
return t > 0 && t <= now + 60000 && now - t < MEDIA_MAX_AGE_MS;
}
let mediaPruning = null;
let mediaPruneAgain = false;
async function pruneMediaCache() {
const cache = await caches.open(MEDIA_CACHE);
const keys = await cache.keys();
const now = Date.now();
const live = [];
const drop = [];
for (const k of keys) {
const r = await cache.match(k);
if (!r || !mediaFresh(r, now)) { drop.push(k); continue; }
const size = Number(r.headers.get(MEDIA_SIZE)) || 0;
live.push({ k, t: mediaStamp(r), size });
}
live.sort((a, b) => a.t - b.t);
let bytes = live.reduce((n, x) => n + x.size, 0);
let count = live.length;
for (const x of live) {
if (count <= MEDIA_MAX_ENTRIES && bytes <= MEDIA_MAX_BYTES) break;
drop.push(x.k);
count--;
bytes -= x.size;
}
await Promise.all(drop.map((k) => cache.delete(k)));
}
function trimMediaCache() {
if (mediaPruning) { mediaPruneAgain = true; return mediaPruning; }
mediaPruning = (async () => {
try {
do {
mediaPruneAgain = false;
await pruneMediaCache();
} while (mediaPruneAgain);
} catch (_) { }
mediaPruning = null;
})();
return mediaPruning;
}
async function storeMedia(cache, req, resp) {
try {
const body = await resp.blob();
if (body.size > MEDIA_MAX_BYTES) return;
const headers = new Headers(resp.headers);
headers.set(MEDIA_STAMP, String(Date.now()));
headers.set(MEDIA_SIZE, String(body.size));
await cache.put(req, new Response(body, { status: resp.status, statusText: resp.statusText, headers }));
await trimMediaCache();
} catch (_) { }
}
let mediaStartupPruned = false;
self.addEventListener('install', (e) => {
e.waitUntil((async () => {
try {
const c = await caches.open(CACHE);
// Entry document plus the critical JS/CSS bundle.
await c.add(new Request('/', { cache: 'reload' }));
if (PRECACHE.length) {
await Promise.all(PRECACHE.map((u) =>
c.add(new Request(u, { cache: 'reload' })).catch(() => { })
));
}
} catch (_) { }
self.skipWaiting();
})());
});
self.addEventListener('activate', (e) => {
e.waitUntil((async () => {
const keys = await caches.keys();
await Promise.all(keys.filter((k) => k.startsWith('nym-') && k !== CACHE && k !== MEDIA_CACHE).map((k) => caches.delete(k)));
mediaStartupPruned = true;
await trimMediaCache();
await self.clients.claim();
})());
});
self.addEventListener('fetch', (e) => {
const req = e.request;
if (req.method !== 'GET') return;
const url = new URL(req.url);
// Brand icons on the marketing origin: cache-first.
if (url.origin === 'https://nymchat.app' && url.pathname.startsWith('/images/')) {
e.respondWith((async () => {
const cached = await caches.match(req);
if (cached) return cached;
const resp = await fetch(req);
if (resp && resp.ok) {
const cache = await caches.open(CACHE);
cache.put(req, resp.clone());
}
return resp;
})());
return;
}
if (url.origin !== self.location.origin) return;
// Proxied media is effectively immutable, so cache-first; range requests and non-images pass through.
if (url.pathname === '/api/proxy' && url.searchParams.has('url') && !req.headers.has('range')) {
if (!mediaStartupPruned) {
mediaStartupPruned = true;
const startup = trimMediaCache();
if (e.waitUntil) e.waitUntil(startup);
}
e.respondWith((async () => {
const cache = await caches.open(MEDIA_CACHE);
const cached = await cache.match(req);
if (cached) {
if (mediaFresh(cached, Date.now())) return cached;
await cache.delete(req);
}
const resp = await fetch(req);
const type = resp && resp.headers.get('content-type') || '';
if (resp && resp.ok && type.indexOf('image/') === 0) {
const stored = storeMedia(cache, req, resp.clone());
if (e.waitUntil) e.waitUntil(stored);
}
return resp;
})());
return;
}
if (req.mode === 'navigate') {
e.respondWith((async () => {
const cache = await caches.open(CACHE);
try {
const resp = await fetch(req);
if (resp && resp.ok && (url.pathname === '/' || url.pathname === '/index.html')
&& (resp.headers.get('content-type') || '').indexOf('text/html') === 0) cache.put('/', resp.clone());
return resp;
} catch (_) {
const cached = (await cache.match(req)) || (await cache.match('/'));
return cached || Response.error();
}
})());
return;
}
// Unhashed i18n packs: cache first, refreshed in the background.
if (url.origin === self.location.origin && url.pathname.startsWith('/i18n/')) {
e.respondWith((async () => {
const cache = await caches.open(CACHE);
const cached = await cache.match(req);
const network = fetch(req).then((resp) => {
if (resp && resp.ok) cache.put(req, resp.clone());
return resp;
}).catch(() => cached || Response.error());
return cached || network;
})());
return;
}
// Hashed assets are immutable, and the cache name rotates per build.
if (ASSET_RE.test(url.pathname)) {
e.respondWith((async () => {
const cached = await caches.match(req);
if (cached) return cached;
const resp = await fetch(req);
if (resp && resp.ok) {
const cache = await caches.open(CACHE);
cache.put(req, resp.clone());
}
return resp;
})());
}
});
function sameOrigin(c) {
try { return new URL(c.url).origin === self.location.origin; } catch (_) { return false; }
}
self.addEventListener('notificationclick', (e) => {
const n = e.notification;
const route = n && n.data && n.data.nymRoute ? n.data.nymRoute : null;
if (n) n.close();
e.waitUntil((async () => {
const all = (await self.clients.matchAll({ type: 'window', includeUncontrolled: true })).filter(sameOrigin);
let client = all.find((c) => c.focused) || all.find((c) => c.visibilityState === 'visible') || all[0] || null;
if (client) {
try { client = (await client.focus()) || client; } catch (_) { }
} else if (self.clients.openWindow) {
try { client = await self.clients.openWindow('/'); } catch (_) { client = null; }
}
if (client && route) client.postMessage({ type: 'nym-notification-click', route });
})());
});