@inkandswitch/patchwork-bootloader 0.2.5 → 0.2.7
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 +14 -0
- package/dist/automerge-worker.d.ts +1 -0
- package/dist/automerge-worker.js +505 -0
- package/dist/externals.js +0 -1
- package/dist/service-worker.js +98 -205
- package/dist/setup.d.ts +1 -0
- package/dist/setup.js +89 -106
- package/dist/site.d.ts +12 -7
- package/dist/site.js +61 -53
- package/dist/sync-config.d.ts +8 -0
- package/dist/sync-config.js +13 -0
- package/dist/types.d.ts +90 -0
- package/dist/types.js +7 -1
- package/dist/vite/importmap-plugin.js +9 -3
- package/dist/vite/service-worker-plugin.js +25 -9
- package/package.json +20 -19
- package/src/automerge-worker.ts +647 -0
- package/src/externals.ts +0 -1
- package/src/service-worker.ts +123 -248
- package/src/setup.ts +105 -118
- package/src/site.ts +89 -65
- package/src/sync-config.ts +23 -0
- package/src/types.ts +98 -0
- package/src/vite/importmap-plugin.ts +13 -3
- package/src/vite/service-worker-plugin.ts +26 -11
- package/tsconfig.json +1 -1
- package/dist/sw-logger.d.ts +0 -105
- package/dist/sw-logger.js +0 -366
- package/src/sw-logger.ts +0 -463
package/src/service-worker.ts
CHANGED
|
@@ -1,71 +1,27 @@
|
|
|
1
1
|
/// <reference types="service-worker-types" />
|
|
2
2
|
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
//
|
|
6
|
-
//
|
|
7
|
-
//
|
|
8
|
-
//
|
|
9
|
-
// Wasm is fetched from /automerge.wasm (emitted by the vite plugin) instead
|
|
10
|
-
// of bundling the ~3MB base64 string.
|
|
11
|
-
import { initializeWasm, hasHeads } from "@automerge/automerge/slim";
|
|
12
|
-
// eslint-disable-next-line
|
|
13
|
-
// @ts-ignore — initSync is a wasm-bindgen runtime helper not in the .d.ts
|
|
14
|
-
import { initSync as initSubductionSync } from "@automerge/automerge-subduction/slim";
|
|
15
|
-
import { WebCryptoSigner } from "@automerge/automerge-subduction/slim";
|
|
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).
|
|
16
9
|
|
|
17
10
|
import {
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
type PeerId,
|
|
23
|
-
} from "@automerge/automerge-repo/slim";
|
|
24
|
-
import { resolvePath } from "@inkandswitch/patchwork-filesystem";
|
|
25
|
-
|
|
26
|
-
// Small adapters — bundled directly into the SW
|
|
27
|
-
import { IndexedDBStorageAdapter } from "@automerge/automerge-repo-storage-indexeddb";
|
|
28
|
-
import { MessageChannelNetworkAdapter } from "@automerge/automerge-repo-network-messagechannel";
|
|
29
|
-
import { WebSocketClientAdapter } from "@automerge/automerge-repo-network-websocket";
|
|
11
|
+
HANDOFF_CHANNEL,
|
|
12
|
+
type HandoffReplyMessage,
|
|
13
|
+
type HandoffRequestMessage,
|
|
14
|
+
} from "./types.js";
|
|
30
15
|
|
|
31
|
-
// TEMPORARY: enable debug npm module in SW context (no localStorage available)
|
|
32
16
|
let cachename = "default";
|
|
33
17
|
let debugging = false;
|
|
34
|
-
const workerInstanceId = crypto.randomUUID();
|
|
35
|
-
|
|
36
|
-
const SUBDUCTION_ENDPOINTS = ["wss://subduction.sync.inkandswitch.com"];
|
|
37
|
-
const RESOLVE_TIMEOUT_MS = 30_000;
|
|
38
|
-
|
|
39
|
-
// ── Persistent logger ───────────────────────────────────────────────────
|
|
40
|
-
// Initialized eagerly so it's available for the entire SW lifetime.
|
|
41
|
-
// Access from the SW inspector console via self.printLogs(), self.tailLogs(),
|
|
42
|
-
// self.exportLogs(), self.clearLogs().
|
|
43
|
-
const slog = SwLogger.open().then((logger) => {
|
|
44
|
-
(self as any).slog = logger;
|
|
45
|
-
|
|
46
|
-
(self as any).printLogs = async (n = 200) => {
|
|
47
|
-
const entries = await logger.tail(n);
|
|
48
|
-
for (const e of entries) {
|
|
49
|
-
const prefix = `[${e.ts}] [${e.level}]`;
|
|
50
|
-
if (e.data !== undefined) {
|
|
51
|
-
console.log(prefix, e.msg, e.data);
|
|
52
|
-
} else {
|
|
53
|
-
console.log(prefix, e.msg);
|
|
54
|
-
}
|
|
55
|
-
}
|
|
56
|
-
console.log(`--- ${entries.length} entries ---`);
|
|
57
|
-
};
|
|
58
|
-
|
|
59
|
-
(self as any).tailLogs = (n = 200) => logger.tail(n);
|
|
60
|
-
(self as any).exportLogs = () => logger.exportAll();
|
|
61
|
-
(self as any).clearLogs = () => logger.clear();
|
|
62
|
-
|
|
63
|
-
logger.info("sw-logger initialized");
|
|
64
|
-
return logger;
|
|
65
|
-
});
|
|
66
18
|
|
|
67
19
|
const cacheableStatuses = [200, 203, 204, 206];
|
|
68
20
|
|
|
21
|
+
// The automerge worker times its own resolution out after 30s and replies
|
|
22
|
+
// with an error, so this only fires when nobody is listening at all.
|
|
23
|
+
const HANDOFF_TIMEOUT_MS = 35_000;
|
|
24
|
+
|
|
69
25
|
function log(...args: any[]) {
|
|
70
26
|
if (!debugging) return;
|
|
71
27
|
console.log.call(
|
|
@@ -77,7 +33,12 @@ function log(...args: any[]) {
|
|
|
77
33
|
);
|
|
78
34
|
}
|
|
79
35
|
|
|
80
|
-
self.addEventListener("install", () =>
|
|
36
|
+
self.addEventListener("install", (event) => {
|
|
37
|
+
// waitUntil keeps the worker alive until skipWaiting resolves, so a freshly
|
|
38
|
+
// installed SW reliably jumps the "waiting" queue instead of stalling until
|
|
39
|
+
// every old tab closes.
|
|
40
|
+
(event as ExtendableEvent).waitUntil(self.skipWaiting());
|
|
41
|
+
});
|
|
81
42
|
|
|
82
43
|
async function clearOldCaches() {
|
|
83
44
|
const cacheWhitelist = [cachename];
|
|
@@ -90,117 +51,20 @@ async function clearOldCaches() {
|
|
|
90
51
|
await Promise.all(deletePromises);
|
|
91
52
|
}
|
|
92
53
|
|
|
93
|
-
self.addEventListener("activate",
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
const p: Promise<Repo> = (async () => {
|
|
103
|
-
const logger = await slog;
|
|
104
|
-
logger.info("getRepo: starting");
|
|
105
|
-
|
|
106
|
-
logger.info("fetching wasm modules");
|
|
107
|
-
const [amWasmBuf, sdnWasmBuf] = await Promise.all([
|
|
108
|
-
fetch("/automerge.wasm?sw").then((r) => r.arrayBuffer()),
|
|
109
|
-
fetch("/subduction.wasm").then((r) => r.arrayBuffer()),
|
|
110
|
-
]);
|
|
111
|
-
initSubductionSync(new Uint8Array(sdnWasmBuf));
|
|
112
|
-
await initializeWasm(new Uint8Array(amWasmBuf));
|
|
113
|
-
logger.info("wasm initialized");
|
|
114
|
-
|
|
115
|
-
const signer = await WebCryptoSigner.setup();
|
|
116
|
-
|
|
117
|
-
const repo = new Repo({
|
|
118
|
-
storage: new IndexedDBStorageAdapter(),
|
|
119
|
-
signer,
|
|
120
|
-
peerId: ("service-worker-" +
|
|
121
|
-
(Math.random() * 10000).toString(36).slice(2)) as PeerId,
|
|
122
|
-
async sharePolicy(peerId) {
|
|
123
|
-
return peerId.includes("storage-server");
|
|
124
|
-
},
|
|
125
|
-
enableRemoteHeadsGossiping: true,
|
|
126
|
-
subductionWebsocketEndpoints: SUBDUCTION_ENDPOINTS,
|
|
127
|
-
network: [new WebSocketClientAdapter("wss://sync3.automerge.org")],
|
|
128
|
-
});
|
|
129
|
-
|
|
130
|
-
(self as any).repo = repo;
|
|
131
|
-
logger.info("repo constructed, waiting for network subsystem");
|
|
132
|
-
|
|
133
|
-
// Don't block getRepo() on whenReady() — the network subsystem starts
|
|
134
|
-
// with only the subduction adapter, and the MessageChannel adapter is
|
|
135
|
-
// added later via connectPort (which awaits getRepo). Blocking here
|
|
136
|
-
// would deadlock that path and starve the fetch handler.
|
|
137
|
-
repo.networkSubsystem.whenReady().then(() => {
|
|
138
|
-
logger.info("repo network subsystem ready");
|
|
139
|
-
});
|
|
140
|
-
|
|
141
|
-
return repo;
|
|
142
|
-
})();
|
|
143
|
-
// If construction fails (e.g. wasm fetch errors out because the SW was
|
|
144
|
-
// terminated mid-flight), don't permanently cache the rejection — clear
|
|
145
|
-
// the slot so the next caller can retry from scratch.
|
|
146
|
-
p.catch(() => {
|
|
147
|
-
if (repoPromise === p) repoPromise = null;
|
|
148
|
-
});
|
|
149
|
-
repoPromise = p;
|
|
150
|
-
}
|
|
151
|
-
return repoPromise;
|
|
152
|
-
}
|
|
153
|
-
|
|
154
|
-
// Connect client MessagePorts to the repo for sync
|
|
155
|
-
async function connectPort(port: MessagePort) {
|
|
156
|
-
const repo = await getRepo();
|
|
157
|
-
repo.networkSubsystem.addNetworkAdapter(
|
|
158
|
-
new MessageChannelNetworkAdapter(port, { useWeakRef: true })
|
|
54
|
+
self.addEventListener("activate", (event) => {
|
|
55
|
+
// Without waitUntil the activate event settles immediately and clients.claim()
|
|
56
|
+
// runs detached — the new worker can be killed before it takes control, so
|
|
57
|
+
// existing tabs keep talking to the old SW. Extend the event instead.
|
|
58
|
+
(event as ExtendableEvent).waitUntil(
|
|
59
|
+
(async () => {
|
|
60
|
+
await clearOldCaches();
|
|
61
|
+
await self.clients.claim();
|
|
62
|
+
})()
|
|
159
63
|
);
|
|
160
|
-
}
|
|
64
|
+
});
|
|
161
65
|
|
|
162
66
|
self.addEventListener("message", async (event) => {
|
|
163
|
-
if (event.data.type == "
|
|
164
|
-
// Keepalive — Chromium idles out service workers after ~30s of inactivity.
|
|
165
|
-
// Reply via the provided port if any; the message event itself also resets
|
|
166
|
-
// the idle timer.
|
|
167
|
-
const [pongPort] = event.ports;
|
|
168
|
-
log("ping");
|
|
169
|
-
if (pongPort) {
|
|
170
|
-
pongPort.postMessage({ type: "pong", workerInstanceId });
|
|
171
|
-
log("pong");
|
|
172
|
-
pongPort.close();
|
|
173
|
-
} else if (event.source) {
|
|
174
|
-
(event.source as unknown as Client).postMessage({
|
|
175
|
-
type: "pong",
|
|
176
|
-
workerInstanceId,
|
|
177
|
-
});
|
|
178
|
-
log("pong");
|
|
179
|
-
}
|
|
180
|
-
} else if (event.data.type == "port") {
|
|
181
|
-
log("received messagechannel");
|
|
182
|
-
const [port] = event.ports;
|
|
183
|
-
const source = event.source as Client | null;
|
|
184
|
-
const id = event.data.id;
|
|
185
|
-
// event.waitUntil keeps the SW alive until the work completes. Without
|
|
186
|
-
// it, the browser can terminate the SW the moment this synchronous block
|
|
187
|
-
// returns, killing the in-flight wasm fetch.
|
|
188
|
-
(event as unknown as FetchEvent).waitUntil(
|
|
189
|
-
connectPort(port).then(
|
|
190
|
-
() => source?.postMessage({ type: "port-ready", id, workerInstanceId }),
|
|
191
|
-
(err) => {
|
|
192
|
-
console.error("connectPort failed", err);
|
|
193
|
-
// Tell the client we failed so it doesn't hang forever.
|
|
194
|
-
source?.postMessage({
|
|
195
|
-
type: "port-failed",
|
|
196
|
-
id,
|
|
197
|
-
error: String(err),
|
|
198
|
-
workerInstanceId,
|
|
199
|
-
});
|
|
200
|
-
}
|
|
201
|
-
)
|
|
202
|
-
);
|
|
203
|
-
} else if (event.data.type == "cachename") {
|
|
67
|
+
if (event.data.type == "cachename") {
|
|
204
68
|
const nextCachename = event.data.cachename;
|
|
205
69
|
if (cachename == nextCachename) {
|
|
206
70
|
return;
|
|
@@ -216,65 +80,82 @@ self.addEventListener("message", async (event) => {
|
|
|
216
80
|
}
|
|
217
81
|
});
|
|
218
82
|
|
|
219
|
-
// ──
|
|
83
|
+
// ── Handoff to the automerge worker ────────────────────────────────────
|
|
220
84
|
|
|
221
|
-
|
|
222
|
-
const repo = await getRepo();
|
|
223
|
-
const href = automergeURL.href;
|
|
224
|
-
const [maybeAutomergeUrl, ...path] = href.split("/");
|
|
85
|
+
const handoffChannel = new BroadcastChannel(HANDOFF_CHANNEL);
|
|
225
86
|
|
|
226
|
-
|
|
227
|
-
|
|
228
|
-
|
|
87
|
+
type PendingHandoff = {
|
|
88
|
+
message: HandoffRequestMessage;
|
|
89
|
+
resolvers: PromiseWithResolvers<HandoffReplyMessage>;
|
|
90
|
+
};
|
|
229
91
|
|
|
230
|
-
|
|
231
|
-
if (path.length && !path[path.length - 1]) path.pop();
|
|
92
|
+
const pendingHandoffs = new Map<string, PendingHandoff>();
|
|
232
93
|
|
|
233
|
-
|
|
234
|
-
const
|
|
235
|
-
|
|
236
|
-
|
|
237
|
-
|
|
238
|
-
|
|
239
|
-
|
|
240
|
-
|
|
241
|
-
|
|
242
|
-
|
|
243
|
-
|
|
244
|
-
|
|
245
|
-
|
|
246
|
-
|
|
247
|
-
|
|
248
|
-
const baseHandle = await repo.find(stringifyAutomergeUrl({ documentId }), {
|
|
249
|
-
signal,
|
|
250
|
-
});
|
|
251
|
-
if (!hasHeads(baseHandle.doc(), hexHeads ?? [])) {
|
|
252
|
-
return new Response("heads not found", { status: 404 });
|
|
94
|
+
handoffChannel.addEventListener("message", (event) => {
|
|
95
|
+
const data = event.data;
|
|
96
|
+
if (data?.type === "cached" || data?.type === "response") {
|
|
97
|
+
const pending = pendingHandoffs.get(data.id);
|
|
98
|
+
if (!pending) {
|
|
99
|
+
return log(`no pending handoff for id ${data.id}`);
|
|
100
|
+
}
|
|
101
|
+
pending.resolvers.resolve(data as HandoffReplyMessage);
|
|
102
|
+
} else if (data?.type === "online") {
|
|
103
|
+
// The automerge worker (re)started — re-broadcast anything still in
|
|
104
|
+
// flight so requests that raced its boot aren't stranded.
|
|
105
|
+
for (const { message } of pendingHandoffs.values()) {
|
|
106
|
+
log(`re-broadcasting handoff ${message.id} to the fresh worker`);
|
|
107
|
+
handoffChannel.postMessage(message);
|
|
108
|
+
}
|
|
253
109
|
}
|
|
254
|
-
|
|
255
|
-
|
|
256
|
-
const resolved = await resolvePath(
|
|
257
|
-
repo,
|
|
258
|
-
rootHandle,
|
|
259
|
-
path.map(decodeURIComponent)
|
|
260
|
-
);
|
|
110
|
+
});
|
|
261
111
|
|
|
262
|
-
|
|
263
|
-
|
|
264
|
-
|
|
112
|
+
function handoff(
|
|
113
|
+
request: Request,
|
|
114
|
+
handoffURL: URL
|
|
115
|
+
): Promise<HandoffReplyMessage> {
|
|
116
|
+
const id = crypto.randomUUID();
|
|
117
|
+
const resolvers = Promise.withResolvers<HandoffReplyMessage>();
|
|
118
|
+
const message: HandoffRequestMessage = {
|
|
119
|
+
id,
|
|
120
|
+
type: "request",
|
|
121
|
+
cachename,
|
|
122
|
+
request: {
|
|
123
|
+
url: request.url,
|
|
124
|
+
handoffURL: handoffURL.href,
|
|
125
|
+
headers: Object.fromEntries(request.headers.entries()),
|
|
126
|
+
method: request.method,
|
|
127
|
+
destination: request.destination,
|
|
128
|
+
referrer: request.referrer,
|
|
129
|
+
},
|
|
130
|
+
};
|
|
131
|
+
pendingHandoffs.set(id, { message, resolvers });
|
|
132
|
+
log(`broadcasting handoff request for cache ${cachename}`, message);
|
|
133
|
+
handoffChannel.postMessage(message);
|
|
134
|
+
const timeout = setTimeout(() => {
|
|
135
|
+
resolvers.reject(
|
|
136
|
+
new Error(
|
|
137
|
+
`no reply from the automerge worker after ${HANDOFF_TIMEOUT_MS}ms`
|
|
138
|
+
)
|
|
265
139
|
);
|
|
266
|
-
}
|
|
267
|
-
|
|
268
|
-
|
|
269
|
-
|
|
270
|
-
|
|
271
|
-
|
|
140
|
+
}, HANDOFF_TIMEOUT_MS);
|
|
141
|
+
return resolvers.promise.finally(() => {
|
|
142
|
+
clearTimeout(timeout);
|
|
143
|
+
pendingHandoffs.delete(id);
|
|
144
|
+
});
|
|
145
|
+
}
|
|
272
146
|
|
|
273
|
-
|
|
147
|
+
function withSpecialHeaders(response: {
|
|
148
|
+
body?: BodyInit | ReadableStream<Uint8Array> | null;
|
|
149
|
+
status?: number;
|
|
150
|
+
headers?: HeadersInit;
|
|
151
|
+
}): Response {
|
|
152
|
+
const headers = new Headers(response.headers);
|
|
274
153
|
headers.set("cross-origin-embedder-policy", "credentialless");
|
|
275
154
|
headers.set("cross-origin-resource-policy", "cross-origin");
|
|
276
|
-
|
|
277
|
-
|
|
155
|
+
return new Response(response.body ?? null, {
|
|
156
|
+
status: response.status ?? 200,
|
|
157
|
+
headers,
|
|
158
|
+
});
|
|
278
159
|
}
|
|
279
160
|
|
|
280
161
|
// ── Fetch handler ──────────────────────────────────────────────────────
|
|
@@ -285,7 +166,7 @@ self.addEventListener("fetch", (fetchEvent: FetchEvent) => {
|
|
|
285
166
|
if (request.method !== "GET") return fetchEvent.respondWith(fetch(request));
|
|
286
167
|
const url = new URL(fetchEvent.request.url);
|
|
287
168
|
|
|
288
|
-
let
|
|
169
|
+
let handoffURL: URL | undefined;
|
|
289
170
|
|
|
290
171
|
if (
|
|
291
172
|
url.hostname == self.location.hostname &&
|
|
@@ -293,8 +174,8 @@ self.addEventListener("fetch", (fetchEvent: FetchEvent) => {
|
|
|
293
174
|
url.protocol == self.location.protocol
|
|
294
175
|
) {
|
|
295
176
|
try {
|
|
296
|
-
|
|
297
|
-
log(`received special request ${
|
|
177
|
+
handoffURL = new URL(decodeURIComponent(url.pathname.slice(1)));
|
|
178
|
+
log(`received special request ${handoffURL}`);
|
|
298
179
|
} catch {}
|
|
299
180
|
}
|
|
300
181
|
|
|
@@ -304,41 +185,34 @@ self.addEventListener("fetch", (fetchEvent: FetchEvent) => {
|
|
|
304
185
|
const match = await cache.match(request);
|
|
305
186
|
|
|
306
187
|
try {
|
|
307
|
-
if (
|
|
188
|
+
if (handoffURL) {
|
|
308
189
|
if (match) {
|
|
309
|
-
log(`serving ${
|
|
310
|
-
|
|
311
|
-
headers.set("cross-origin-embedder-policy", "credentialless");
|
|
312
|
-
headers.set("cross-origin-resource-policy", "cross-origin");
|
|
313
|
-
return new Response(match.body, {
|
|
314
|
-
status: match.status,
|
|
315
|
-
headers,
|
|
316
|
-
});
|
|
190
|
+
log(`serving ${handoffURL} from cache ${cachename}`);
|
|
191
|
+
return withSpecialHeaders(match);
|
|
317
192
|
}
|
|
318
193
|
|
|
319
|
-
|
|
320
|
-
|
|
321
|
-
|
|
322
|
-
|
|
323
|
-
() =>
|
|
324
|
-
reject(
|
|
325
|
-
new Error(`resolve timeout after ${RESOLVE_TIMEOUT_MS}ms`)
|
|
326
|
-
),
|
|
327
|
-
RESOLVE_TIMEOUT_MS
|
|
328
|
-
)
|
|
329
|
-
),
|
|
330
|
-
]);
|
|
194
|
+
log(`handing ${handoffURL} off to the automerge worker`);
|
|
195
|
+
const replyPromise = handoff(request, handoffURL);
|
|
196
|
+
fetchEvent.waitUntil(replyPromise.catch(() => {}));
|
|
197
|
+
const reply = await replyPromise;
|
|
331
198
|
|
|
332
|
-
if (
|
|
333
|
-
|
|
199
|
+
if (reply.type === "response") {
|
|
200
|
+
// errors, redirects and other things that shouldn't be cached
|
|
201
|
+
log(`serving handed-off response for ${handoffURL}`, reply);
|
|
202
|
+
return withSpecialHeaders(reply.response);
|
|
334
203
|
}
|
|
335
204
|
|
|
336
|
-
|
|
337
|
-
|
|
338
|
-
|
|
205
|
+
// reply.type === "cached": the automerge worker has put the
|
|
206
|
+
// response in our cache
|
|
207
|
+
const cached = await cache.match(request);
|
|
208
|
+
if (!cached) {
|
|
209
|
+
return new Response(
|
|
210
|
+
`the automerge worker reported ${handoffURL} cached, but it has no match in ${cachename}`,
|
|
211
|
+
{ status: 500 }
|
|
212
|
+
);
|
|
339
213
|
}
|
|
340
|
-
|
|
341
|
-
return
|
|
214
|
+
log(`serving ${handoffURL} from cache ${cachename} after handoff`);
|
|
215
|
+
return withSpecialHeaders(cached);
|
|
342
216
|
} else {
|
|
343
217
|
const response = await fetch(request).catch(() => null);
|
|
344
218
|
if (response) {
|
|
@@ -363,7 +237,8 @@ self.addEventListener("fetch", (fetchEvent: FetchEvent) => {
|
|
|
363
237
|
? `${error.message}\n\n${error.stack}`
|
|
364
238
|
: String(error);
|
|
365
239
|
console.error(
|
|
366
|
-
`service worker error resolving ${request.url}${
|
|
240
|
+
`service worker error resolving ${request.url}${handoffURL ? ` (for: ${handoffURL})` : ""}`,
|
|
241
|
+
error
|
|
367
242
|
);
|
|
368
243
|
if (match) return match;
|
|
369
244
|
|