@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 CHANGED
@@ -1,5 +1,19 @@
1
1
  # @inkandswitch/patchwork-bootloader
2
2
 
3
+ ## 0.2.7
4
+
5
+ ### Patch Changes
6
+
7
+ - 14bd0e2: Don't externalize @automerge/automerge-repo-react-hooks
8
+
9
+ ## 0.2.6
10
+
11
+ ### Patch Changes
12
+
13
+ - Updated dependencies [db46689]
14
+ - @inkandswitch/patchwork-providers@0.2.0
15
+ - @inkandswitch/patchwork-elements@1.0.0
16
+
3
17
  ## 0.2.5
4
18
 
5
19
  ### Patch Changes
@@ -0,0 +1 @@
1
+ export {};
@@ -0,0 +1,505 @@
1
+ // The automerge repo for a patchwork site. This runs in a SharedWorker, so
2
+ // one instance serves every tab and lives exactly as long as any tab does —
3
+ // no keepalive pings, no idle teardown.
4
+ //
5
+ // The service worker holds no repo. When it misses the cache for a special
6
+ // URL it broadcasts a HandoffRequestMessage on HANDOFF_CHANNEL; we resolve
7
+ // the automerge URL, write the response into the service worker's cache
8
+ // (keyed by a Request reconstructed to match the one it's holding), and
9
+ // reply on the same channel.
10
+ // Heavy imports — marked external by the service-worker vite plugin,
11
+ // resolved to /packages/... URLs at build time. The worker is created with
12
+ // type:"module" so the browser fetches these as regular network requests.
13
+ // Uses /slim so wasm is fetched from /automerge.wasm (emitted by the vite
14
+ // plugin) instead of bundling the ~3MB base64 string.
15
+ import { initializeWasm, hasHeads } from "@automerge/automerge/slim";
16
+ // eslint-disable-next-line
17
+ // @ts-ignore — initSync is a wasm-bindgen runtime helper not in the .d.ts
18
+ import { initSync as initSubductionSync } from "@automerge/automerge-subduction/slim";
19
+ import { WebCryptoSigner } from "@automerge/automerge-subduction/slim";
20
+ import { Repo, isValidAutomergeUrl, parseAutomergeUrl, stringifyAutomergeUrl, } from "@automerge/automerge-repo/slim";
21
+ import { resolvePath } from "@inkandswitch/patchwork-filesystem";
22
+ // Small adapters — bundled directly into the worker
23
+ import { IndexedDBStorageAdapter } from "@automerge/automerge-repo-storage-indexeddb";
24
+ import { MessageChannelNetworkAdapter } from "@automerge/automerge-repo-network-messagechannel";
25
+ import { WebSocketClientAdapter } from "@automerge/automerge-repo-network-websocket";
26
+ import { initializeAutomergeRepoKeyhiveRust, initKeyhiveWasm, } from "@automerge/automerge-repo-keyhive";
27
+ import { HANDOFF_CHANNEL, } from "./types.js";
28
+ let debugging = false;
29
+ // Sync server selection. Sub is the default. Build with KEYHIVE_SYNC_SERVER=true
30
+ // to target keyhive.sync.automerge.org.
31
+ const useKeyhiveSyncServer = typeof __KEYHIVE_SYNC_SERVER__ !== "undefined" && __KEYHIVE_SYNC_SERVER__;
32
+ // Set the correct env var for automerge_repo_keyhive if need be.
33
+ if (useKeyhiveSyncServer) {
34
+ globalThis.process = globalThis.process ?? {};
35
+ globalThis.process.env = {
36
+ ...(globalThis.process.env ?? {}),
37
+ KEYHIVE_SERVER_IDENTITY: "keyhive-sync",
38
+ };
39
+ }
40
+ // keyhive.sync.automerge.org's keyhive identity (issuer d7f41e6f…).
41
+ const KEYHIVE_SYNC_SERVER_PEER_ID = "1/Qebw9O69oH8T/ejYMhFup0tNBh69I3ytGqsmIl358=";
42
+ const KEYHIVE_SYNC_SERVER_CONTACT_CARD_JSON = '{"Rotate":{"payload":{"old":[73,163,230,244,111,233,153,119,133,211,134,237,111,36,52,131,22,50,54,144,150,45,227,235,128,36,33,217,190,198,55,75],"new":[109,115,204,144,178,114,182,238,113,124,4,139,249,76,220,44,128,104,194,68,187,184,82,241,94,145,104,198,159,122,186,43]},"issuer":[215,244,30,111,15,78,235,218,7,241,63,222,141,131,33,22,234,116,180,208,97,235,210,55,202,209,170,178,98,37,223,159],"signature":[178,64,85,76,51,199,196,151,129,14,191,53,127,191,34,223,97,238,95,109,118,179,152,17,205,188,204,177,116,166,147,231,192,201,48,137,19,214,180,45,108,104,34,8,14,63,115,139,215,142,4,179,233,89,150,218,174,168,107,23,8,109,228,6]}}';
43
+ const SUBDUCTION_ENDPOINTS = [
44
+ useKeyhiveSyncServer
45
+ ? "wss://keyhive.sync.automerge.org"
46
+ : "wss://subduction.sync.inkandswitch.com",
47
+ ];
48
+ const RESOLVE_TIMEOUT_MS = 30_000;
49
+ const DEFAULT_CLASSIC_SYNC_SERVER = "wss://sync3.automerge.org";
50
+ let classicSyncServer = DEFAULT_CLASSIC_SYNC_SERVER;
51
+ let classicSyncAdapter = null;
52
+ let classicSyncConnectPromise = null;
53
+ async function connectClassicSyncNetwork(server) {
54
+ const url = server.trim() || DEFAULT_CLASSIC_SYNC_SERVER;
55
+ if (classicSyncConnectPromise && classicSyncServer === url) {
56
+ return classicSyncConnectPromise;
57
+ }
58
+ if (classicSyncAdapter && classicSyncServer !== url) {
59
+ classicSyncAdapter.disconnect();
60
+ classicSyncAdapter = null;
61
+ classicSyncConnectPromise = null;
62
+ }
63
+ classicSyncServer = url;
64
+ classicSyncConnectPromise = (async () => {
65
+ const { repo } = await getRepoHive();
66
+ if (!classicSyncAdapter) {
67
+ classicSyncAdapter = new WebSocketClientAdapter(url);
68
+ repo.networkSubsystem.addNetworkAdapter(classicSyncAdapter);
69
+ }
70
+ await classicSyncAdapter.whenReady();
71
+ log("classic sync connected", { server: url });
72
+ })();
73
+ try {
74
+ await classicSyncConnectPromise;
75
+ }
76
+ catch (err) {
77
+ classicSyncConnectPromise = null;
78
+ throw err;
79
+ }
80
+ }
81
+ const siteName = typeof __SITE_NAME__ !== "undefined" ? __SITE_NAME__ : "tiny-patchwork";
82
+ const cacheableStatuses = [200, 203, 204, 206];
83
+ function log(...args) {
84
+ if (!debugging)
85
+ return;
86
+ console.log.call(console, `%cpatchwork:automergeworker%c\n`, `color: #ffaa00; font-weight: bold`, "color: inherit", ...args);
87
+ }
88
+ let repoHivePromise = null;
89
+ const useKeyhive = typeof __KEYHIVE__ !== "undefined" && __KEYHIVE__;
90
+ function getRepoHive() {
91
+ if (!repoHivePromise) {
92
+ repoHivePromise = (async () => {
93
+ log("getRepo: starting");
94
+ log("fetching wasm modules");
95
+ const [amWasmBuf, sdnWasmBuf] = await Promise.all([
96
+ fetch("/automerge.wasm?worker").then((r) => r.arrayBuffer()),
97
+ fetch("/subduction.wasm").then((r) => r.arrayBuffer()),
98
+ ]);
99
+ initSubductionSync(new Uint8Array(sdnWasmBuf));
100
+ await initializeWasm(new Uint8Array(amWasmBuf));
101
+ log("wasm initialized");
102
+ if (!useKeyhive) {
103
+ const signer = await WebCryptoSigner.setup();
104
+ const repo = new Repo({
105
+ storage: new IndexedDBStorageAdapter(),
106
+ signer,
107
+ peerId: ("automerge-worker-" +
108
+ Math.random()
109
+ .toString(36)
110
+ .slice(2)),
111
+ async sharePolicy(peerId) {
112
+ return peerId.includes("storage-server");
113
+ },
114
+ enableRemoteHeadsGossiping: true,
115
+ subductionWebsocketEndpoints: SUBDUCTION_ENDPOINTS,
116
+ });
117
+ self.repo = repo;
118
+ log("repo constructed (no keyhive), waiting for network subsystem");
119
+ repo.networkSubsystem.whenReady().then(() => {
120
+ log("repo network subsystem ready");
121
+ });
122
+ return { repo };
123
+ }
124
+ initKeyhiveWasm();
125
+ const keyhiveStorage = new IndexedDBStorageAdapter(`${siteName}-keyhive`);
126
+ // Keyhive bootstrap needs to run before Repo creation but
127
+ // the adapter needs the subduction instance from the Repo.
128
+ // A deferred promise breaks the cycle.
129
+ let resolveRepoSubduction;
130
+ const repoSubductionPromise = new Promise((resolve) => {
131
+ resolveRepoSubduction = resolve;
132
+ });
133
+ // We use the Rust variant of Keyhive initialization to talk
134
+ // to the Rust keyhive-enabled subduction sync server.
135
+ const hive = await initializeAutomergeRepoKeyhiveRust({
136
+ storage: keyhiveStorage,
137
+ peerIdSuffix: `${siteName}-worker` + Math.random().toString(36).slice(2),
138
+ subduction: repoSubductionPromise,
139
+ automaticArchiveIngestion: true,
140
+ cachingMode: "periodic",
141
+ ...(useKeyhiveSyncServer
142
+ ? {
143
+ serverPeerId: KEYHIVE_SYNC_SERVER_PEER_ID,
144
+ serverContactCardJson: KEYHIVE_SYNC_SERVER_CONTACT_CARD_JSON,
145
+ }
146
+ : {}),
147
+ });
148
+ const signer = await hive.constructSubductionSigner();
149
+ const repo = new Repo({
150
+ storage: new IndexedDBStorageAdapter(),
151
+ signer,
152
+ subductionWebsocketEndpoints: SUBDUCTION_ENDPOINTS,
153
+ peerId: hive.peerId,
154
+ enableRemoteHeadsGossiping: true,
155
+ idFactory: hive.idFactory,
156
+ });
157
+ repo.subduction.then(resolveRepoSubduction);
158
+ hive.linkRepo(repo);
159
+ self.repo = repo;
160
+ self.hive = hive;
161
+ log("repo constructed, waiting for network subsystem");
162
+ // Don't block getRepoHive() on whenReady() — the network subsystem starts
163
+ // with only the subduction adapter, and the MessageChannel adapter is
164
+ // added later via connectPort (which awaits getRepoHive). Blocking here
165
+ // would deadlock that path and starve the handoff handler.
166
+ repo.networkSubsystem.whenReady().then(() => {
167
+ log("repo network subsystem ready");
168
+ });
169
+ hive.networkAdapter.whenReady().then(() => {
170
+ hive.networkAdapter.syncKeyhive();
171
+ });
172
+ return { hive, repo };
173
+ })();
174
+ // If construction fails (e.g. wasm fetch errors out), don't permanently
175
+ // cache the rejection — clear the slot so the next caller can retry from
176
+ // scratch.
177
+ repoHivePromise.catch(() => {
178
+ repoHivePromise = null;
179
+ });
180
+ }
181
+ return repoHivePromise;
182
+ }
183
+ function dropRepoChannel(repo, channel) {
184
+ // removeNetworkAdapter pulls the adapter out of networkSubsystem.adapters and
185
+ // calls adapter.disconnect(), which (for the MessageChannel adapter) emits the
186
+ // "close"/"peer-disconnected" events that also clear #adaptersByPeer.
187
+ try {
188
+ repo.networkSubsystem.removeNetworkAdapter(channel.adapter);
189
+ }
190
+ catch (err) {
191
+ console.error("removeNetworkAdapter failed", err);
192
+ }
193
+ // Belt and braces for the keyhive path, where the registered adapter is a
194
+ // wrapper: make sure the underlying port is disconnected and closed too.
195
+ try {
196
+ channel.mcAdapter.disconnect();
197
+ }
198
+ catch { }
199
+ try {
200
+ channel.port.close();
201
+ }
202
+ catch { }
203
+ }
204
+ async function dropConnection(connection) {
205
+ if (!connection.channels.size || !repoHivePromise)
206
+ return;
207
+ const { repo } = await getRepoHive();
208
+ log(`tab gone — removing ${connection.channels.size} network adapter(s)`);
209
+ for (const channel of connection.channels) {
210
+ dropRepoChannel(repo, channel);
211
+ }
212
+ connection.channels.clear();
213
+ }
214
+ // Connect client MessagePorts to the repo for sync
215
+ async function connectPort(port, connection) {
216
+ const { hive, repo } = await getRepoHive();
217
+ const networkAdapter = new MessageChannelNetworkAdapter(port, {
218
+ useWeakRef: true,
219
+ });
220
+ const track = (adapter) => {
221
+ connection.channels.add({ adapter, mcAdapter: networkAdapter, port });
222
+ };
223
+ if (!hive) {
224
+ repo.networkSubsystem.addNetworkAdapter(networkAdapter);
225
+ track(networkAdapter);
226
+ return;
227
+ }
228
+ const onlyShareWithHardcodedServerPeerId = false;
229
+ const periodicallyRequestKeyhiveSync = false;
230
+ const keyhiveNetworkAdapter = hive.createKeyhiveNetworkAdapter(networkAdapter, onlyShareWithHardcodedServerPeerId, periodicallyRequestKeyhiveSync, 2000);
231
+ keyhiveNetworkAdapter.on("message", async (msg) => {
232
+ if ((msg.type === "sync" || msg.type === "request") && msg.documentId) {
233
+ const handle = repo.handles[msg.documentId];
234
+ if (!handle || handle.state === "unavailable") {
235
+ const url = `automerge:${msg.documentId}`;
236
+ repo.findWithProgress(url);
237
+ repo.shareConfigChanged();
238
+ }
239
+ }
240
+ });
241
+ keyhiveNetworkAdapter.on("ingest-remote", () => {
242
+ hive.networkAdapter.syncKeyhive?.();
243
+ repo.shareConfigChanged();
244
+ });
245
+ repo.networkSubsystem.addNetworkAdapter(keyhiveNetworkAdapter);
246
+ track(keyhiveNetworkAdapter);
247
+ }
248
+ function handleControlMessage(event, controlPort, connection) {
249
+ const data = event.data;
250
+ if (data?.type === "port") {
251
+ log("received repo channel");
252
+ const [repoPort] = event.ports;
253
+ const id = data.id;
254
+ connectPort(repoPort, connection).then(() => controlPort.postMessage({ type: "port-ready", id }), (err) => {
255
+ console.error("connectPort failed", err);
256
+ // Tell the client we failed so it doesn't hang forever.
257
+ controlPort.postMessage({
258
+ type: "port-failed",
259
+ id,
260
+ error: String(err),
261
+ });
262
+ });
263
+ }
264
+ else if (data?.type === "debug") {
265
+ debugging = data.debug;
266
+ log("automerge worker debugging enabled");
267
+ }
268
+ else if (data?.type === "connect-classic-sync") {
269
+ const [replyPort] = event.ports;
270
+ const server = typeof data.server === "string"
271
+ ? data.server
272
+ : DEFAULT_CLASSIC_SYNC_SERVER;
273
+ connectClassicSyncNetwork(server)
274
+ .then(() => {
275
+ replyPort?.postMessage({ type: "connect-classic-sync-ready" });
276
+ replyPort?.close();
277
+ log("classic sync connected on demand", { server });
278
+ })
279
+ .catch((err) => {
280
+ console.error("connectClassicSyncNetwork failed", err);
281
+ replyPort?.postMessage({
282
+ type: "connect-classic-sync-failed",
283
+ error: String(err),
284
+ });
285
+ replyPort?.close();
286
+ });
287
+ }
288
+ }
289
+ self.addEventListener("connect", (event) => {
290
+ const controlPort = event.ports[0];
291
+ const connection = { channels: new Set() };
292
+ controlPort.addEventListener("message", (messageEvent) => {
293
+ handleControlMessage(messageEvent, controlPort, connection);
294
+ });
295
+ // Fires when the owning page is destroyed. Browsers without the close
296
+ // event fall back to the adapters' lazy useWeakRef cleanup.
297
+ controlPort.addEventListener("close", () => {
298
+ void dropConnection(connection);
299
+ });
300
+ controlPort.start();
301
+ });
302
+ // ── Automerge URL resolution ───────────────────────────────────────────
303
+ /**
304
+ * Wait for the requested heads to appear in the handle's local history —
305
+ * they may still be syncing toward us when the request lands. Resolves
306
+ * false if the signal aborts before they arrive.
307
+ */
308
+ function waitForHeads(handle, hexHeads, signal) {
309
+ if (hasHeads(handle.doc(), hexHeads))
310
+ return Promise.resolve(true);
311
+ if (signal.aborted)
312
+ return Promise.resolve(false);
313
+ return new Promise((resolve) => {
314
+ const check = () => {
315
+ if (!hasHeads(handle.doc(), hexHeads))
316
+ return;
317
+ cleanup();
318
+ resolve(true);
319
+ };
320
+ const onAbort = () => {
321
+ cleanup();
322
+ resolve(false);
323
+ };
324
+ const cleanup = () => {
325
+ handle.off("heads-changed", check);
326
+ signal.removeEventListener("abort", onAbort);
327
+ };
328
+ handle.on("heads-changed", check);
329
+ signal.addEventListener("abort", onAbort);
330
+ // The heads may have landed between the synchronous check above and
331
+ // subscribing.
332
+ check();
333
+ });
334
+ }
335
+ async function resolveAutomergeUrl(automergeURL) {
336
+ const { repo } = await getRepoHive();
337
+ const href = automergeURL.href;
338
+ const [maybeAutomergeUrl, ...path] = href.split("/");
339
+ if (!isValidAutomergeUrl(maybeAutomergeUrl)) {
340
+ return new Response("invalid automerge url", { status: 400 });
341
+ }
342
+ // Trim trailing empty path segment
343
+ if (path.length && !path[path.length - 1])
344
+ path.pop();
345
+ const { heads, hexHeads, documentId } = parseAutomergeUrl(maybeAutomergeUrl);
346
+ const signal = AbortSignal.timeout(RESOLVE_TIMEOUT_MS);
347
+ if (!heads) {
348
+ const folder = await repo.find(maybeAutomergeUrl, { signal });
349
+ const latestHeads = folder.heads();
350
+ const url = stringifyAutomergeUrl({ documentId, heads: latestHeads });
351
+ let location = `/${encodeURIComponent(url)}`;
352
+ if (path.length)
353
+ location += `/${path.join("/")}`;
354
+ return Response.redirect(location, 307);
355
+ }
356
+ // Load by documentId only so we can verify the requested heads are actually
357
+ // in our local history. repo.find with a heads-bearing URL returns a view
358
+ // at those heads, which silently materializes garbage if we never synced them.
359
+ const baseHandle = await repo.find(stringifyAutomergeUrl({ documentId }), {
360
+ signal,
361
+ });
362
+ // The heads may not have synced to us yet — give them the rest of the
363
+ // resolve window to arrive before giving up.
364
+ if (!(await waitForHeads(baseHandle, hexHeads ?? [], signal))) {
365
+ return new Response("heads not found", { status: 404 });
366
+ }
367
+ const rootHandle = baseHandle.view(heads);
368
+ const resolved = await resolvePath(repo, rootHandle, path.map(decodeURIComponent));
369
+ if (!resolved) {
370
+ throw new Error(`couldn't resolve ${path.join("/")} in folder at ${maybeAutomergeUrl}`);
371
+ }
372
+ const body = resolved.content instanceof Uint8Array
373
+ ? new Uint8Array(resolved.content)
374
+ : resolved.content;
375
+ const headers = new Headers({ "content-type": resolved.type });
376
+ headers.set("cross-origin-embedder-policy", "credentialless");
377
+ headers.set("cross-origin-resource-policy", "cross-origin");
378
+ return new Response(body, { status: 200, headers });
379
+ }
380
+ // ── Handoff: resolve special URLs for the service worker ──────────────
381
+ const handoffChannel = new BroadcastChannel(HANDOFF_CHANNEL);
382
+ /**
383
+ * Pull the special URL out of a handoff request, whichever generation of
384
+ * service worker sent it. Returns null rather than throwing — a stale
385
+ * worker on the other end of the channel can send us anything.
386
+ */
387
+ function parseHandoffhandoffURL(request) {
388
+ try {
389
+ // The service worker already decoded the special URL out of the
390
+ // request it's holding and sends it alongside.
391
+ if (request.handoffURL)
392
+ return new URL(request.handoffURL);
393
+ // TODO(backcompat): a briefly-deployed shape sent the special URL in
394
+ // request.url and the http URL in cacheKey.
395
+ if (request.cacheKey)
396
+ return new URL(request.url);
397
+ // TODO(backcompat): older service workers send only the http URL,
398
+ // special URL still URI-encoded in its pathname.
399
+ return new URL(decodeURIComponent(new URL(request.url).pathname.slice(1)));
400
+ }
401
+ catch {
402
+ return null;
403
+ }
404
+ }
405
+ async function handleHandoffRequest(message) {
406
+ const { id, cachename, request } = message;
407
+ const handoffURL = parseHandoffhandoffURL(request);
408
+ if (!handoffURL) {
409
+ console.error(`automerge worker couldn't parse a special url out of handoff request`, request);
410
+ handoffChannel.postMessage({
411
+ id,
412
+ type: "response",
413
+ response: {
414
+ status: 400,
415
+ body: `couldn't parse a special url out of ${request.url}`,
416
+ headers: { "content-type": "text/plain" },
417
+ },
418
+ });
419
+ return;
420
+ }
421
+ if (handoffURL.protocol != "automerge:") {
422
+ // This worker only resolves automerge: URLs. Other handlers may be
423
+ // listening on the channel for other schemes — stay quiet rather than
424
+ // clobbering their reply with an error.
425
+ return log(`ignoring handoff ${id} for non-automerge url ${handoffURL}`);
426
+ }
427
+ let response;
428
+ try {
429
+ log(`resolving handoff ${id} for ${handoffURL}`);
430
+ response = await Promise.race([
431
+ resolveAutomergeUrl(handoffURL),
432
+ new Promise((_, reject) => setTimeout(() => reject(new Error(`resolve timeout after ${RESOLVE_TIMEOUT_MS}ms`)), RESOLVE_TIMEOUT_MS)),
433
+ ]);
434
+ }
435
+ catch (error) {
436
+ const body = error instanceof Error
437
+ ? `${error.message}\n\n${error.stack}`
438
+ : String(error);
439
+ console.error(`automerge worker error resolving ${request.url}`, error);
440
+ handoffChannel.postMessage({
441
+ id,
442
+ type: "response",
443
+ response: {
444
+ status: 500,
445
+ body,
446
+ headers: { "content-type": "text/plain" },
447
+ },
448
+ });
449
+ return;
450
+ }
451
+ try {
452
+ if (cacheableStatuses.includes(response.status)) {
453
+ // Reconstruct the request the service worker is holding so the entry
454
+ // matches on its cache.match. (destination isn't constructible, but it
455
+ // doesn't participate in cache matching.) request.url is the http URL
456
+ // the SW is holding except in the briefly-deployed cacheKey shape.
457
+ const cacheKey = new Request(request.cacheKey ?? request.url, {
458
+ method: request.method,
459
+ headers: request.headers,
460
+ referrer: request.referrer,
461
+ });
462
+ const cache = await caches.open(cachename);
463
+ await cache.put(cacheKey, response);
464
+ log(`cached ${cacheKey.url} in ${cachename}`);
465
+ handoffChannel.postMessage({
466
+ id,
467
+ type: "cached",
468
+ });
469
+ }
470
+ else {
471
+ // Errors, redirects &c — things that shouldn't be cached — go back
472
+ // inline for the service worker to serve directly.
473
+ log(`responding inline to ${request.url} with ${response.status}`);
474
+ handoffChannel.postMessage({
475
+ id,
476
+ type: "response",
477
+ response: {
478
+ status: response.status,
479
+ headers: Object.fromEntries(response.headers.entries()),
480
+ body: response.body ? await response.text() : undefined,
481
+ },
482
+ });
483
+ }
484
+ }
485
+ catch (error) {
486
+ console.error(`automerge worker failed to reply for ${request.url}`, error);
487
+ handoffChannel.postMessage({
488
+ id,
489
+ type: "response",
490
+ response: {
491
+ status: 500,
492
+ body: String(error),
493
+ headers: { "content-type": "text/plain" },
494
+ },
495
+ });
496
+ }
497
+ }
498
+ handoffChannel.addEventListener("message", (event) => {
499
+ if (event.data?.type === "request") {
500
+ void handleHandoffRequest(event.data);
501
+ }
502
+ });
503
+ // Announce ourselves so the service worker can re-broadcast any handoff
504
+ // requests that were sent while we were still booting.
505
+ handoffChannel.postMessage({ type: "online" });
package/dist/externals.js CHANGED
@@ -9,7 +9,6 @@ const externals = [
9
9
  "@automerge/automerge-repo-network-messagechannel",
10
10
  "@automerge/automerge-repo-storage-indexeddb",
11
11
  "@automerge/automerge-repo-keyhive",
12
- "@automerge/automerge-repo-react-hooks",
13
12
  "@automerge/automerge-repo-network-messagechannel",
14
13
  "@automerge/automerge-repo-storage-indexeddb",
15
14
  "@automerge/automerge-subduction",