@inkandswitch/patchwork-bootloader 0.2.6 → 0.2.8
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 +18 -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 +99 -273
- package/dist/setup.d.ts +1 -0
- package/dist/setup.js +89 -106
- package/dist/site.d.ts +3 -7
- package/dist/site.js +26 -55
- 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/service-worker-plugin.js +25 -9
- package/package.json +19 -19
- package/src/automerge-worker.ts +647 -0
- package/src/externals.ts +0 -1
- package/src/service-worker.ts +124 -349
- package/src/setup.ts +105 -118
- package/src/site.ts +31 -66
- package/src/sync-config.ts +23 -0
- package/src/types.ts +98 -0
- 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,81 +1,26 @@
|
|
|
1
1
|
/// <reference types="service-worker-types" />
|
|
2
2
|
|
|
3
|
-
|
|
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).
|
|
4
9
|
|
|
5
|
-
// Heavy imports — marked external by the service-worker vite plugin,
|
|
6
|
-
// resolved to /packages/... URLs at build time. The SW is registered with
|
|
7
|
-
// type:"module" so the browser fetches these as regular network requests.
|
|
8
|
-
// Uses /slim to avoid top-level await (disallowed in service workers).
|
|
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";
|
|
16
|
-
|
|
17
|
-
import {
|
|
18
|
-
Repo,
|
|
19
|
-
isValidAutomergeUrl,
|
|
20
|
-
parseAutomergeUrl,
|
|
21
|
-
stringifyAutomergeUrl,
|
|
22
|
-
type AutomergeUrl,
|
|
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";
|
|
30
10
|
import {
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
type
|
|
34
|
-
} from "
|
|
35
|
-
|
|
36
|
-
declare const __SITE_NAME__: string;
|
|
37
|
-
declare const __KEYHIVE__: boolean;
|
|
38
|
-
|
|
39
|
-
// TEMPORARY: enable debug npm module in SW context (no localStorage available)
|
|
11
|
+
HANDOFF_CHANNEL,
|
|
12
|
+
type HandoffReplyMessage,
|
|
13
|
+
type HandoffRequestMessage,
|
|
14
|
+
} from "./types.js";
|
|
40
15
|
|
|
41
16
|
let cachename = "default";
|
|
42
17
|
let debugging = false;
|
|
43
|
-
const workerInstanceId = crypto.randomUUID();
|
|
44
|
-
|
|
45
|
-
const SUBDUCTION_ENDPOINTS = ["wss://subduction.sync.inkandswitch.com"];
|
|
46
|
-
const RESOLVE_TIMEOUT_MS = 30_000;
|
|
47
|
-
|
|
48
|
-
// ── Persistent logger ───────────────────────────────────────────────────
|
|
49
|
-
// Initialized eagerly so it's available for the entire SW lifetime.
|
|
50
|
-
// Access from the SW inspector console via self.printLogs(), self.tailLogs(),
|
|
51
|
-
// self.exportLogs(), self.clearLogs().
|
|
52
|
-
const slog = SwLogger.open().then((logger) => {
|
|
53
|
-
(self as any).slog = logger;
|
|
54
|
-
|
|
55
|
-
(self as any).printLogs = async (n = 200) => {
|
|
56
|
-
const entries = await logger.tail(n);
|
|
57
|
-
for (const e of entries) {
|
|
58
|
-
const prefix = `[${e.ts}] [${e.level}]`;
|
|
59
|
-
if (e.data !== undefined) {
|
|
60
|
-
console.log(prefix, e.msg, e.data);
|
|
61
|
-
} else {
|
|
62
|
-
console.log(prefix, e.msg);
|
|
63
|
-
}
|
|
64
|
-
}
|
|
65
|
-
console.log(`--- ${entries.length} entries ---`);
|
|
66
|
-
};
|
|
67
|
-
|
|
68
|
-
(self as any).tailLogs = (n = 200) => logger.tail(n);
|
|
69
|
-
(self as any).exportLogs = () => logger.exportAll();
|
|
70
|
-
(self as any).clearLogs = () => logger.clear();
|
|
71
18
|
|
|
72
|
-
|
|
73
|
-
return logger;
|
|
74
|
-
});
|
|
19
|
+
const cacheableStatuses = [200, 203, 204];
|
|
75
20
|
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
const
|
|
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;
|
|
79
24
|
|
|
80
25
|
function log(...args: any[]) {
|
|
81
26
|
if (!debugging) return;
|
|
@@ -88,7 +33,12 @@ function log(...args: any[]) {
|
|
|
88
33
|
);
|
|
89
34
|
}
|
|
90
35
|
|
|
91
|
-
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
|
+
});
|
|
92
42
|
|
|
93
43
|
async function clearOldCaches() {
|
|
94
44
|
const cacheWhitelist = [cachename];
|
|
@@ -101,201 +51,20 @@ async function clearOldCaches() {
|
|
|
101
51
|
await Promise.all(deletePromises);
|
|
102
52
|
}
|
|
103
53
|
|
|
104
|
-
self.addEventListener("activate",
|
|
105
|
-
|
|
106
|
-
|
|
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
|
+
})()
|
|
63
|
+
);
|
|
107
64
|
});
|
|
108
65
|
|
|
109
|
-
let repoHivePromise: Promise<{
|
|
110
|
-
repo: Repo;
|
|
111
|
-
hive?: AutomergeRepoKeyhiveRust;
|
|
112
|
-
}> | null = null;
|
|
113
|
-
|
|
114
|
-
const useKeyhive = typeof __KEYHIVE__ !== "undefined" && __KEYHIVE__;
|
|
115
|
-
|
|
116
|
-
function getRepoHive() {
|
|
117
|
-
if (!repoHivePromise) {
|
|
118
|
-
repoHivePromise = (async () => {
|
|
119
|
-
const logger = await slog;
|
|
120
|
-
logger.info("getRepo: starting");
|
|
121
|
-
|
|
122
|
-
logger.info("fetching wasm modules");
|
|
123
|
-
const [amWasmBuf, sdnWasmBuf] = await Promise.all([
|
|
124
|
-
fetch("/automerge.wasm?sw").then((r) => r.arrayBuffer()),
|
|
125
|
-
fetch("/subduction.wasm").then((r) => r.arrayBuffer()),
|
|
126
|
-
]);
|
|
127
|
-
initSubductionSync(new Uint8Array(sdnWasmBuf));
|
|
128
|
-
await initializeWasm(new Uint8Array(amWasmBuf));
|
|
129
|
-
logger.info("wasm initialized");
|
|
130
|
-
|
|
131
|
-
if (!useKeyhive) {
|
|
132
|
-
const signer = await WebCryptoSigner.setup();
|
|
133
|
-
|
|
134
|
-
const repo = new Repo({
|
|
135
|
-
storage: new IndexedDBStorageAdapter(),
|
|
136
|
-
signer,
|
|
137
|
-
peerId: ("service-worker-" +
|
|
138
|
-
Math.random().toString(36).slice(2)) as import("@automerge/automerge-repo/slim").PeerId,
|
|
139
|
-
async sharePolicy(peerId) {
|
|
140
|
-
return peerId.includes("storage-server");
|
|
141
|
-
},
|
|
142
|
-
enableRemoteHeadsGossiping: true,
|
|
143
|
-
subductionWebsocketEndpoints: SUBDUCTION_ENDPOINTS,
|
|
144
|
-
});
|
|
145
|
-
|
|
146
|
-
(self as any).repo = repo;
|
|
147
|
-
logger.info("repo constructed (no keyhive), waiting for network subsystem");
|
|
148
|
-
|
|
149
|
-
repo.networkSubsystem.whenReady().then(() => {
|
|
150
|
-
logger.info("repo network subsystem ready");
|
|
151
|
-
});
|
|
152
|
-
|
|
153
|
-
return { repo };
|
|
154
|
-
}
|
|
155
|
-
|
|
156
|
-
initKeyhiveWasm();
|
|
157
|
-
const keyhiveStorage = new IndexedDBStorageAdapter(
|
|
158
|
-
`${siteName}-keyhive`
|
|
159
|
-
);
|
|
160
|
-
|
|
161
|
-
// Keyhive bootstrap needs to run before Repo creation but
|
|
162
|
-
// the adapter needs the subduction instance from the Repo.
|
|
163
|
-
// A deferred promise breaks the cycle.
|
|
164
|
-
let resolveRepoSubduction!: (s: any) => void;
|
|
165
|
-
const repoSubductionPromise = new Promise((resolve) => {
|
|
166
|
-
resolveRepoSubduction = resolve;
|
|
167
|
-
});
|
|
168
|
-
|
|
169
|
-
// We use the Rust variant of Keyhive initialization to talk
|
|
170
|
-
// to the Rust keyhive-enabled subduction sync server.
|
|
171
|
-
const hive = await initializeAutomergeRepoKeyhiveRust({
|
|
172
|
-
storage: keyhiveStorage,
|
|
173
|
-
peerIdSuffix:
|
|
174
|
-
`${siteName}-worker` + Math.random().toString(36).slice(2),
|
|
175
|
-
subduction: repoSubductionPromise as any,
|
|
176
|
-
automaticArchiveIngestion: true,
|
|
177
|
-
cachingMode: "periodic",
|
|
178
|
-
});
|
|
179
|
-
|
|
180
|
-
const signer = await hive.constructSubductionSigner();
|
|
181
|
-
|
|
182
|
-
const repo = new Repo({
|
|
183
|
-
storage: new IndexedDBStorageAdapter(),
|
|
184
|
-
signer,
|
|
185
|
-
subductionWebsocketEndpoints: SUBDUCTION_ENDPOINTS,
|
|
186
|
-
peerId: hive.peerId,
|
|
187
|
-
enableRemoteHeadsGossiping: true,
|
|
188
|
-
idFactory: hive.idFactory,
|
|
189
|
-
//network: [new WebSocketClientAdapter("wss://sync3.automerge.org")],
|
|
190
|
-
});
|
|
191
|
-
|
|
192
|
-
repo.subduction.then(resolveRepoSubduction);
|
|
193
|
-
|
|
194
|
-
hive.linkRepo(repo);
|
|
195
|
-
|
|
196
|
-
(self as any).repo = repo;
|
|
197
|
-
(self as any).hive = hive;
|
|
198
|
-
logger.info("repo constructed, waiting for network subsystem");
|
|
199
|
-
|
|
200
|
-
// Don't block getRepoHive() on whenReady() — the network subsystem starts
|
|
201
|
-
// with only the subduction adapter, and the MessageChannel adapter is
|
|
202
|
-
// added later via connectPort (which awaits getRepoHive). Blocking here
|
|
203
|
-
// would deadlock that path and starve the fetch handler.
|
|
204
|
-
repo.networkSubsystem.whenReady().then(() => {
|
|
205
|
-
logger.info("repo network subsystem ready");
|
|
206
|
-
});
|
|
207
|
-
|
|
208
|
-
hive.networkAdapter.whenReady().then(() => {
|
|
209
|
-
(hive.networkAdapter as any).syncKeyhive();
|
|
210
|
-
});
|
|
211
|
-
|
|
212
|
-
return { hive, repo };
|
|
213
|
-
})();
|
|
214
|
-
// If construction fails (e.g. wasm fetch errors out because the SW was
|
|
215
|
-
// terminated mid-flight), don't permanently cache the rejection — clear
|
|
216
|
-
// the slot so the next caller can retry from scratch.
|
|
217
|
-
repoHivePromise.catch(() => {
|
|
218
|
-
repoHivePromise = null;
|
|
219
|
-
});
|
|
220
|
-
}
|
|
221
|
-
return repoHivePromise;
|
|
222
|
-
}
|
|
223
|
-
|
|
224
|
-
// Connect client MessagePorts to the repo for sync
|
|
225
|
-
async function connectPort(port: MessagePort) {
|
|
226
|
-
const { hive, repo } = await getRepoHive();
|
|
227
|
-
const networkAdapter = new MessageChannelNetworkAdapter(port, { useWeakRef: true });
|
|
228
|
-
|
|
229
|
-
if (!hive) {
|
|
230
|
-
repo.networkSubsystem.addNetworkAdapter(networkAdapter);
|
|
231
|
-
return;
|
|
232
|
-
}
|
|
233
|
-
|
|
234
|
-
const onlyShareWithHardcodedServerPeerId = false;
|
|
235
|
-
const periodicallyRequestKeyhiveSync = false;
|
|
236
|
-
const keyhiveNetworkAdapter = hive.createKeyhiveNetworkAdapter(networkAdapter, onlyShareWithHardcodedServerPeerId, periodicallyRequestKeyhiveSync, 2000);
|
|
237
|
-
|
|
238
|
-
keyhiveNetworkAdapter.on("message", async (msg: any) => {
|
|
239
|
-
if ((msg.type === "sync" || msg.type === "request") && msg.documentId) {
|
|
240
|
-
const handle = repo.handles[msg.documentId];
|
|
241
|
-
if (!handle || handle.state === "unavailable") {
|
|
242
|
-
const url = `automerge:${msg.documentId}` as AutomergeUrl;
|
|
243
|
-
repo.findWithProgress(url);
|
|
244
|
-
repo.shareConfigChanged();
|
|
245
|
-
}
|
|
246
|
-
}
|
|
247
|
-
});
|
|
248
|
-
|
|
249
|
-
(keyhiveNetworkAdapter as any).on("ingest-remote", () => {
|
|
250
|
-
(hive.networkAdapter as any).syncKeyhive?.();
|
|
251
|
-
repo.shareConfigChanged();
|
|
252
|
-
});
|
|
253
|
-
|
|
254
|
-
repo.networkSubsystem.addNetworkAdapter(keyhiveNetworkAdapter);
|
|
255
|
-
}
|
|
256
|
-
|
|
257
66
|
self.addEventListener("message", async (event) => {
|
|
258
|
-
if (event.data.type == "
|
|
259
|
-
// Keepalive — Chromium idles out service workers after ~30s of inactivity.
|
|
260
|
-
// Reply via the provided port if any; the message event itself also resets
|
|
261
|
-
// the idle timer.
|
|
262
|
-
const [pongPort] = event.ports;
|
|
263
|
-
log("ping");
|
|
264
|
-
if (pongPort) {
|
|
265
|
-
pongPort.postMessage({ type: "pong", workerInstanceId });
|
|
266
|
-
log("pong");
|
|
267
|
-
pongPort.close();
|
|
268
|
-
} else if (event.source) {
|
|
269
|
-
(event.source as unknown as Client).postMessage({
|
|
270
|
-
type: "pong",
|
|
271
|
-
workerInstanceId,
|
|
272
|
-
});
|
|
273
|
-
log("pong");
|
|
274
|
-
}
|
|
275
|
-
} else if (event.data.type == "port") {
|
|
276
|
-
log("received messagechannel");
|
|
277
|
-
const [port] = event.ports;
|
|
278
|
-
const source = event.source as Client | null;
|
|
279
|
-
const id = event.data.id;
|
|
280
|
-
// event.waitUntil keeps the SW alive until the work completes. Without
|
|
281
|
-
// it, the browser can terminate the SW the moment this synchronous block
|
|
282
|
-
// returns, killing the in-flight wasm fetch.
|
|
283
|
-
(event as unknown as FetchEvent).waitUntil(
|
|
284
|
-
connectPort(port).then(
|
|
285
|
-
() => source?.postMessage({ type: "port-ready", id, workerInstanceId }),
|
|
286
|
-
(err) => {
|
|
287
|
-
console.error("connectPort failed", err);
|
|
288
|
-
// Tell the client we failed so it doesn't hang forever.
|
|
289
|
-
source?.postMessage({
|
|
290
|
-
type: "port-failed",
|
|
291
|
-
id,
|
|
292
|
-
error: String(err),
|
|
293
|
-
workerInstanceId,
|
|
294
|
-
});
|
|
295
|
-
}
|
|
296
|
-
)
|
|
297
|
-
);
|
|
298
|
-
} else if (event.data.type == "cachename") {
|
|
67
|
+
if (event.data.type == "cachename") {
|
|
299
68
|
const nextCachename = event.data.cachename;
|
|
300
69
|
if (cachename == nextCachename) {
|
|
301
70
|
return;
|
|
@@ -311,65 +80,82 @@ self.addEventListener("message", async (event) => {
|
|
|
311
80
|
}
|
|
312
81
|
});
|
|
313
82
|
|
|
314
|
-
// ──
|
|
315
|
-
|
|
316
|
-
async function resolveAutomergeUrl(automergeURL: URL): Promise<Response> {
|
|
317
|
-
const { repo } = await getRepoHive();
|
|
318
|
-
const href = automergeURL.href;
|
|
319
|
-
const [maybeAutomergeUrl, ...path] = href.split("/");
|
|
83
|
+
// ── Handoff to the automerge worker ────────────────────────────────────
|
|
320
84
|
|
|
321
|
-
|
|
322
|
-
return new Response("invalid automerge url", { status: 400 });
|
|
323
|
-
}
|
|
324
|
-
|
|
325
|
-
// Trim trailing empty path segment
|
|
326
|
-
if (path.length && !path[path.length - 1]) path.pop();
|
|
85
|
+
const handoffChannel = new BroadcastChannel(HANDOFF_CHANNEL);
|
|
327
86
|
|
|
328
|
-
|
|
329
|
-
|
|
87
|
+
type PendingHandoff = {
|
|
88
|
+
message: HandoffRequestMessage;
|
|
89
|
+
resolvers: PromiseWithResolvers<HandoffReplyMessage>;
|
|
90
|
+
};
|
|
330
91
|
|
|
331
|
-
|
|
332
|
-
const folder = await repo.find(maybeAutomergeUrl, { signal });
|
|
333
|
-
const latestHeads = folder.heads();
|
|
334
|
-
const url = stringifyAutomergeUrl({ documentId, heads: latestHeads });
|
|
335
|
-
let location = `/${encodeURIComponent(url)}`;
|
|
336
|
-
if (path.length) location += `/${path.join("/")}`;
|
|
337
|
-
return Response.redirect(location, 307);
|
|
338
|
-
}
|
|
92
|
+
const pendingHandoffs = new Map<string, PendingHandoff>();
|
|
339
93
|
|
|
340
|
-
|
|
341
|
-
|
|
342
|
-
|
|
343
|
-
|
|
344
|
-
|
|
345
|
-
|
|
346
|
-
|
|
347
|
-
|
|
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
|
+
}
|
|
348
109
|
}
|
|
349
|
-
|
|
350
|
-
|
|
351
|
-
const resolved = await resolvePath(
|
|
352
|
-
repo,
|
|
353
|
-
rootHandle,
|
|
354
|
-
path.map(decodeURIComponent)
|
|
355
|
-
);
|
|
110
|
+
});
|
|
356
111
|
|
|
357
|
-
|
|
358
|
-
|
|
359
|
-
|
|
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
|
+
)
|
|
360
139
|
);
|
|
361
|
-
}
|
|
362
|
-
|
|
363
|
-
|
|
364
|
-
|
|
365
|
-
|
|
366
|
-
|
|
140
|
+
}, HANDOFF_TIMEOUT_MS);
|
|
141
|
+
return resolvers.promise.finally(() => {
|
|
142
|
+
clearTimeout(timeout);
|
|
143
|
+
pendingHandoffs.delete(id);
|
|
144
|
+
});
|
|
145
|
+
}
|
|
367
146
|
|
|
368
|
-
|
|
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);
|
|
369
153
|
headers.set("cross-origin-embedder-policy", "credentialless");
|
|
370
154
|
headers.set("cross-origin-resource-policy", "cross-origin");
|
|
371
|
-
|
|
372
|
-
|
|
155
|
+
return new Response(response.body ?? null, {
|
|
156
|
+
status: response.status ?? 200,
|
|
157
|
+
headers,
|
|
158
|
+
});
|
|
373
159
|
}
|
|
374
160
|
|
|
375
161
|
// ── Fetch handler ──────────────────────────────────────────────────────
|
|
@@ -380,7 +166,7 @@ self.addEventListener("fetch", (fetchEvent: FetchEvent) => {
|
|
|
380
166
|
if (request.method !== "GET") return fetchEvent.respondWith(fetch(request));
|
|
381
167
|
const url = new URL(fetchEvent.request.url);
|
|
382
168
|
|
|
383
|
-
let
|
|
169
|
+
let handoffURL: URL | undefined;
|
|
384
170
|
|
|
385
171
|
if (
|
|
386
172
|
url.hostname == self.location.hostname &&
|
|
@@ -388,8 +174,8 @@ self.addEventListener("fetch", (fetchEvent: FetchEvent) => {
|
|
|
388
174
|
url.protocol == self.location.protocol
|
|
389
175
|
) {
|
|
390
176
|
try {
|
|
391
|
-
|
|
392
|
-
log(`received special request ${
|
|
177
|
+
handoffURL = new URL(decodeURIComponent(url.pathname.slice(1)));
|
|
178
|
+
log(`received special request ${handoffURL}`);
|
|
393
179
|
} catch {}
|
|
394
180
|
}
|
|
395
181
|
|
|
@@ -399,41 +185,34 @@ self.addEventListener("fetch", (fetchEvent: FetchEvent) => {
|
|
|
399
185
|
const match = await cache.match(request);
|
|
400
186
|
|
|
401
187
|
try {
|
|
402
|
-
if (
|
|
188
|
+
if (handoffURL) {
|
|
403
189
|
if (match) {
|
|
404
|
-
log(`serving ${
|
|
405
|
-
|
|
406
|
-
headers.set("cross-origin-embedder-policy", "credentialless");
|
|
407
|
-
headers.set("cross-origin-resource-policy", "cross-origin");
|
|
408
|
-
return new Response(match.body, {
|
|
409
|
-
status: match.status,
|
|
410
|
-
headers,
|
|
411
|
-
});
|
|
190
|
+
log(`serving ${handoffURL} from cache ${cachename}`);
|
|
191
|
+
return withSpecialHeaders(match);
|
|
412
192
|
}
|
|
413
193
|
|
|
414
|
-
|
|
415
|
-
|
|
416
|
-
|
|
417
|
-
|
|
418
|
-
() =>
|
|
419
|
-
reject(
|
|
420
|
-
new Error(`resolve timeout after ${RESOLVE_TIMEOUT_MS}ms`)
|
|
421
|
-
),
|
|
422
|
-
RESOLVE_TIMEOUT_MS
|
|
423
|
-
)
|
|
424
|
-
),
|
|
425
|
-
]);
|
|
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;
|
|
426
198
|
|
|
427
|
-
if (
|
|
428
|
-
|
|
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);
|
|
429
203
|
}
|
|
430
204
|
|
|
431
|
-
|
|
432
|
-
|
|
433
|
-
|
|
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
|
+
);
|
|
434
213
|
}
|
|
435
|
-
|
|
436
|
-
return
|
|
214
|
+
log(`serving ${handoffURL} from cache ${cachename} after handoff`);
|
|
215
|
+
return withSpecialHeaders(cached);
|
|
437
216
|
} else {
|
|
438
217
|
const response = await fetch(request).catch(() => null);
|
|
439
218
|
if (response) {
|
|
@@ -457,13 +236,9 @@ self.addEventListener("fetch", (fetchEvent: FetchEvent) => {
|
|
|
457
236
|
error instanceof Error
|
|
458
237
|
? `${error.message}\n\n${error.stack}`
|
|
459
238
|
: String(error);
|
|
460
|
-
|
|
461
|
-
|
|
462
|
-
|
|
463
|
-
{
|
|
464
|
-
message: error instanceof Error ? error.message : String(error),
|
|
465
|
-
stack: error instanceof Error ? error.stack : undefined,
|
|
466
|
-
}
|
|
239
|
+
console.error(
|
|
240
|
+
`service worker error resolving ${request.url}${handoffURL ? ` (for: ${handoffURL})` : ""}`,
|
|
241
|
+
error
|
|
467
242
|
);
|
|
468
243
|
if (match) return match;
|
|
469
244
|
|