@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/src/setup.ts CHANGED
@@ -3,13 +3,17 @@ import type {
3
3
  SetupServiceWorkerOptions,
4
4
  SetupServiceWorkerResult,
5
5
  } from "./types.js";
6
+ import {
7
+ readClassicSyncServer,
8
+ DEFAULT_CLASSIC_SYNC_SERVER,
9
+ } from "./sync-config.js";
6
10
  import debug from "debug";
7
11
 
8
- const debugging = debug.enabled("patchwork:serviceworker");
12
+ const serviceWorkerDebugging = debug.enabled("patchwork:serviceworker");
13
+ const workerDebugging = debug.enabled("patchwork:automergeworker");
9
14
 
10
15
  const key = "patchworkServiceWorkerCacheVersion";
11
16
  let nextRepoChannelId = 0;
12
- let serviceWorkerInstanceId: string | undefined;
13
17
 
14
18
  function bumpServiceWorkerCacheVersion() {
15
19
  const version = new Date().valueOf().toString(36);
@@ -48,17 +52,69 @@ export function bumpServiceWorkerCache(
48
52
 
49
53
  function configureServiceWorker(sw: ServiceWorker | null) {
50
54
  if (!sw) return;
51
- sw.postMessage({ type: "debug", debug: debugging });
55
+ sw.postMessage({ type: "debug", debug: serviceWorkerDebugging });
52
56
  const cachename = getServiceWorkerCacheVersion();
53
57
  if (cachename) sw.postMessage({ type: "cachename", cachename });
54
58
  }
55
59
 
56
- function updateServiceWorkerInstanceId(next: unknown) {
57
- if (typeof next !== "string") return false;
58
- const changed =
59
- serviceWorkerInstanceId != null && serviceWorkerInstanceId !== next;
60
- serviceWorkerInstanceId = next;
61
- return changed;
60
+ // ── The automerge worker ───────────────────────────────────────────────
61
+ // The automerge repo lives in a SharedWorker (not the service worker). One
62
+ // instance is shared by every tab and lives exactly as long as any tab
63
+ // does, so there's no keepalive ping and no restart detection: if we're
64
+ // alive, it's alive. Repo sync ports are passed to it over its connect
65
+ // port; it talks to the service worker over a BroadcastChannel.
66
+
67
+ let automergeWorkerPath = "/automerge-worker.js";
68
+ let automergeWorker: SharedWorker | undefined;
69
+
70
+ function getAutomergeWorker(): SharedWorker {
71
+ if (!automergeWorker) {
72
+ automergeWorker = new SharedWorker(automergeWorkerPath, {
73
+ name: "patchwork-automerge",
74
+ type: "module",
75
+ });
76
+ // Control replies (port-ready &c) come back on this port, so it needs
77
+ // start() — we listen with addEventListener, not onmessage.
78
+ automergeWorker.port.start();
79
+ automergeWorker.port.postMessage({ type: "debug", debug: workerDebugging });
80
+ }
81
+ return automergeWorker;
82
+ }
83
+
84
+ export function connectClassicSync(
85
+ server: string = readClassicSyncServer()
86
+ ): Promise<void> {
87
+ const url = server.trim() || DEFAULT_CLASSIC_SYNC_SERVER;
88
+ if (!/^wss?:\/\//.test(url)) {
89
+ return Promise.reject(
90
+ new Error(`invalid classic sync server URL: ${server}`)
91
+ );
92
+ }
93
+
94
+ const worker = getAutomergeWorker();
95
+ const { port1, port2 } = new MessageChannel();
96
+ return new Promise((resolve, reject) => {
97
+ const timeout = setTimeout(() => {
98
+ port1.close();
99
+ reject(new Error("connect-classic-sync timeout"));
100
+ }, 30_000);
101
+ port1.onmessage = (event) => {
102
+ clearTimeout(timeout);
103
+ port1.close();
104
+ if (event.data?.type === "connect-classic-sync-ready") {
105
+ resolve();
106
+ } else {
107
+ reject(
108
+ new Error(
109
+ event.data?.error ?? "connect-classic-sync failed"
110
+ )
111
+ );
112
+ }
113
+ };
114
+ worker.port.postMessage({ type: "connect-classic-sync", server: url }, [
115
+ port2,
116
+ ]);
117
+ });
62
118
  }
63
119
 
64
120
  /** Wait for a registration to have an active worker */
@@ -74,122 +130,69 @@ function waitForActive(reg: ServiceWorkerRegistration): Promise<ServiceWorker> {
74
130
  });
75
131
  }
76
132
 
77
- async function openRepoChannel(): Promise<{
78
- port: MessagePort;
79
- workerInstanceChanged: boolean;
80
- }> {
81
- const controller = navigator.serviceWorker.controller;
82
- if (!controller) {
83
- throw new Error("no service worker controller");
84
- }
133
+ async function openRepoChannel(): Promise<MessagePort> {
134
+ const worker = getAutomergeWorker();
85
135
 
86
- // Send a MessagePort so the SW's repo can sync with clients, and wait for
87
- // the SW to confirm its repo is constructed before returning. The
136
+ // Send a MessagePort so the worker's repo can sync with this tab, and wait
137
+ // for the worker to confirm its repo is constructed before returning. The
88
138
  // MessageChannel adapter's whenReady() force-resolves after 100ms regardless
89
139
  // of the other end's state, so it can't be used as a real readiness signal
90
- // on first install (when the SW still has to fetch wasm and build its repo).
140
+ // on first boot (when the worker still has to fetch wasm and build its repo).
91
141
  const id = ++nextRepoChannelId;
92
- let workerInstanceChanged = false;
93
142
  const { port1, port2 } = new MessageChannel();
94
- const swReady = new Promise<void>((resolve, reject) => {
143
+ const workerReady = new Promise<void>((resolve, reject) => {
95
144
  let timeout: ReturnType<typeof setTimeout>;
96
145
  const cleanup = () => {
97
146
  clearTimeout(timeout);
98
- navigator.serviceWorker.removeEventListener("message", listener);
147
+ worker.port.removeEventListener("message", listener);
99
148
  };
100
149
  const listener = (event: MessageEvent) => {
101
- if (event.data?.id != null && event.data.id !== id) return;
150
+ if (event.data?.id !== id) return;
102
151
  if (event.data?.type === "port-ready") {
103
- workerInstanceChanged = updateServiceWorkerInstanceId(
104
- event.data.workerInstanceId
105
- );
106
152
  cleanup();
107
153
  resolve();
108
154
  } else if (event.data?.type === "port-failed") {
109
- workerInstanceChanged = updateServiceWorkerInstanceId(
110
- event.data.workerInstanceId
111
- );
112
155
  cleanup();
113
- reject(new Error(`service worker init failed: ${event.data.error}`));
156
+ reject(new Error(`automerge worker init failed: ${event.data.error}`));
114
157
  }
115
158
  };
116
- navigator.serviceWorker.addEventListener("message", listener);
117
- // Failsafe: don't block boot forever if the SW never replies. Surface the
118
- // issue and let the rest of the site come up rather than hanging on a
159
+ worker.port.addEventListener("message", listener);
160
+ // Failsafe: don't block boot forever if the worker never replies. Surface
161
+ // the issue and let the rest of the site come up rather than hanging on a
119
162
  // blank page.
120
163
  timeout = setTimeout(() => {
121
164
  cleanup();
122
- reject(new Error("service worker port-ready timeout"));
165
+ reject(new Error("automerge worker port-ready timeout"));
123
166
  }, 30_000);
124
167
  });
125
- controller.postMessage({ type: "port", id }, [port2]);
168
+ worker.port.postMessage({ type: "port", id }, [port2]);
126
169
  try {
127
- await swReady;
170
+ await workerReady;
128
171
  } catch (err) {
129
172
  console.warn(
130
- "proceeding without SW ready ack:",
173
+ "proceeding without worker ready ack:",
131
174
  err instanceof Error ? err.message : err
132
175
  );
133
176
  }
134
- return { port: port1, workerInstanceChanged };
177
+ return port1;
178
+ }
179
+
180
+ /** Open a fresh repo sync port to the automerge worker (dev console). */
181
+ function getRepoChannel(): MessagePort {
182
+ const worker = getAutomergeWorker();
183
+ const { port1, port2 } = new MessageChannel();
184
+ worker.port.postMessage({ type: "port", id: ++nextRepoChannelId }, [port2]);
185
+ return port1;
135
186
  }
136
187
 
137
188
  export default async function setupServiceWorker(
138
189
  options?: SetupServiceWorkerOptions
139
190
  ): Promise<SetupServiceWorkerResult> {
140
- const repoChannelListeners = new Set<ServiceWorkerRepoChannelListener>();
141
- let reconnectPromise: Promise<void> | null = null;
191
+ if (options?.workerPath) automergeWorkerPath = options.workerPath;
142
192
 
143
- const reconnectRepoChannels = (reason: string) => {
144
- if (reconnectPromise) return reconnectPromise;
145
- reconnectPromise = (async () => {
146
- console.info(
147
- `%cservice worker ${reason}, reconnecting repo channels...`,
148
- "color: pink; font-weight: bold"
149
- );
150
- configureServiceWorker(navigator.serviceWorker.controller);
151
- for (const listener of repoChannelListeners) {
152
- try {
153
- const { port } = await openRepoChannel();
154
- await listener(port);
155
- } catch (err) {
156
- console.error("service worker repo channel listener failed", err);
157
- }
158
- }
159
- })().finally(() => {
160
- reconnectPromise = null;
161
- });
162
- return reconnectPromise;
163
- };
164
-
165
- const pingServiceWorker = async () => {
166
- const controller = navigator.serviceWorker.controller;
167
- if (!controller) return;
168
- const { port1, port2 } = new MessageChannel();
169
- const pong = new Promise<unknown>((resolve, reject) => {
170
- const timeout = setTimeout(() => {
171
- port1.close();
172
- reject(new Error("service worker pong timeout"));
173
- }, 5_000);
174
- port1.onmessage = (event) => {
175
- clearTimeout(timeout);
176
- port1.close();
177
- resolve(event.data?.workerInstanceId);
178
- };
179
- });
180
- controller.postMessage({ type: "ping" }, [port2]);
181
- try {
182
- const restarted = updateServiceWorkerInstanceId(await pong);
183
- if (restarted) {
184
- await reconnectRepoChannels("restarted");
185
- }
186
- } catch (err) {
187
- console.warn(
188
- "service worker ping failed:",
189
- err instanceof Error ? err.message : err
190
- );
191
- }
192
- };
193
+ // Start the automerge worker right away so it boots (wasm, repo) while the
194
+ // service worker installs.
195
+ getAutomergeWorker();
193
196
 
194
197
  const path = options?.path ?? "/service-worker.js";
195
198
  // No controller at this point means the page loaded without a service
@@ -216,20 +219,10 @@ export default async function setupServiceWorker(
216
219
  });
217
220
  }
218
221
 
219
- // Keepalive Chromium idles out service workers after ~30s of inactivity,
220
- // which tears down the in-memory Repo and forces a cold restart on the next
221
- // fetch. Ping through a MessageChannel so we can detect when a restarted SW
222
- // has a new in-memory Repo and reconnect all repo channels.
223
- setInterval(() => {
224
- void pingServiceWorker();
225
- }, 20_000);
226
-
227
- // Reconnect on future SW updates (added after setup so the initial
228
- // activation doesn't notify before callers subscribe).
229
- navigator.serviceWorker.addEventListener("controllerchange", function () {
230
- void reconnectRepoChannels("took control").catch((err) => {
231
- console.error("service worker reconnect failed", err);
232
- });
222
+ // A replacement service worker boots with the default cache name — re-send
223
+ // its configuration whenever a new one takes control.
224
+ navigator.serviceWorker.addEventListener("controllerchange", () => {
225
+ configureServiceWorker(navigator.serviceWorker.controller);
233
226
  });
234
227
 
235
228
  console.log(
@@ -238,19 +231,13 @@ export default async function setupServiceWorker(
238
231
  );
239
232
 
240
233
  return {
241
- async subscribeToRepoChannel(listener) {
242
- const { port, workerInstanceChanged } = await openRepoChannel();
243
- if (workerInstanceChanged) {
244
- await reconnectRepoChannels("restarted");
245
- }
246
- repoChannelListeners.add(listener);
247
- try {
248
- await listener(port);
249
- } catch (err) {
250
- repoChannelListeners.delete(listener);
251
- throw err;
252
- }
253
- return () => repoChannelListeners.delete(listener);
234
+ connectClassicSync,
235
+ getRepoChannel,
236
+ async subscribeToRepoChannel(listener: ServiceWorkerRepoChannelListener) {
237
+ // The automerge worker outlives the page, so unlike the old in-service-
238
+ // worker repo there's nothing to reconnect: one port, handed over once.
239
+ await listener(await openRepoChannel());
240
+ return () => {};
254
241
  },
255
242
  };
256
243
  }
package/src/site.ts CHANGED
@@ -2,14 +2,14 @@
2
2
  * High-level browser-app boot sequence for a Patchwork site.
3
3
  *
4
4
  * Layers on top of {@link setupServiceWorker} (the package default export) to
5
- * construct the Repo, wire up the service-worker port, load plugins via the
5
+ * construct the Repo, wire up the automerge-worker port, load plugins via the
6
6
  * ModuleWatcher, resolve the user's account document, and hand control to the
7
7
  * configured root tool.
8
8
  *
9
9
  * This entry point pulls in DOM- and plugin-layer dependencies (patchwork
10
10
  * elements, plugins, filesystem) and is intended for use only from a browser
11
11
  * site's `main.ts`. Non-UI consumers should import the package default (which
12
- * only does SW registration and port handoff).
12
+ * only does SW registration and the automerge-worker handoff).
13
13
  */
14
14
  import {
15
15
  type DocHandle,
@@ -60,7 +60,6 @@ import * as plugins from "@inkandswitch/patchwork-plugins";
60
60
 
61
61
  import setupServiceWorker from "./setup.js";
62
62
  import type { ServiceWorkerRepoChannelListener } from "./types.js";
63
- import { SwLogReader } from "./sw-logger.js";
64
63
  import debug from "debug";
65
64
  const log = debug("patchwork:bootloader:site");
66
65
 
@@ -78,10 +77,7 @@ declare global {
78
77
  plugins: typeof plugins;
79
78
  accountDocHandle: DocHandle<AccountDoc>;
80
79
  sw: {
81
- printLogs: (n?: number) => Promise<void>;
82
- tailLogs: (n?: number) => ReturnType<typeof SwLogReader.tail>;
83
- exportLogs: () => Promise<string>;
84
- clearLogs: () => Promise<void>;
80
+ connectClassicSync: (server?: string) => Promise<void>;
85
81
  subscribeToRepoChannel: (
86
82
  listener: ServiceWorkerRepoChannelListener
87
83
  ) => Promise<() => void>;
@@ -177,24 +173,22 @@ export async function bootPatchworkSite(
177
173
  if (!sw) throw new Error("Failed to set up service worker");
178
174
 
179
175
  let hive: AutomergeRepoKeyhive | undefined;
176
+ // Get the initial automerge-worker port via subscribeToRepoChannel,
177
+ // then pass it to keyhive init which wraps it in its own network adapter.
178
+ let resolvePort!: (port: MessagePort) => void;
179
+ const portPromise = new Promise<MessagePort>((r) => {
180
+ resolvePort = r;
181
+ });
182
+ await sw.subscribeToRepoChannel(resolvePort);
183
+ const workerPort = await portPromise;
184
+
180
185
  if (config.keyhive) {
181
186
  initKeyhiveWasm();
182
187
 
183
- // Get the initial SW port via subscribeToRepoChannel, then pass it
184
- // to keyhive init which wraps it in its own network adapter.
185
- let resolvePort!: (port: MessagePort) => void;
186
- const portPromise = new Promise<MessagePort>((r) => { resolvePort = r; });
187
- sw.subscribeToRepoChannel((port) => { resolvePort(port); });
188
- const swPort = await portPromise;
189
-
190
188
  hive = await initializeAutomergeRepoKeyhive({
191
- storage: new IndexedDBStorageAdapter(
192
- `${siteName}-keyhive`
193
- ),
194
- peerIdSuffix:
195
- siteName +
196
- Math.random().toString(36).slice(2),
197
- networkAdapter: new MessageChannelNetworkAdapter(swPort),
189
+ storage: new IndexedDBStorageAdapter(`${siteName}-keyhive`),
190
+ peerIdSuffix: siteName + Math.random().toString(36).slice(2),
191
+ networkAdapter: new MessageChannelNetworkAdapter(workerPort),
198
192
  automaticArchiveIngestion: true,
199
193
  cachingMode: "periodic",
200
194
  onlyShareWithHardcodedServerPeerId: false,
@@ -210,9 +204,10 @@ export async function bootPatchworkSite(
210
204
  idFactory: hive.idFactory,
211
205
  })
212
206
  : new Repo({
207
+ network: [new MessageChannelNetworkAdapter(workerPort)],
213
208
  storage: new IndexedDBStorageAdapter(),
214
209
  async sharePolicy(peerId) {
215
- return peerId.includes("service-worker");
210
+ return peerId.includes("automerge-worker");
216
211
  },
217
212
  enableRemoteHeadsGossiping: true,
218
213
  peerId:
@@ -222,25 +217,14 @@ export async function bootPatchworkSite(
222
217
  config.remoteStorageIds ?? [DEFAULT_REMOTE_STORAGE_ID]
223
218
  );
224
219
 
220
+ await repo.networkSubsystem.whenReady();
225
221
  if (hive) {
226
- await repo.networkSubsystem.whenReady();
227
222
  (hive.networkAdapter as any).syncKeyhive?.();
228
- } else {
229
- let activeServiceWorkerPort: MessagePort | undefined;
230
- const connectServiceWorkerPort = async (port: MessagePort) => {
231
- const previousPort = activeServiceWorkerPort;
232
- activeServiceWorkerPort = port;
233
- const net = new MessageChannelNetworkAdapter(port);
234
- repo.networkSubsystem.addNetworkAdapter(net);
235
- await net.whenReady();
236
- previousPort?.close();
237
- };
238
- await sw.subscribeToRepoChannel(connectServiceWorkerPort);
239
223
  }
240
224
 
241
- installDevConsoleGlobals(repo, hive);
225
+ installDevConsoleGlobals(repo, hive, sw.getRepoChannel);
242
226
 
243
- registerRepoProviderElement(repo);
227
+ registerRepoProviderElement(repo as any);
244
228
 
245
229
  const rootElement = document.getElementById(config.rootElementId ?? "root");
246
230
  if (!rootElement) {
@@ -248,11 +232,13 @@ export async function bootPatchworkSite(
248
232
  `bootPatchworkSite: no element with id="${config.rootElementId ?? "root"}"`
249
233
  );
250
234
  }
235
+ // `<repo-provider>` sits above the root and answers `repo:handle-descriptor`
236
+ // for any view outside a remapper (resolving to the requested url unchanged).
251
237
  const repoProvider = document.createElement("repo-provider");
252
238
  rootElement.parentElement!.insertBefore(repoProvider, rootElement);
253
239
  repoProvider.appendChild(rootElement);
254
240
 
255
- registerPatchworkViewElement(hive ? { hive } : {});
241
+ registerPatchworkViewElement({ hive, repo });
256
242
 
257
243
  // The watcher is started with the site's default-tools bundle alone so that
258
244
  // `resolveAccountHandle` below has something to await on (the `account`
@@ -265,10 +251,12 @@ export async function bootPatchworkSite(
265
251
  unregisterPlugins
266
252
  );
267
253
 
268
- const accountDocHandle = await resolveAccountHandle(repo, {
254
+ const accountDocHandle = (await resolveAccountHandle(repo, {
269
255
  storageKey: config.accountStorageKey,
270
256
  hive,
271
- });
257
+ })) as DocHandle<AccountDoc>;
258
+ // TODO: something we (Orion & pvh) changed in the types made this necessary
259
+ // fix this before merging to main!
272
260
 
273
261
  window.accountDocHandle = accountDocHandle;
274
262
 
@@ -283,7 +271,7 @@ export async function bootPatchworkSite(
283
271
  plugins,
284
272
  accountDocHandle,
285
273
  sw: {
286
- ...buildSwLogApi(),
274
+ connectClassicSync: sw.connectClassicSync,
287
275
  subscribeToRepoChannel: sw.subscribeToRepoChannel,
288
276
  },
289
277
  };
@@ -321,7 +309,8 @@ function resolveDefaultModulesUrl(builtin: AutomergeUrl): AutomergeUrl {
321
309
 
322
310
  function installDevConsoleGlobals(
323
311
  repo: Repo,
324
- hive: AutomergeRepoKeyhive | undefined
312
+ hive: AutomergeRepoKeyhive | undefined,
313
+ getRepoChannel: () => MessagePort
325
314
  ): void {
326
315
  window.repo = repo;
327
316
  window.Automerge = Automerge;
@@ -329,11 +318,7 @@ function installDevConsoleGlobals(
329
318
  if (hive) {
330
319
  window.hive = hive;
331
320
  }
332
- window.getRepoChannel = () => {
333
- const { port1, port2 } = new MessageChannel();
334
- navigator.serviceWorker.controller!.postMessage({ type: "port" }, [port2]);
335
- return port1;
336
- };
321
+ window.getRepoChannel = getRepoChannel;
337
322
  }
338
323
 
339
324
  function onModuleLoaded(name: string, mod: any): void {
@@ -413,26 +398,6 @@ function logToolRegistryWhenLoaded(moduleWatcher: ModuleWatcher): void {
413
398
  });
414
399
  }
415
400
 
416
- function buildSwLogApi(): Omit<
417
- Window["patchwork"]["sw"],
418
- "subscribeToRepoChannel"
419
- > {
420
- return {
421
- printLogs: async (n = 200) => {
422
- const entries = await SwLogReader.tail(n);
423
- for (const e of entries) {
424
- const prefix = `[${e.ts}] [${e.level}]`;
425
- if (e.data !== undefined) log(prefix, e.msg, e.data);
426
- else log(prefix, e.msg);
427
- }
428
- log(`--- ${entries.length} entries ---`);
429
- },
430
- tailLogs: (n = 200) => SwLogReader.tail(n),
431
- exportLogs: () => SwLogReader.exportAll(),
432
- clearLogs: () => SwLogReader.clear(),
433
- };
434
- }
435
-
436
401
  const LOADING_STYLE_ID = "pw-bootloader-loading-styles";
437
402
  const LOADING_ELEMENT_ID = "pw-bootloader-loading";
438
403
 
@@ -0,0 +1,23 @@
1
+ /** localStorage key: optional override for the classic sync WebSocket URL. */
2
+ export const CLASSIC_SYNC_SERVER_KEY = "patchworkClassicSyncServer";
3
+
4
+ export const DEFAULT_CLASSIC_SYNC_SERVER = "wss://sync3.automerge.org";
5
+
6
+ export function readClassicSyncServer(
7
+ storage: Pick<Storage, "getItem"> = globalThis.localStorage
8
+ ): string {
9
+ const override = storage.getItem(CLASSIC_SYNC_SERVER_KEY)?.trim();
10
+ if (!override) return DEFAULT_CLASSIC_SYNC_SERVER;
11
+ if (!/^wss?:\/\//.test(override)) {
12
+ console.warn(
13
+ `ignoring invalid ${CLASSIC_SYNC_SERVER_KEY} in localStorage: ${override}; using ${DEFAULT_CLASSIC_SYNC_SERVER}`
14
+ );
15
+ return DEFAULT_CLASSIC_SYNC_SERVER;
16
+ }
17
+ return override;
18
+ }
19
+
20
+ export type ConnectClassicSyncMessage = {
21
+ type: "connect-classic-sync";
22
+ server: string;
23
+ };
package/src/types.ts CHANGED
@@ -1,9 +1,103 @@
1
+ /**
2
+ * The BroadcastChannel the service worker and the automerge shared worker
3
+ * use to hand requests off to each other. Broadcast (rather than a
4
+ * MessagePort handed from one to the other) so the two never need to be
5
+ * reintroduced when either of them restarts — and so tabs can listen in.
6
+ */
7
+ export const HANDOFF_CHANNEL = "@patchwork/handoff";
8
+
9
+ /**
10
+ * The special URL to resolve, plus enough of the {@link Request} the service
11
+ * worker is holding that the automerge worker can construct one that
12
+ * `cache.match`es it.
13
+ *
14
+ * Stale workers on either side of the channel can outlive a deploy, so the
15
+ * shape can only ever change additively: `url` must stay the http request
16
+ * URL old automerge workers decode the special URL out of, and new meaning
17
+ * goes in new fields old receivers ignore.
18
+ */
19
+ export interface HandoffRequest {
20
+ /**
21
+ * The URL of the request the service worker is holding (the encoded
22
+ * `https://…/automerge%3Aabc/…` form) — the cache key.
23
+ */
24
+ url: string;
25
+ /** the decoded special URL, e.g. `automerge:abc/some/path` */
26
+ handoffURL: string;
27
+ /**
28
+ * @deprecated A briefly-deployed shape put the special URL in `url` and
29
+ * the cache key here. Only read, never sent.
30
+ */
31
+ cacheKey?: string;
32
+ headers: Record<string, string>;
33
+ method: string;
34
+ destination: RequestDestination;
35
+ referrer: string;
36
+ }
37
+
38
+ /**
39
+ * Service worker → automerge worker: please resolve this request and put
40
+ * the response in my cache.
41
+ */
42
+ export interface HandoffRequestMessage {
43
+ id: string;
44
+ type: "request";
45
+ /** the current name of the service worker cache */
46
+ cachename: string;
47
+ request: HandoffRequest;
48
+ }
49
+
50
+ /**
51
+ * Automerge worker → service worker: the response is stored in the cache
52
+ * under the request you're holding. Serve `cache.match`.
53
+ */
54
+ export interface HandoffCachedMessage {
55
+ id: string;
56
+ type: "cached";
57
+ }
58
+
59
+ /**
60
+ * An inline response for things that shouldn't be cached: errors, redirects
61
+ * &c.
62
+ */
63
+ export interface HandoffResponse {
64
+ body?: string | Uint8Array<ArrayBuffer>;
65
+ /** defaults to 200 */
66
+ status?: number;
67
+ headers?: Record<string, string>;
68
+ }
69
+
70
+ /**
71
+ * Automerge worker → service worker: don't cache anything, serve this
72
+ * response directly.
73
+ */
74
+ export interface HandoffResponseMessage {
75
+ id: string;
76
+ type: "response";
77
+ response: HandoffResponse;
78
+ }
79
+
80
+ export type HandoffReplyMessage = HandoffCachedMessage | HandoffResponseMessage;
81
+
82
+ /**
83
+ * Automerge worker → world: broadcast once on startup so the service worker
84
+ * can re-send any handoff requests that raced the worker's boot.
85
+ */
86
+ export interface HandoffOnlineMessage {
87
+ type: "online";
88
+ }
89
+
1
90
  export type SetupServiceWorkerOptions = {
2
91
  /**
3
92
  * The public path to the service worker file.
4
93
  * Defaults to `/service-worker.js`
5
94
  */
6
95
  path?: string;
96
+ /**
97
+ * The public path to the automerge shared worker file.
98
+ * Defaults to `/automerge-worker.js`
99
+ */
100
+ workerPath?: string;
7
101
  };
8
102
 
9
103
  export type ServiceWorkerRepoChannelListener = (
@@ -11,7 +105,11 @@ export type ServiceWorkerRepoChannelListener = (
11
105
  ) => void | Promise<void>;
12
106
 
13
107
  export type SetupServiceWorkerResult = {
108
+ /** Open a classic Automerge sync WebSocket from the automerge worker. */
109
+ connectClassicSync: (server?: string) => Promise<void>;
14
110
  subscribeToRepoChannel: (
15
111
  listener: ServiceWorkerRepoChannelListener
16
112
  ) => Promise<() => void>;
113
+ /** Open a fresh repo sync port to the automerge worker (dev console). */
114
+ getRepoChannel: () => MessagePort;
17
115
  };