@inkandswitch/patchwork-bootloader 0.4.3 → 0.4.4
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +11 -0
- package/dist/automerge-worker.js +543 -771
- package/dist/module-loader.d.ts +0 -5
- package/dist/module-loader.js +0 -6
- package/dist/service-worker.js +174 -205
- package/dist/setup.d.ts +2 -1
- package/dist/setup.js +251 -375
- package/dist/site.d.ts +17 -32
- package/dist/site.js +291 -376
- package/dist/vite/importmap-plugin.js +1 -5
- package/package.json +4 -4
- package/src/automerge-worker.ts +635 -833
- package/src/module-loader.ts +0 -6
- package/src/service-worker.ts +219 -238
- package/src/setup.ts +287 -401
- package/src/site.ts +377 -439
- package/src/vite/importmap-plugin.ts +1 -5
package/src/module-loader.ts
CHANGED
|
@@ -52,7 +52,6 @@ function getWorker(): Worker {
|
|
|
52
52
|
return worker;
|
|
53
53
|
}
|
|
54
54
|
|
|
55
|
-
/** Ask the worker which plugins the package at `urlAtHeads` exports. */
|
|
56
55
|
function discoverDescriptors(urlAtHeads: AutomergeUrl): Promise<Descriptor[]> {
|
|
57
56
|
const id = nextRequestId++;
|
|
58
57
|
return new Promise<Descriptor[]>((resolve, reject) => {
|
|
@@ -61,11 +60,6 @@ function discoverDescriptors(urlAtHeads: AutomergeUrl): Promise<Descriptor[]> {
|
|
|
61
60
|
});
|
|
62
61
|
}
|
|
63
62
|
|
|
64
|
-
/**
|
|
65
|
-
* ModuleWatcher `importAutomergePackage` hook: discover descriptors in the
|
|
66
|
-
* worker, then return the `{ plugins }` shape with a main-thread `load()` per
|
|
67
|
-
* plugin that imports the package at heads and calls its real loader.
|
|
68
|
-
*/
|
|
69
63
|
export async function importAutomergePackageViaWorker(
|
|
70
64
|
urlAtHeads: string
|
|
71
65
|
): Promise<{ plugins: Descriptor[] }> {
|
package/src/service-worker.ts
CHANGED
|
@@ -1,12 +1,5 @@
|
|
|
1
1
|
/// <reference types="service-worker-types" />
|
|
2
2
|
|
|
3
|
-
// The service worker holds no automerge repo — that lives in the automerge
|
|
4
|
-
// SharedWorker (automerge-worker.ts). This worker manages the cache. When a
|
|
5
|
-
// special URL misses the cache it broadcasts a handoff request; the
|
|
6
|
-
// automerge worker resolves it, puts the response in our cache, and replies
|
|
7
|
-
// "cached" (or "response" for errors and other things that shouldn't be
|
|
8
|
-
// cached).
|
|
9
|
-
|
|
10
3
|
import {
|
|
11
4
|
HANDOFF_CHANNEL,
|
|
12
5
|
type HandoffReplyMessage,
|
|
@@ -18,30 +11,20 @@ const DEFAULT_CACHE_NAME = "patchwork";
|
|
|
18
11
|
let cachename = DEFAULT_CACHE_NAME;
|
|
19
12
|
let debugging = false;
|
|
20
13
|
|
|
21
|
-
// 0 is an opaque response,
|
|
22
|
-
|
|
14
|
+
// 0 is an opaque cross-origin response, which is cacheable and replays to the
|
|
15
|
+
// same no-cors consumer. these are big, so unfortunate.
|
|
16
|
+
const CACHEABLE_STATUSES = [0, 200, 203, 204];
|
|
23
17
|
|
|
24
|
-
// The automerge worker times its own resolution out after 30s and replies
|
|
25
|
-
//
|
|
18
|
+
// The automerge worker times its own resolution out after 30s and replies with
|
|
19
|
+
// an error, so this only fires when nobody is listening at all.
|
|
26
20
|
const HANDOFF_TIMEOUT_MS = 35_000;
|
|
27
21
|
|
|
28
22
|
function log(...args: any[]) {
|
|
29
|
-
if (
|
|
30
|
-
console.log.call(
|
|
31
|
-
console,
|
|
32
|
-
`%cpatchwork:serviceworker%c\n`,
|
|
33
|
-
`color: #00ffcc; font-weight: bold`,
|
|
34
|
-
"color: inherit",
|
|
35
|
-
...args
|
|
36
|
-
);
|
|
23
|
+
if (debugging) console.log("[service-worker]", ...args);
|
|
37
24
|
}
|
|
38
25
|
|
|
39
|
-
//
|
|
40
|
-
//
|
|
41
|
-
// handoffs. The SW can't read localStorage, so it always emits and forwards to
|
|
42
|
-
// the tab, which gates rendering on the live toggle. The SW holds no sync
|
|
43
|
-
// socket — observability only.
|
|
44
|
-
|
|
26
|
+
// A service worker has no localStorage and so can't read the debug config: it
|
|
27
|
+
// always emits these and forwards them to the tab, which does the filtering.
|
|
45
28
|
async function postToClients(message: unknown) {
|
|
46
29
|
const clients = await self.clients.matchAll({
|
|
47
30
|
type: "window",
|
|
@@ -51,9 +34,9 @@ async function postToClients(message: unknown) {
|
|
|
51
34
|
}
|
|
52
35
|
|
|
53
36
|
function lifecycle(level: "info" | "warn", text: string) {
|
|
54
|
-
const msg =
|
|
37
|
+
const msg = `${new Date().toISOString()} ${text}`;
|
|
55
38
|
console[level](msg);
|
|
56
|
-
|
|
39
|
+
postToClients({ type: "sw-lifecycle", level, msg });
|
|
57
40
|
}
|
|
58
41
|
|
|
59
42
|
lifecycle("info", `booted (scope ${self.registration?.scope ?? "?"})`);
|
|
@@ -77,19 +60,22 @@ self.addEventListener("unhandledrejection", (event) => {
|
|
|
77
60
|
);
|
|
78
61
|
});
|
|
79
62
|
|
|
63
|
+
// ── Lifecycle ──────────────────────────────────────────────────────────
|
|
64
|
+
|
|
80
65
|
self.addEventListener("install", (event) => {
|
|
81
66
|
lifecycle("info", "install (skipWaiting)");
|
|
82
67
|
// waitUntil keeps the worker alive until skipWaiting resolves, so a freshly
|
|
83
|
-
// installed
|
|
68
|
+
// installed worker reliably jumps the waiting queue instead of stalling until
|
|
84
69
|
// every old tab closes.
|
|
85
70
|
(event as ExtendableEvent).waitUntil(self.skipWaiting());
|
|
86
71
|
});
|
|
87
72
|
|
|
88
73
|
async function clearOtherCaches() {
|
|
74
|
+
const names = await caches.keys();
|
|
89
75
|
await Promise.all(
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
76
|
+
names
|
|
77
|
+
.filter((name) => name !== cachename)
|
|
78
|
+
.map((name) => caches.delete(name))
|
|
93
79
|
);
|
|
94
80
|
}
|
|
95
81
|
|
|
@@ -99,22 +85,20 @@ self.addEventListener("activate", (event) => {
|
|
|
99
85
|
(async () => {
|
|
100
86
|
await clearOtherCaches();
|
|
101
87
|
await self.clients.claim();
|
|
102
|
-
// Pre-cache pages of already-open clients so they survive going
|
|
103
|
-
// before the next navigation.
|
|
104
|
-
const
|
|
88
|
+
// Pre-cache the pages of already-open clients so they survive going
|
|
89
|
+
// offline before the next navigation.
|
|
90
|
+
const clients = await self.clients.matchAll({ type: "window" });
|
|
105
91
|
const cache = await caches.open(cachename);
|
|
106
92
|
await Promise.all(
|
|
107
|
-
|
|
93
|
+
clients.map(async (client) => {
|
|
108
94
|
try {
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
await cachePage(cache, client.url, response);
|
|
114
|
-
}
|
|
95
|
+
if (await cache.match(client.url)) return;
|
|
96
|
+
const response = await fetch(client.url);
|
|
97
|
+
if (CACHEABLE_STATUSES.includes(response.status)) {
|
|
98
|
+
await cachePage(cache, client.url, response);
|
|
115
99
|
}
|
|
116
100
|
} catch {
|
|
117
|
-
// Network may be unavailable during activation
|
|
101
|
+
// Network may be unavailable during activation.
|
|
118
102
|
}
|
|
119
103
|
})
|
|
120
104
|
);
|
|
@@ -123,31 +107,33 @@ self.addEventListener("activate", (event) => {
|
|
|
123
107
|
});
|
|
124
108
|
|
|
125
109
|
self.addEventListener("message", async (event) => {
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
}
|
|
131
|
-
console.info(`moving from cache ${cachename} to ${nextCachename}`);
|
|
132
|
-
if (cachename === DEFAULT_CACHE_NAME) {
|
|
133
|
-
const defaultCache = await caches.open(cachename);
|
|
134
|
-
const nextCache = await caches.open(nextCachename);
|
|
135
|
-
await Promise.all(
|
|
136
|
-
(await defaultCache.keys()).map(async (request) => {
|
|
137
|
-
const response = await defaultCache.match(request);
|
|
138
|
-
if (response) await nextCache.put(request, response);
|
|
139
|
-
})
|
|
140
|
-
);
|
|
141
|
-
}
|
|
142
|
-
cachename = nextCachename;
|
|
143
|
-
await clearOtherCaches();
|
|
144
|
-
} else if (event.data.type == "debug") {
|
|
145
|
-
debugging = event.data.debug;
|
|
110
|
+
const data = event.data;
|
|
111
|
+
|
|
112
|
+
if (data?.type === "debug") {
|
|
113
|
+
debugging = data.debug;
|
|
146
114
|
log("serviceworker debugging enabled");
|
|
115
|
+
return;
|
|
147
116
|
}
|
|
148
|
-
});
|
|
149
117
|
|
|
150
|
-
|
|
118
|
+
if (data?.type !== "cachename" || cachename === data.cachename) return;
|
|
119
|
+
|
|
120
|
+
console.info(`moving from cache ${cachename} to ${data.cachename}`);
|
|
121
|
+
const previous = cachename;
|
|
122
|
+
// Switch before copying: fetches landing mid-copy must write into the new
|
|
123
|
+
// cache, or their entries get deleted along with the old one below.
|
|
124
|
+
cachename = data.cachename;
|
|
125
|
+
if (previous === DEFAULT_CACHE_NAME) {
|
|
126
|
+
const from = await caches.open(previous);
|
|
127
|
+
const to = await caches.open(cachename);
|
|
128
|
+
await Promise.all(
|
|
129
|
+
(await from.keys()).map(async (request) => {
|
|
130
|
+
const response = await from.match(request);
|
|
131
|
+
if (response) await to.put(request, response);
|
|
132
|
+
})
|
|
133
|
+
);
|
|
134
|
+
}
|
|
135
|
+
await clearOtherCaches();
|
|
136
|
+
});
|
|
151
137
|
|
|
152
138
|
const handoffChannel = new BroadcastChannel(HANDOFF_CHANNEL);
|
|
153
139
|
|
|
@@ -160,34 +146,30 @@ const pendingHandoffs = new Map<string, PendingHandoff>();
|
|
|
160
146
|
|
|
161
147
|
handoffChannel.addEventListener("message", (event) => {
|
|
162
148
|
const data = event.data;
|
|
149
|
+
|
|
163
150
|
if (data?.type === "cached" || data?.type === "response") {
|
|
164
151
|
const pending = pendingHandoffs.get(data.id);
|
|
165
|
-
if (!pending) {
|
|
166
|
-
return log(`no pending handoff for id ${data.id}`);
|
|
167
|
-
}
|
|
152
|
+
if (!pending) return log(`no pending handoff for id ${data.id}`);
|
|
168
153
|
pending.resolvers.resolve(data as HandoffReplyMessage);
|
|
169
|
-
|
|
170
|
-
// The automerge worker (re)started — re-broadcast anything still in
|
|
171
|
-
// flight so requests that raced its boot aren't stranded.
|
|
172
|
-
const stranded = [...pendingHandoffs.values()];
|
|
173
|
-
if (stranded.length > 0) {
|
|
174
|
-
lifecycle(
|
|
175
|
-
"info",
|
|
176
|
-
`automerge worker (re)started; re-broadcasting ${stranded.length} ` +
|
|
177
|
-
`in-flight asset handoff(s)`
|
|
178
|
-
);
|
|
179
|
-
}
|
|
180
|
-
for (const { message } of stranded) {
|
|
181
|
-
log(`re-broadcasting handoff ${message.id} to the fresh worker`);
|
|
182
|
-
handoffChannel.postMessage(message);
|
|
183
|
-
}
|
|
154
|
+
return;
|
|
184
155
|
}
|
|
156
|
+
|
|
157
|
+
if (data?.type !== "online") return;
|
|
158
|
+
// The automerge worker (re)started — re-broadcast anything still in flight so
|
|
159
|
+
// requests that raced its boot aren't stranded.
|
|
160
|
+
const stranded = [...pendingHandoffs.values()];
|
|
161
|
+
if (stranded.length === 0) return;
|
|
162
|
+
lifecycle(
|
|
163
|
+
"info",
|
|
164
|
+
`automerge worker (re)started; re-broadcasting ${stranded.length} in-flight asset handoff(s)`
|
|
165
|
+
);
|
|
166
|
+
for (const { message } of stranded) handoffChannel.postMessage(message);
|
|
185
167
|
});
|
|
186
168
|
|
|
187
169
|
/** Signals that respondWith should reject; see {@link HandoffAbortMessage}. */
|
|
188
170
|
class HandoffAborted extends Error {}
|
|
189
171
|
|
|
190
|
-
function handoff(
|
|
172
|
+
async function handoff(
|
|
191
173
|
request: Request,
|
|
192
174
|
handoffURL: URL
|
|
193
175
|
): Promise<HandoffReplyMessage> {
|
|
@@ -206,14 +188,15 @@ function handoff(
|
|
|
206
188
|
referrer: request.referrer,
|
|
207
189
|
},
|
|
208
190
|
};
|
|
191
|
+
|
|
209
192
|
pendingHandoffs.set(id, { message, resolvers });
|
|
210
193
|
log(`broadcasting handoff request for cache ${cachename}`, message);
|
|
211
194
|
handoffChannel.postMessage(message);
|
|
195
|
+
|
|
212
196
|
const timeout = setTimeout(() => {
|
|
213
197
|
lifecycle(
|
|
214
198
|
"warn",
|
|
215
|
-
`asset handoff ${id} stranded: no reply from the automerge worker after `
|
|
216
|
-
`${HANDOFF_TIMEOUT_MS}ms (${handoffURL.href})`
|
|
199
|
+
`asset handoff ${id} stranded: no reply from the automerge worker after ${HANDOFF_TIMEOUT_MS}ms (${handoffURL.href})`
|
|
217
200
|
);
|
|
218
201
|
resolvers.reject(
|
|
219
202
|
new Error(
|
|
@@ -221,39 +204,31 @@ function handoff(
|
|
|
221
204
|
)
|
|
222
205
|
);
|
|
223
206
|
}, HANDOFF_TIMEOUT_MS);
|
|
207
|
+
|
|
224
208
|
return resolvers.promise.finally(() => {
|
|
225
209
|
clearTimeout(timeout);
|
|
226
210
|
pendingHandoffs.delete(id);
|
|
227
211
|
});
|
|
228
212
|
}
|
|
229
213
|
|
|
230
|
-
|
|
231
|
-
|
|
232
|
-
|
|
233
|
-
|
|
234
|
-
|
|
235
|
-
|
|
236
|
-
|
|
237
|
-
|
|
238
|
-
|
|
239
|
-
|
|
240
|
-
|
|
241
|
-
|
|
242
|
-
|
|
243
|
-
|
|
244
|
-
|
|
245
|
-
|
|
246
|
-
|
|
247
|
-
|
|
248
|
-
}
|
|
249
|
-
|
|
250
|
-
function rootRequestFor(request: Request | string): Request | undefined {
|
|
251
|
-
const url = new URL(typeof request === "string" ? request : request.url);
|
|
252
|
-
if (url.origin !== self.location.origin) return undefined;
|
|
253
|
-
url.pathname = "/";
|
|
254
|
-
url.search = "";
|
|
255
|
-
url.hash = "";
|
|
256
|
-
return new Request(url.href);
|
|
214
|
+
// ── Caching ────────────────────────────────────────────────────────────
|
|
215
|
+
|
|
216
|
+
/** A page is cached under its own url plus /index.html and / on this origin. */
|
|
217
|
+
function pageCacheKeys(request: Request | string): Request[] {
|
|
218
|
+
const original = typeof request === "string" ? new Request(request) : request;
|
|
219
|
+
const url = new URL(original.url);
|
|
220
|
+
if (url.origin !== self.location.origin) return [original];
|
|
221
|
+
|
|
222
|
+
return [
|
|
223
|
+
original,
|
|
224
|
+
...["/index.html", "/"].map((pathname) => {
|
|
225
|
+
const variant = new URL(url.href);
|
|
226
|
+
variant.pathname = pathname;
|
|
227
|
+
variant.search = "";
|
|
228
|
+
variant.hash = "";
|
|
229
|
+
return new Request(variant.href);
|
|
230
|
+
}),
|
|
231
|
+
];
|
|
257
232
|
}
|
|
258
233
|
|
|
259
234
|
async function cachePage(
|
|
@@ -261,157 +236,163 @@ async function cachePage(
|
|
|
261
236
|
request: Request | string,
|
|
262
237
|
response: Response
|
|
263
238
|
) {
|
|
264
|
-
const
|
|
265
|
-
|
|
266
|
-
|
|
267
|
-
|
|
268
|
-
|
|
269
|
-
|
|
270
|
-
]);
|
|
239
|
+
const keys = pageCacheKeys(request);
|
|
240
|
+
await Promise.all(
|
|
241
|
+
keys.map((key, i) =>
|
|
242
|
+
cache.put(key, i === keys.length - 1 ? response : response.clone())
|
|
243
|
+
)
|
|
244
|
+
);
|
|
271
245
|
}
|
|
272
246
|
|
|
273
|
-
//
|
|
274
|
-
//
|
|
275
|
-
//
|
|
276
|
-
//
|
|
247
|
+
// cache.put only resolves once the whole body has been consumed and persisted,
|
|
248
|
+
// so awaiting it before returning would turn time-to-first-byte into
|
|
249
|
+
// time-to-last-byte-plus-disk for every proxied asset. waitUntil keeps the
|
|
250
|
+
// worker alive for the write.
|
|
277
251
|
function cacheInBackground(
|
|
278
252
|
fetchEvent: FetchEvent,
|
|
279
253
|
cache: Cache,
|
|
280
254
|
request: Request,
|
|
281
255
|
response: Response
|
|
282
256
|
) {
|
|
257
|
+
const isPage =
|
|
258
|
+
request.mode === "navigate" || request.destination === "document";
|
|
283
259
|
fetchEvent.waitUntil(
|
|
284
|
-
(
|
|
260
|
+
(isPage
|
|
285
261
|
? cachePage(cache, request, response)
|
|
286
262
|
: cache.put(request, response)
|
|
287
263
|
).catch((error) => {
|
|
288
|
-
// Always loud
|
|
289
|
-
//
|
|
264
|
+
// Always loud: a QuotaExceededError here is the first sign the origin is
|
|
265
|
+
// under storage pressure.
|
|
290
266
|
console.warn(`error caching ${request.url} in ${cachename}`, error);
|
|
291
267
|
})
|
|
292
268
|
);
|
|
293
269
|
}
|
|
294
270
|
|
|
295
|
-
|
|
271
|
+
/** A same-origin path that is itself an encoded URL, e.g. /automerge%3Aabc/x. */
|
|
272
|
+
function specialURLFor(request: Request): URL | undefined {
|
|
273
|
+
const url = new URL(request.url);
|
|
274
|
+
if (
|
|
275
|
+
url.hostname !== self.location.hostname ||
|
|
276
|
+
url.port !== self.location.port ||
|
|
277
|
+
url.protocol !== self.location.protocol
|
|
278
|
+
) {
|
|
279
|
+
return undefined;
|
|
280
|
+
}
|
|
281
|
+
try {
|
|
282
|
+
return new URL(decodeURIComponent(url.pathname.slice(1)));
|
|
283
|
+
} catch {
|
|
284
|
+
return undefined;
|
|
285
|
+
}
|
|
286
|
+
}
|
|
296
287
|
|
|
297
|
-
|
|
298
|
-
|
|
299
|
-
|
|
300
|
-
|
|
301
|
-
|
|
302
|
-
|
|
303
|
-
|
|
288
|
+
async function serveHandoff(
|
|
289
|
+
fetchEvent: FetchEvent,
|
|
290
|
+
cache: Cache,
|
|
291
|
+
handoffURL: URL,
|
|
292
|
+
cached: Response | undefined
|
|
293
|
+
): Promise<Response> {
|
|
294
|
+
if (cached) {
|
|
295
|
+
log(`serving ${handoffURL} from cache ${cachename}`);
|
|
296
|
+
return cached;
|
|
297
|
+
}
|
|
304
298
|
|
|
305
|
-
|
|
299
|
+
log(`handing ${handoffURL} off to the automerge worker`);
|
|
300
|
+
const replyPromise = handoff(fetchEvent.request, handoffURL);
|
|
301
|
+
fetchEvent.waitUntil(replyPromise.catch(() => {}));
|
|
302
|
+
const reply = await replyPromise;
|
|
306
303
|
|
|
307
|
-
if (
|
|
308
|
-
|
|
309
|
-
|
|
310
|
-
|
|
311
|
-
|
|
312
|
-
try {
|
|
313
|
-
handoffURL = new URL(decodeURIComponent(url.pathname.slice(1)));
|
|
314
|
-
log(`received special request ${handoffURL}`);
|
|
315
|
-
} catch {}
|
|
304
|
+
if (reply.type === "abort") {
|
|
305
|
+
// Rejecting respondWith gives the caller a network error rather than a
|
|
306
|
+
// response it can memoize.
|
|
307
|
+
log(`aborting ${handoffURL}: ${reply.reason}`);
|
|
308
|
+
throw new HandoffAborted(reply.reason);
|
|
316
309
|
}
|
|
317
310
|
|
|
318
|
-
|
|
319
|
-
(
|
|
320
|
-
|
|
321
|
-
|
|
311
|
+
if (reply.type === "response") {
|
|
312
|
+
log(`serving handed-off response for ${handoffURL}`, reply);
|
|
313
|
+
return new Response(reply.response.body ?? null, {
|
|
314
|
+
status: reply.response.status ?? 200,
|
|
315
|
+
headers: reply.response.headers,
|
|
316
|
+
});
|
|
317
|
+
}
|
|
322
318
|
|
|
323
|
-
|
|
324
|
-
|
|
325
|
-
|
|
326
|
-
|
|
327
|
-
|
|
328
|
-
|
|
319
|
+
const stored = await cache.match(fetchEvent.request);
|
|
320
|
+
if (!stored) {
|
|
321
|
+
return new Response(
|
|
322
|
+
`the automerge worker reported ${handoffURL} cached, but it has no match in ${cachename}`,
|
|
323
|
+
{ status: 555 }
|
|
324
|
+
);
|
|
325
|
+
}
|
|
326
|
+
log(`serving ${handoffURL} from cache ${cachename} after handoff`);
|
|
327
|
+
return stored;
|
|
328
|
+
}
|
|
329
329
|
|
|
330
|
-
|
|
331
|
-
|
|
332
|
-
|
|
333
|
-
|
|
334
|
-
|
|
335
|
-
|
|
336
|
-
|
|
337
|
-
|
|
338
|
-
|
|
339
|
-
|
|
340
|
-
|
|
341
|
-
}
|
|
330
|
+
async function servePassthrough(
|
|
331
|
+
fetchEvent: FetchEvent,
|
|
332
|
+
cache: Cache,
|
|
333
|
+
cached: Response | undefined
|
|
334
|
+
): Promise<Response> {
|
|
335
|
+
const request = fetchEvent.request;
|
|
336
|
+
// fetch() rejects on network error rather than resolving, so keep the error
|
|
337
|
+
// to surface in the 503 body below.
|
|
338
|
+
const result = await fetch(request).catch((error: unknown) =>
|
|
339
|
+
error instanceof Error ? error : new Error(String(error))
|
|
340
|
+
);
|
|
342
341
|
|
|
343
|
-
|
|
344
|
-
|
|
345
|
-
|
|
346
|
-
|
|
347
|
-
|
|
342
|
+
if (result instanceof Response) {
|
|
343
|
+
if (
|
|
344
|
+
CACHEABLE_STATUSES.includes(result.status) &&
|
|
345
|
+
/^https?:/.test(request.url)
|
|
346
|
+
) {
|
|
347
|
+
cacheInBackground(fetchEvent, cache, request, result.clone());
|
|
348
|
+
} else {
|
|
349
|
+
log(`not caching status ${result.status} for ${request.url}`);
|
|
350
|
+
}
|
|
351
|
+
return result;
|
|
352
|
+
}
|
|
348
353
|
|
|
349
|
-
|
|
350
|
-
|
|
351
|
-
|
|
352
|
-
|
|
353
|
-
|
|
354
|
-
|
|
355
|
-
{ status: 555 }
|
|
356
|
-
);
|
|
357
|
-
}
|
|
358
|
-
log(`serving ${handoffURL} from cache ${cachename} after handoff`);
|
|
359
|
-
return cached;
|
|
360
|
-
} else {
|
|
361
|
-
// fetch() rejects on network error / abort rather than resolving;
|
|
362
|
-
// keep the error so we can surface it in the 503 body below.
|
|
363
|
-
const result = await fetch(request).catch((error: unknown) =>
|
|
364
|
-
error instanceof Error ? error : new Error(String(error))
|
|
365
|
-
);
|
|
366
|
-
if (result instanceof Response) {
|
|
367
|
-
const response = result;
|
|
368
|
-
// Tool subresources (<link>/<script>) are requested from srcdoc
|
|
369
|
-
// frames whose origin is "null", so they come back as opaque
|
|
370
|
-
// cross-origin `no-cors` responses: status 0 and an empty url. They
|
|
371
|
-
// render fine while online but were being excluded from the cache,
|
|
372
|
-
// so e.g. a theme stylesheet vanished on an offline refresh. Opaque
|
|
373
|
-
// responses are cacheable and replay to the same no-cors consumer,
|
|
374
|
-
// so treat status 0 as cacheable and gate the scheme on request.url
|
|
375
|
-
// (an opaque response's own url is "").
|
|
376
|
-
if (
|
|
377
|
-
(response.status === 0 ||
|
|
378
|
-
cacheableStatuses.includes(response.status)) &&
|
|
379
|
-
/^https?:/.test(request.url)
|
|
380
|
-
) {
|
|
381
|
-
cacheInBackground(fetchEvent, cache, request, response.clone());
|
|
382
|
-
} else {
|
|
383
|
-
log(
|
|
384
|
-
`skipping uncacheable response code from cache: ${response.status} for ${request.url}`
|
|
385
|
-
);
|
|
386
|
-
}
|
|
387
|
-
return response;
|
|
388
|
-
}
|
|
389
|
-
if (match) return match;
|
|
390
|
-
return new Response(
|
|
391
|
-
`couldnt fetch ${request.url} and no stale copy in ${cachename}\n\n${
|
|
392
|
-
result.stack ?? result.message
|
|
393
|
-
}`,
|
|
394
|
-
{ status: 503, headers: { "content-type": "text/plain" } }
|
|
395
|
-
);
|
|
396
|
-
}
|
|
397
|
-
} catch (error) {
|
|
398
|
-
// Deliberate: fail the request as a network error, no response.
|
|
399
|
-
if (error instanceof HandoffAborted) throw error;
|
|
400
|
-
const message =
|
|
401
|
-
error instanceof Error
|
|
402
|
-
? `${error.message}\n\n${error.stack}`
|
|
403
|
-
: String(error);
|
|
404
|
-
console.error(
|
|
405
|
-
`service worker error resolving ${request.url}${handoffURL ? ` (for: ${handoffURL})` : ""}`,
|
|
406
|
-
error
|
|
407
|
-
);
|
|
408
|
-
if (match) return match;
|
|
409
|
-
|
|
410
|
-
return new Response(message, {
|
|
411
|
-
status: 556,
|
|
412
|
-
headers: { "content-type": "text/plain" },
|
|
413
|
-
});
|
|
414
|
-
}
|
|
415
|
-
})()
|
|
354
|
+
if (cached) return cached;
|
|
355
|
+
return new Response(
|
|
356
|
+
`couldnt fetch ${request.url} and no stale copy in ${cachename}\n\n${
|
|
357
|
+
result.stack ?? result.message
|
|
358
|
+
}`,
|
|
359
|
+
{ status: 503, headers: { "content-type": "text/plain" } }
|
|
416
360
|
);
|
|
361
|
+
}
|
|
362
|
+
|
|
363
|
+
async function respond(
|
|
364
|
+
fetchEvent: FetchEvent,
|
|
365
|
+
handoffURL: URL | undefined
|
|
366
|
+
): Promise<Response> {
|
|
367
|
+
const cache = await caches.open(cachename);
|
|
368
|
+
const cached = await cache.match(fetchEvent.request);
|
|
369
|
+
|
|
370
|
+
try {
|
|
371
|
+
return handoffURL
|
|
372
|
+
? await serveHandoff(fetchEvent, cache, handoffURL, cached)
|
|
373
|
+
: await servePassthrough(fetchEvent, cache, cached);
|
|
374
|
+
} catch (error) {
|
|
375
|
+
// Deliberate: fail the request as a network error, with no response.
|
|
376
|
+
if (error instanceof HandoffAborted) throw error;
|
|
377
|
+
|
|
378
|
+
console.error(
|
|
379
|
+
`service worker error resolving ${fetchEvent.request.url}` +
|
|
380
|
+
(handoffURL ? ` (for: ${handoffURL})` : ""),
|
|
381
|
+
error
|
|
382
|
+
);
|
|
383
|
+
if (cached) return cached;
|
|
384
|
+
return new Response(
|
|
385
|
+
error instanceof Error
|
|
386
|
+
? `${error.message}\n\n${error.stack}`
|
|
387
|
+
: String(error),
|
|
388
|
+
{ status: 556, headers: { "content-type": "text/plain" } }
|
|
389
|
+
);
|
|
390
|
+
}
|
|
391
|
+
}
|
|
392
|
+
|
|
393
|
+
self.addEventListener("fetch", (fetchEvent: FetchEvent) => {
|
|
394
|
+
const request = fetchEvent.request;
|
|
395
|
+
log("fetch event", request.url);
|
|
396
|
+
if (request.method !== "GET") return;
|
|
397
|
+
fetchEvent.respondWith(respond(fetchEvent, specialURLFor(request)));
|
|
417
398
|
});
|