@inkandswitch/patchwork-bootloader 0.4.3 → 0.5.0
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 +29 -0
- package/dist/automerge-worker.js +533 -771
- package/dist/externals-list.d.ts +5 -0
- package/dist/externals-list.js +41 -0
- package/dist/externals.d.ts +14 -3
- package/dist/externals.js +50 -38
- package/dist/global.css +30 -0
- package/dist/module-loader.d.ts +0 -5
- package/dist/module-loader.js +0 -6
- package/dist/service-worker.js +178 -205
- package/dist/setup.d.ts +2 -1
- package/dist/setup.js +251 -375
- package/dist/sync-config.d.ts +1 -1
- package/dist/sync-config.js +3 -1
- package/package.json +15 -14
- package/src/automerge-worker.ts +634 -839
- package/src/externals-list.ts +43 -0
- package/src/externals.ts +64 -38
- package/src/global.css +30 -0
- package/src/module-loader.ts +0 -6
- package/src/service-worker.ts +224 -238
- package/src/setup.ts +287 -401
- package/src/sync-config.ts +6 -1
- package/dist/site.d.ts +0 -111
- package/dist/site.js +0 -602
- package/dist/vite/importmap-plugin.d.ts +0 -4
- package/dist/vite/importmap-plugin.js +0 -105
- package/dist/vite/patchwork-plugin.d.ts +0 -14
- package/dist/vite/patchwork-plugin.js +0 -8
- package/dist/vite/service-worker-plugin.d.ts +0 -2
- package/dist/vite/service-worker-plugin.js +0 -42
- package/src/site.ts +0 -859
- package/src/vite/importmap-plugin.ts +0 -133
- package/src/vite/patchwork-plugin.ts +0 -20
- package/src/vite/service-worker-plugin.ts +0 -46
package/dist/service-worker.js
CHANGED
|
@@ -1,29 +1,20 @@
|
|
|
1
1
|
/// <reference types="service-worker-types" />
|
|
2
|
-
// The service worker holds no automerge repo — that lives in the automerge
|
|
3
|
-
// SharedWorker (automerge-worker.ts). This worker manages the cache. When a
|
|
4
|
-
// special URL misses the cache it broadcasts a handoff request; the
|
|
5
|
-
// automerge worker resolves it, puts the response in our cache, and replies
|
|
6
|
-
// "cached" (or "response" for errors and other things that shouldn't be
|
|
7
|
-
// cached).
|
|
8
2
|
import { HANDOFF_CHANNEL, } from "./types.js";
|
|
9
3
|
const DEFAULT_CACHE_NAME = "patchwork";
|
|
10
4
|
let cachename = DEFAULT_CACHE_NAME;
|
|
11
5
|
let debugging = false;
|
|
12
|
-
// 0 is an opaque response,
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
//
|
|
6
|
+
// 0 is an opaque cross-origin response, which is cacheable and replays to the
|
|
7
|
+
// same no-cors consumer. these are big, so unfortunate.
|
|
8
|
+
const CACHEABLE_STATUSES = [0, 200, 203, 204];
|
|
9
|
+
// The automerge worker times its own resolution out after 30s and replies with
|
|
10
|
+
// an error, so this only fires when nobody is listening at all.
|
|
16
11
|
const HANDOFF_TIMEOUT_MS = 35_000;
|
|
17
12
|
function log(...args) {
|
|
18
|
-
if (
|
|
19
|
-
|
|
20
|
-
console.log.call(console, `%cpatchwork:serviceworker%c\n`, `color: #00ffcc; font-weight: bold`, "color: inherit", ...args);
|
|
13
|
+
if (debugging)
|
|
14
|
+
console.log("[service-worker]", ...args);
|
|
21
15
|
}
|
|
22
|
-
//
|
|
23
|
-
//
|
|
24
|
-
// handoffs. The SW can't read localStorage, so it always emits and forwards to
|
|
25
|
-
// the tab, which gates rendering on the live toggle. The SW holds no sync
|
|
26
|
-
// socket — observability only.
|
|
16
|
+
// A service worker has no localStorage and so can't read the debug config: it
|
|
17
|
+
// always emits these and forwards them to the tab, which does the filtering.
|
|
27
18
|
async function postToClients(message) {
|
|
28
19
|
const clients = await self.clients.matchAll({
|
|
29
20
|
type: "window",
|
|
@@ -33,9 +24,9 @@ async function postToClients(message) {
|
|
|
33
24
|
client.postMessage(message);
|
|
34
25
|
}
|
|
35
26
|
function lifecycle(level, text) {
|
|
36
|
-
const msg =
|
|
27
|
+
const msg = `${new Date().toISOString()} ${text}`;
|
|
37
28
|
console[level](msg);
|
|
38
|
-
|
|
29
|
+
postToClients({ type: "sw-lifecycle", level, msg });
|
|
39
30
|
}
|
|
40
31
|
lifecycle("info", `booted (scope ${self.registration?.scope ?? "?"})`);
|
|
41
32
|
self.addEventListener("error", (event) => {
|
|
@@ -47,98 +38,95 @@ self.addEventListener("unhandledrejection", (event) => {
|
|
|
47
38
|
const reason = event.reason;
|
|
48
39
|
lifecycle("warn", `unhandled rejection: ${reason instanceof Error ? reason.stack || reason.message : String(reason)}`);
|
|
49
40
|
});
|
|
41
|
+
// ── Lifecycle ──────────────────────────────────────────────────────────
|
|
50
42
|
self.addEventListener("install", (event) => {
|
|
51
43
|
lifecycle("info", "install (skipWaiting)");
|
|
52
44
|
// waitUntil keeps the worker alive until skipWaiting resolves, so a freshly
|
|
53
|
-
// installed
|
|
45
|
+
// installed worker reliably jumps the waiting queue instead of stalling until
|
|
54
46
|
// every old tab closes.
|
|
55
47
|
event.waitUntil(self.skipWaiting());
|
|
56
48
|
});
|
|
57
49
|
async function clearOtherCaches() {
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
50
|
+
const names = await caches.keys();
|
|
51
|
+
await Promise.all(names
|
|
52
|
+
.filter((name) => name !== cachename)
|
|
53
|
+
.map((name) => caches.delete(name)));
|
|
62
54
|
}
|
|
63
55
|
self.addEventListener("activate", (event) => {
|
|
64
56
|
lifecycle("info", "activate (claiming clients)");
|
|
65
57
|
event.waitUntil((async () => {
|
|
66
58
|
await clearOtherCaches();
|
|
67
59
|
await self.clients.claim();
|
|
68
|
-
// Pre-cache pages of already-open clients so they survive going
|
|
69
|
-
// before the next navigation.
|
|
70
|
-
const
|
|
60
|
+
// Pre-cache the pages of already-open clients so they survive going
|
|
61
|
+
// offline before the next navigation.
|
|
62
|
+
const clients = await self.clients.matchAll({ type: "window" });
|
|
71
63
|
const cache = await caches.open(cachename);
|
|
72
|
-
await Promise.all(
|
|
64
|
+
await Promise.all(clients.map(async (client) => {
|
|
73
65
|
try {
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
}
|
|
66
|
+
if (await cache.match(client.url))
|
|
67
|
+
return;
|
|
68
|
+
const response = await fetch(client.url);
|
|
69
|
+
if (CACHEABLE_STATUSES.includes(response.status)) {
|
|
70
|
+
await cachePage(cache, client.url, response);
|
|
80
71
|
}
|
|
81
72
|
}
|
|
82
73
|
catch {
|
|
83
|
-
// Network may be unavailable during activation
|
|
74
|
+
// Network may be unavailable during activation.
|
|
84
75
|
}
|
|
85
76
|
}));
|
|
86
77
|
})());
|
|
87
78
|
});
|
|
88
79
|
self.addEventListener("message", async (event) => {
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
return;
|
|
93
|
-
}
|
|
94
|
-
console.info(`moving from cache ${cachename} to ${nextCachename}`);
|
|
95
|
-
if (cachename === DEFAULT_CACHE_NAME) {
|
|
96
|
-
const defaultCache = await caches.open(cachename);
|
|
97
|
-
const nextCache = await caches.open(nextCachename);
|
|
98
|
-
await Promise.all((await defaultCache.keys()).map(async (request) => {
|
|
99
|
-
const response = await defaultCache.match(request);
|
|
100
|
-
if (response)
|
|
101
|
-
await nextCache.put(request, response);
|
|
102
|
-
}));
|
|
103
|
-
}
|
|
104
|
-
cachename = nextCachename;
|
|
105
|
-
await clearOtherCaches();
|
|
106
|
-
}
|
|
107
|
-
else if (event.data.type == "debug") {
|
|
108
|
-
debugging = event.data.debug;
|
|
80
|
+
const data = event.data;
|
|
81
|
+
if (data?.type === "debug") {
|
|
82
|
+
debugging = data.debug;
|
|
109
83
|
log("serviceworker debugging enabled");
|
|
84
|
+
return;
|
|
85
|
+
}
|
|
86
|
+
if (data?.type !== "cachename" || cachename === data.cachename)
|
|
87
|
+
return;
|
|
88
|
+
console.info(`moving from cache ${cachename} to ${data.cachename}`);
|
|
89
|
+
const previous = cachename;
|
|
90
|
+
// Switch before copying: fetches landing mid-copy must write into the new
|
|
91
|
+
// cache, or their entries get deleted along with the old one below.
|
|
92
|
+
cachename = data.cachename;
|
|
93
|
+
if (previous === DEFAULT_CACHE_NAME) {
|
|
94
|
+
const from = await caches.open(previous);
|
|
95
|
+
const to = await caches.open(cachename);
|
|
96
|
+
await Promise.all((await from.keys()).map(async (request) => {
|
|
97
|
+
const response = await from.match(request);
|
|
98
|
+
if (response)
|
|
99
|
+
await to.put(request, response);
|
|
100
|
+
}));
|
|
110
101
|
}
|
|
102
|
+
await clearOtherCaches();
|
|
111
103
|
});
|
|
112
|
-
// ── Handoff to the automerge worker ────────────────────────────────────
|
|
113
104
|
const handoffChannel = new BroadcastChannel(HANDOFF_CHANNEL);
|
|
114
105
|
const pendingHandoffs = new Map();
|
|
115
106
|
handoffChannel.addEventListener("message", (event) => {
|
|
116
107
|
const data = event.data;
|
|
117
108
|
if (data?.type === "cached" || data?.type === "response") {
|
|
118
109
|
const pending = pendingHandoffs.get(data.id);
|
|
119
|
-
if (!pending)
|
|
110
|
+
if (!pending)
|
|
120
111
|
return log(`no pending handoff for id ${data.id}`);
|
|
121
|
-
}
|
|
122
112
|
pending.resolvers.resolve(data);
|
|
113
|
+
return;
|
|
123
114
|
}
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
handoffChannel.postMessage(message);
|
|
135
|
-
}
|
|
136
|
-
}
|
|
115
|
+
if (data?.type !== "online")
|
|
116
|
+
return;
|
|
117
|
+
// The automerge worker (re)started — re-broadcast anything still in flight so
|
|
118
|
+
// requests that raced its boot aren't stranded.
|
|
119
|
+
const stranded = [...pendingHandoffs.values()];
|
|
120
|
+
if (stranded.length === 0)
|
|
121
|
+
return;
|
|
122
|
+
lifecycle("info", `automerge worker (re)started; re-broadcasting ${stranded.length} in-flight asset handoff(s)`);
|
|
123
|
+
for (const { message } of stranded)
|
|
124
|
+
handoffChannel.postMessage(message);
|
|
137
125
|
});
|
|
138
126
|
/** Signals that respondWith should reject; see {@link HandoffAbortMessage}. */
|
|
139
127
|
class HandoffAborted extends Error {
|
|
140
128
|
}
|
|
141
|
-
function handoff(request, handoffURL) {
|
|
129
|
+
async function handoff(request, handoffURL) {
|
|
142
130
|
const id = crypto.randomUUID();
|
|
143
131
|
const resolvers = Promise.withResolvers();
|
|
144
132
|
const message = {
|
|
@@ -158,8 +146,7 @@ function handoff(request, handoffURL) {
|
|
|
158
146
|
log(`broadcasting handoff request for cache ${cachename}`, message);
|
|
159
147
|
handoffChannel.postMessage(message);
|
|
160
148
|
const timeout = setTimeout(() => {
|
|
161
|
-
lifecycle("warn", `asset handoff ${id} stranded: no reply from the automerge worker after `
|
|
162
|
-
`${HANDOFF_TIMEOUT_MS}ms (${handoffURL.href})`);
|
|
149
|
+
lifecycle("warn", `asset handoff ${id} stranded: no reply from the automerge worker after ${HANDOFF_TIMEOUT_MS}ms (${handoffURL.href})`);
|
|
163
150
|
resolvers.reject(new Error(`no reply from the automerge worker after ${HANDOFF_TIMEOUT_MS}ms`));
|
|
164
151
|
}, HANDOFF_TIMEOUT_MS);
|
|
165
152
|
return resolvers.promise.finally(() => {
|
|
@@ -167,148 +154,134 @@ function handoff(request, handoffURL) {
|
|
|
167
154
|
pendingHandoffs.delete(id);
|
|
168
155
|
});
|
|
169
156
|
}
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
}
|
|
176
|
-
function indexRequestFor(request) {
|
|
177
|
-
const url = new URL(typeof request === "string" ? request : request.url);
|
|
178
|
-
if (url.origin !== self.location.origin)
|
|
179
|
-
return undefined;
|
|
180
|
-
url.pathname = "/index.html";
|
|
181
|
-
url.search = "";
|
|
182
|
-
url.hash = "";
|
|
183
|
-
return new Request(url.href);
|
|
184
|
-
}
|
|
185
|
-
function rootRequestFor(request) {
|
|
186
|
-
const url = new URL(typeof request === "string" ? request : request.url);
|
|
157
|
+
// ── Caching ────────────────────────────────────────────────────────────
|
|
158
|
+
/** A page is cached under its own url plus /index.html and / on this origin. */
|
|
159
|
+
function pageCacheKeys(request) {
|
|
160
|
+
const original = typeof request === "string" ? new Request(request) : request;
|
|
161
|
+
const url = new URL(original.url);
|
|
187
162
|
if (url.origin !== self.location.origin)
|
|
188
|
-
return
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
|
|
163
|
+
return [original];
|
|
164
|
+
return [
|
|
165
|
+
original,
|
|
166
|
+
...["/index.html", "/"].map((pathname) => {
|
|
167
|
+
const variant = new URL(url.href);
|
|
168
|
+
variant.pathname = pathname;
|
|
169
|
+
variant.search = "";
|
|
170
|
+
variant.hash = "";
|
|
171
|
+
return new Request(variant.href);
|
|
172
|
+
}),
|
|
173
|
+
];
|
|
193
174
|
}
|
|
194
175
|
async function cachePage(cache, request, response) {
|
|
195
|
-
const
|
|
196
|
-
|
|
197
|
-
await Promise.all([
|
|
198
|
-
indexRequest && cache.put(indexRequest, response.clone()),
|
|
199
|
-
rootRequest && cache.put(rootRequest, response.clone()),
|
|
200
|
-
cache.put(request, response),
|
|
201
|
-
]);
|
|
176
|
+
const keys = pageCacheKeys(request);
|
|
177
|
+
await Promise.all(keys.map((key, i) => cache.put(key, i === keys.length - 1 ? response : response.clone())));
|
|
202
178
|
}
|
|
203
|
-
//
|
|
204
|
-
//
|
|
205
|
-
//
|
|
206
|
-
//
|
|
179
|
+
// cache.put only resolves once the whole body has been consumed and persisted,
|
|
180
|
+
// so awaiting it before returning would turn time-to-first-byte into
|
|
181
|
+
// time-to-last-byte-plus-disk for every proxied asset. waitUntil keeps the
|
|
182
|
+
// worker alive for the write.
|
|
207
183
|
function cacheInBackground(fetchEvent, cache, request, response) {
|
|
208
|
-
|
|
184
|
+
const isPage = request.mode === "navigate" || request.destination === "document";
|
|
185
|
+
fetchEvent.waitUntil((isPage
|
|
209
186
|
? cachePage(cache, request, response)
|
|
210
187
|
: cache.put(request, response)).catch((error) => {
|
|
211
|
-
// Always loud
|
|
212
|
-
//
|
|
188
|
+
// Always loud: a QuotaExceededError here is the first sign the origin is
|
|
189
|
+
// under storage pressure.
|
|
213
190
|
console.warn(`error caching ${request.url} in ${cachename}`, error);
|
|
214
191
|
}));
|
|
215
192
|
}
|
|
216
|
-
|
|
193
|
+
/** A same-origin path that is itself an encoded URL, e.g. /automerge%3Aabc/x. */
|
|
194
|
+
function specialURLFor(request) {
|
|
195
|
+
const url = new URL(request.url);
|
|
196
|
+
if (url.hostname !== self.location.hostname ||
|
|
197
|
+
url.port !== self.location.port ||
|
|
198
|
+
url.protocol !== self.location.protocol) {
|
|
199
|
+
return undefined;
|
|
200
|
+
}
|
|
201
|
+
try {
|
|
202
|
+
return new URL(decodeURIComponent(url.pathname.slice(1)));
|
|
203
|
+
}
|
|
204
|
+
catch {
|
|
205
|
+
return undefined;
|
|
206
|
+
}
|
|
207
|
+
}
|
|
208
|
+
async function serveHandoff(fetchEvent, cache, handoffURL, cached) {
|
|
209
|
+
if (cached) {
|
|
210
|
+
log(`serving ${handoffURL} from cache ${cachename}`);
|
|
211
|
+
return cached;
|
|
212
|
+
}
|
|
213
|
+
log(`handing ${handoffURL} off to the automerge worker`);
|
|
214
|
+
const replyPromise = handoff(fetchEvent.request, handoffURL);
|
|
215
|
+
fetchEvent.waitUntil(replyPromise.catch(() => { }));
|
|
216
|
+
const reply = await replyPromise;
|
|
217
|
+
if (reply.type === "abort") {
|
|
218
|
+
// Rejecting respondWith gives the caller a network error rather than a
|
|
219
|
+
// response it can memoize.
|
|
220
|
+
log(`aborting ${handoffURL}: ${reply.reason}`);
|
|
221
|
+
throw new HandoffAborted(reply.reason);
|
|
222
|
+
}
|
|
223
|
+
if (reply.type === "response") {
|
|
224
|
+
log(`serving handed-off response for ${handoffURL}`, reply);
|
|
225
|
+
return new Response(reply.response.body ?? null, {
|
|
226
|
+
status: reply.response.status ?? 200,
|
|
227
|
+
headers: reply.response.headers,
|
|
228
|
+
});
|
|
229
|
+
}
|
|
230
|
+
const stored = await cache.match(fetchEvent.request);
|
|
231
|
+
if (!stored) {
|
|
232
|
+
return new Response(`the automerge worker reported ${handoffURL} cached, but it has no match in ${cachename}`, { status: 555 });
|
|
233
|
+
}
|
|
234
|
+
log(`serving ${handoffURL} from cache ${cachename} after handoff`);
|
|
235
|
+
return stored;
|
|
236
|
+
}
|
|
237
|
+
async function servePassthrough(fetchEvent, cache, cached) {
|
|
238
|
+
const request = fetchEvent.request;
|
|
239
|
+
// fetch() rejects on network error rather than resolving, so keep the error
|
|
240
|
+
// to surface in the 503 body below.
|
|
241
|
+
const result = await fetch(request).catch((error) => error instanceof Error ? error : new Error(String(error)));
|
|
242
|
+
if (result instanceof Response) {
|
|
243
|
+
if (CACHEABLE_STATUSES.includes(result.status) &&
|
|
244
|
+
/^https?:/.test(request.url)) {
|
|
245
|
+
cacheInBackground(fetchEvent, cache, request, result.clone());
|
|
246
|
+
}
|
|
247
|
+
else {
|
|
248
|
+
log(`not caching status ${result.status} for ${request.url}`);
|
|
249
|
+
}
|
|
250
|
+
return result;
|
|
251
|
+
}
|
|
252
|
+
if (cached)
|
|
253
|
+
return cached;
|
|
254
|
+
return new Response(`couldnt fetch ${request.url} and no stale copy in ${cachename}\n\n${result.stack ?? result.message}`, { status: 503, headers: { "content-type": "text/plain" } });
|
|
255
|
+
}
|
|
256
|
+
async function respond(fetchEvent, handoffURL) {
|
|
257
|
+
const cache = await caches.open(cachename);
|
|
258
|
+
// A cors request (e.g. the wasm `<link rel=preload crossorigin>`) can miss
|
|
259
|
+
// an entry that a url-keyed lookup finds, so fall back to the bare url —
|
|
260
|
+
// offline boot depends on this hitting.
|
|
261
|
+
const cached = (await cache.match(fetchEvent.request)) ??
|
|
262
|
+
(await cache.match(fetchEvent.request.url));
|
|
263
|
+
try {
|
|
264
|
+
return handoffURL
|
|
265
|
+
? await serveHandoff(fetchEvent, cache, handoffURL, cached)
|
|
266
|
+
: await servePassthrough(fetchEvent, cache, cached);
|
|
267
|
+
}
|
|
268
|
+
catch (error) {
|
|
269
|
+
// Deliberate: fail the request as a network error, with no response.
|
|
270
|
+
if (error instanceof HandoffAborted)
|
|
271
|
+
throw error;
|
|
272
|
+
console.error(`service worker error resolving ${fetchEvent.request.url}` +
|
|
273
|
+
(handoffURL ? ` (for: ${handoffURL})` : ""), error);
|
|
274
|
+
if (cached)
|
|
275
|
+
return cached;
|
|
276
|
+
return new Response(error instanceof Error
|
|
277
|
+
? `${error.message}\n\n${error.stack}`
|
|
278
|
+
: String(error), { status: 556, headers: { "content-type": "text/plain" } });
|
|
279
|
+
}
|
|
280
|
+
}
|
|
217
281
|
self.addEventListener("fetch", (fetchEvent) => {
|
|
218
|
-
log("fetch event", fetchEvent.request.url);
|
|
219
282
|
const request = fetchEvent.request;
|
|
220
|
-
|
|
221
|
-
// instead of proxying their bodies through this worker.
|
|
283
|
+
log("fetch event", request.url);
|
|
222
284
|
if (request.method !== "GET")
|
|
223
285
|
return;
|
|
224
|
-
|
|
225
|
-
let handoffURL;
|
|
226
|
-
if (url.hostname == self.location.hostname &&
|
|
227
|
-
url.port == self.location.port &&
|
|
228
|
-
url.protocol == self.location.protocol) {
|
|
229
|
-
try {
|
|
230
|
-
handoffURL = new URL(decodeURIComponent(url.pathname.slice(1)));
|
|
231
|
-
log(`received special request ${handoffURL}`);
|
|
232
|
-
}
|
|
233
|
-
catch { }
|
|
234
|
-
}
|
|
235
|
-
fetchEvent.respondWith((async () => {
|
|
236
|
-
const cache = await caches.open(cachename);
|
|
237
|
-
const match = await cache.match(request);
|
|
238
|
-
try {
|
|
239
|
-
if (handoffURL) {
|
|
240
|
-
if (match) {
|
|
241
|
-
log(`serving ${handoffURL} from cache ${cachename}`);
|
|
242
|
-
return match;
|
|
243
|
-
}
|
|
244
|
-
log(`handing ${handoffURL} off to the automerge worker`);
|
|
245
|
-
const replyPromise = handoff(request, handoffURL);
|
|
246
|
-
fetchEvent.waitUntil(replyPromise.catch(() => { }));
|
|
247
|
-
const reply = await replyPromise;
|
|
248
|
-
if (reply.type === "abort") {
|
|
249
|
-
// Rejecting respondWith gives the caller a network error rather
|
|
250
|
-
// than a response it can memoize. Rethrown past the catch below,
|
|
251
|
-
// which would otherwise turn this into a 556.
|
|
252
|
-
log(`aborting ${handoffURL}: ${reply.reason}`);
|
|
253
|
-
throw new HandoffAborted(reply.reason);
|
|
254
|
-
}
|
|
255
|
-
if (reply.type === "response") {
|
|
256
|
-
// errors, redirects and other things that shouldn't be cached
|
|
257
|
-
log(`serving handed-off response for ${handoffURL}`, reply);
|
|
258
|
-
return makeResponse(reply.response);
|
|
259
|
-
}
|
|
260
|
-
// reply.type === "cached": the automerge worker has put the
|
|
261
|
-
// response in our cache
|
|
262
|
-
const cached = await cache.match(request);
|
|
263
|
-
if (!cached) {
|
|
264
|
-
return new Response(`the automerge worker reported ${handoffURL} cached, but it has no match in ${cachename}`, { status: 555 });
|
|
265
|
-
}
|
|
266
|
-
log(`serving ${handoffURL} from cache ${cachename} after handoff`);
|
|
267
|
-
return cached;
|
|
268
|
-
}
|
|
269
|
-
else {
|
|
270
|
-
// fetch() rejects on network error / abort rather than resolving;
|
|
271
|
-
// keep the error so we can surface it in the 503 body below.
|
|
272
|
-
const result = await fetch(request).catch((error) => error instanceof Error ? error : new Error(String(error)));
|
|
273
|
-
if (result instanceof Response) {
|
|
274
|
-
const response = result;
|
|
275
|
-
// Tool subresources (<link>/<script>) are requested from srcdoc
|
|
276
|
-
// frames whose origin is "null", so they come back as opaque
|
|
277
|
-
// cross-origin `no-cors` responses: status 0 and an empty url. They
|
|
278
|
-
// render fine while online but were being excluded from the cache,
|
|
279
|
-
// so e.g. a theme stylesheet vanished on an offline refresh. Opaque
|
|
280
|
-
// responses are cacheable and replay to the same no-cors consumer,
|
|
281
|
-
// so treat status 0 as cacheable and gate the scheme on request.url
|
|
282
|
-
// (an opaque response's own url is "").
|
|
283
|
-
if ((response.status === 0 ||
|
|
284
|
-
cacheableStatuses.includes(response.status)) &&
|
|
285
|
-
/^https?:/.test(request.url)) {
|
|
286
|
-
cacheInBackground(fetchEvent, cache, request, response.clone());
|
|
287
|
-
}
|
|
288
|
-
else {
|
|
289
|
-
log(`skipping uncacheable response code from cache: ${response.status} for ${request.url}`);
|
|
290
|
-
}
|
|
291
|
-
return response;
|
|
292
|
-
}
|
|
293
|
-
if (match)
|
|
294
|
-
return match;
|
|
295
|
-
return new Response(`couldnt fetch ${request.url} and no stale copy in ${cachename}\n\n${result.stack ?? result.message}`, { status: 503, headers: { "content-type": "text/plain" } });
|
|
296
|
-
}
|
|
297
|
-
}
|
|
298
|
-
catch (error) {
|
|
299
|
-
// Deliberate: fail the request as a network error, no response.
|
|
300
|
-
if (error instanceof HandoffAborted)
|
|
301
|
-
throw error;
|
|
302
|
-
const message = error instanceof Error
|
|
303
|
-
? `${error.message}\n\n${error.stack}`
|
|
304
|
-
: String(error);
|
|
305
|
-
console.error(`service worker error resolving ${request.url}${handoffURL ? ` (for: ${handoffURL})` : ""}`, error);
|
|
306
|
-
if (match)
|
|
307
|
-
return match;
|
|
308
|
-
return new Response(message, {
|
|
309
|
-
status: 556,
|
|
310
|
-
headers: { "content-type": "text/plain" },
|
|
311
|
-
});
|
|
312
|
-
}
|
|
313
|
-
})());
|
|
286
|
+
fetchEvent.respondWith(respond(fetchEvent, specialURLFor(request)));
|
|
314
287
|
});
|
package/dist/setup.d.ts
CHANGED
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import type { SetupServiceWorkerOptions, SetupServiceWorkerResult, SyncStateDocMessage } from "./types.js";
|
|
2
|
-
|
|
2
|
+
import debug from "debug";
|
|
3
|
+
export declare const lifecycleLog: debug.Debugger;
|
|
3
4
|
export declare function bumpServiceWorkerCache(sw?: ServiceWorker | null): void;
|
|
4
5
|
export declare function getAutomergeWorker(): SharedWorker;
|
|
5
6
|
type SyncStateListener = (update: SyncStateDocMessage) => void;
|