@inkandswitch/patchwork-bootloader 0.2.6 → 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/dist/setup.js CHANGED
@@ -1,8 +1,9 @@
1
+ import { readClassicSyncServer, DEFAULT_CLASSIC_SYNC_SERVER, } from "./sync-config.js";
1
2
  import debug from "debug";
2
- const debugging = debug.enabled("patchwork:serviceworker");
3
+ const serviceWorkerDebugging = debug.enabled("patchwork:serviceworker");
4
+ const workerDebugging = debug.enabled("patchwork:automergeworker");
3
5
  const key = "patchworkServiceWorkerCacheVersion";
4
6
  let nextRepoChannelId = 0;
5
- let serviceWorkerInstanceId;
6
7
  function bumpServiceWorkerCacheVersion() {
7
8
  const version = new Date().valueOf().toString(36);
8
9
  localStorage.setItem(key, version);
@@ -34,17 +35,58 @@ window.bumpServiceWorkerCache = bumpServiceWorkerCache;
34
35
  function configureServiceWorker(sw) {
35
36
  if (!sw)
36
37
  return;
37
- sw.postMessage({ type: "debug", debug: debugging });
38
+ sw.postMessage({ type: "debug", debug: serviceWorkerDebugging });
38
39
  const cachename = getServiceWorkerCacheVersion();
39
40
  if (cachename)
40
41
  sw.postMessage({ type: "cachename", cachename });
41
42
  }
42
- function updateServiceWorkerInstanceId(next) {
43
- if (typeof next !== "string")
44
- return false;
45
- const changed = serviceWorkerInstanceId != null && serviceWorkerInstanceId !== next;
46
- serviceWorkerInstanceId = next;
47
- return changed;
43
+ // ── The automerge worker ───────────────────────────────────────────────
44
+ // The automerge repo lives in a SharedWorker (not the service worker). One
45
+ // instance is shared by every tab and lives exactly as long as any tab
46
+ // does, so there's no keepalive ping and no restart detection: if we're
47
+ // alive, it's alive. Repo sync ports are passed to it over its connect
48
+ // port; it talks to the service worker over a BroadcastChannel.
49
+ let automergeWorkerPath = "/automerge-worker.js";
50
+ let automergeWorker;
51
+ function getAutomergeWorker() {
52
+ if (!automergeWorker) {
53
+ automergeWorker = new SharedWorker(automergeWorkerPath, {
54
+ name: "patchwork-automerge",
55
+ type: "module",
56
+ });
57
+ // Control replies (port-ready &c) come back on this port, so it needs
58
+ // start() — we listen with addEventListener, not onmessage.
59
+ automergeWorker.port.start();
60
+ automergeWorker.port.postMessage({ type: "debug", debug: workerDebugging });
61
+ }
62
+ return automergeWorker;
63
+ }
64
+ export function connectClassicSync(server = readClassicSyncServer()) {
65
+ const url = server.trim() || DEFAULT_CLASSIC_SYNC_SERVER;
66
+ if (!/^wss?:\/\//.test(url)) {
67
+ return Promise.reject(new Error(`invalid classic sync server URL: ${server}`));
68
+ }
69
+ const worker = getAutomergeWorker();
70
+ const { port1, port2 } = new MessageChannel();
71
+ return new Promise((resolve, reject) => {
72
+ const timeout = setTimeout(() => {
73
+ port1.close();
74
+ reject(new Error("connect-classic-sync timeout"));
75
+ }, 30_000);
76
+ port1.onmessage = (event) => {
77
+ clearTimeout(timeout);
78
+ port1.close();
79
+ if (event.data?.type === "connect-classic-sync-ready") {
80
+ resolve();
81
+ }
82
+ else {
83
+ reject(new Error(event.data?.error ?? "connect-classic-sync failed"));
84
+ }
85
+ };
86
+ worker.port.postMessage({ type: "connect-classic-sync", server: url }, [
87
+ port2,
88
+ ]);
89
+ });
48
90
  }
49
91
  /** Wait for a registration to have an active worker */
50
92
  function waitForActive(reg) {
@@ -61,106 +103,63 @@ function waitForActive(reg) {
61
103
  });
62
104
  }
63
105
  async function openRepoChannel() {
64
- const controller = navigator.serviceWorker.controller;
65
- if (!controller) {
66
- throw new Error("no service worker controller");
67
- }
68
- // Send a MessagePort so the SW's repo can sync with clients, and wait for
69
- // the SW to confirm its repo is constructed before returning. The
106
+ const worker = getAutomergeWorker();
107
+ // Send a MessagePort so the worker's repo can sync with this tab, and wait
108
+ // for the worker to confirm its repo is constructed before returning. The
70
109
  // MessageChannel adapter's whenReady() force-resolves after 100ms regardless
71
110
  // of the other end's state, so it can't be used as a real readiness signal
72
- // on first install (when the SW still has to fetch wasm and build its repo).
111
+ // on first boot (when the worker still has to fetch wasm and build its repo).
73
112
  const id = ++nextRepoChannelId;
74
- let workerInstanceChanged = false;
75
113
  const { port1, port2 } = new MessageChannel();
76
- const swReady = new Promise((resolve, reject) => {
114
+ const workerReady = new Promise((resolve, reject) => {
77
115
  let timeout;
78
116
  const cleanup = () => {
79
117
  clearTimeout(timeout);
80
- navigator.serviceWorker.removeEventListener("message", listener);
118
+ worker.port.removeEventListener("message", listener);
81
119
  };
82
120
  const listener = (event) => {
83
- if (event.data?.id != null && event.data.id !== id)
121
+ if (event.data?.id !== id)
84
122
  return;
85
123
  if (event.data?.type === "port-ready") {
86
- workerInstanceChanged = updateServiceWorkerInstanceId(event.data.workerInstanceId);
87
124
  cleanup();
88
125
  resolve();
89
126
  }
90
127
  else if (event.data?.type === "port-failed") {
91
- workerInstanceChanged = updateServiceWorkerInstanceId(event.data.workerInstanceId);
92
128
  cleanup();
93
- reject(new Error(`service worker init failed: ${event.data.error}`));
129
+ reject(new Error(`automerge worker init failed: ${event.data.error}`));
94
130
  }
95
131
  };
96
- navigator.serviceWorker.addEventListener("message", listener);
97
- // Failsafe: don't block boot forever if the SW never replies. Surface the
98
- // issue and let the rest of the site come up rather than hanging on a
132
+ worker.port.addEventListener("message", listener);
133
+ // Failsafe: don't block boot forever if the worker never replies. Surface
134
+ // the issue and let the rest of the site come up rather than hanging on a
99
135
  // blank page.
100
136
  timeout = setTimeout(() => {
101
137
  cleanup();
102
- reject(new Error("service worker port-ready timeout"));
138
+ reject(new Error("automerge worker port-ready timeout"));
103
139
  }, 30_000);
104
140
  });
105
- controller.postMessage({ type: "port", id }, [port2]);
141
+ worker.port.postMessage({ type: "port", id }, [port2]);
106
142
  try {
107
- await swReady;
143
+ await workerReady;
108
144
  }
109
145
  catch (err) {
110
- console.warn("proceeding without SW ready ack:", err instanceof Error ? err.message : err);
146
+ console.warn("proceeding without worker ready ack:", err instanceof Error ? err.message : err);
111
147
  }
112
- return { port: port1, workerInstanceChanged };
148
+ return port1;
149
+ }
150
+ /** Open a fresh repo sync port to the automerge worker (dev console). */
151
+ function getRepoChannel() {
152
+ const worker = getAutomergeWorker();
153
+ const { port1, port2 } = new MessageChannel();
154
+ worker.port.postMessage({ type: "port", id: ++nextRepoChannelId }, [port2]);
155
+ return port1;
113
156
  }
114
157
  export default async function setupServiceWorker(options) {
115
- const repoChannelListeners = new Set();
116
- let reconnectPromise = null;
117
- const reconnectRepoChannels = (reason) => {
118
- if (reconnectPromise)
119
- return reconnectPromise;
120
- reconnectPromise = (async () => {
121
- console.info(`%cservice worker ${reason}, reconnecting repo channels...`, "color: pink; font-weight: bold");
122
- configureServiceWorker(navigator.serviceWorker.controller);
123
- for (const listener of repoChannelListeners) {
124
- try {
125
- const { port } = await openRepoChannel();
126
- await listener(port);
127
- }
128
- catch (err) {
129
- console.error("service worker repo channel listener failed", err);
130
- }
131
- }
132
- })().finally(() => {
133
- reconnectPromise = null;
134
- });
135
- return reconnectPromise;
136
- };
137
- const pingServiceWorker = async () => {
138
- const controller = navigator.serviceWorker.controller;
139
- if (!controller)
140
- return;
141
- const { port1, port2 } = new MessageChannel();
142
- const pong = new Promise((resolve, reject) => {
143
- const timeout = setTimeout(() => {
144
- port1.close();
145
- reject(new Error("service worker pong timeout"));
146
- }, 5_000);
147
- port1.onmessage = (event) => {
148
- clearTimeout(timeout);
149
- port1.close();
150
- resolve(event.data?.workerInstanceId);
151
- };
152
- });
153
- controller.postMessage({ type: "ping" }, [port2]);
154
- try {
155
- const restarted = updateServiceWorkerInstanceId(await pong);
156
- if (restarted) {
157
- await reconnectRepoChannels("restarted");
158
- }
159
- }
160
- catch (err) {
161
- console.warn("service worker ping failed:", err instanceof Error ? err.message : err);
162
- }
163
- };
158
+ if (options?.workerPath)
159
+ automergeWorkerPath = options.workerPath;
160
+ // Start the automerge worker right away so it boots (wasm, repo) while the
161
+ // service worker installs.
162
+ getAutomergeWorker();
164
163
  const path = options?.path ?? "/service-worker.js";
165
164
  // No controller at this point means the page loaded without a service
166
165
  // worker — i.e. this is a first-time install (or a hard reload). Wait for
@@ -178,36 +177,20 @@ export default async function setupServiceWorker(options) {
178
177
  navigator.serviceWorker.addEventListener("controllerchange", () => resolve(), { once: true });
179
178
  });
180
179
  }
181
- // Keepalive Chromium idles out service workers after ~30s of inactivity,
182
- // which tears down the in-memory Repo and forces a cold restart on the next
183
- // fetch. Ping through a MessageChannel so we can detect when a restarted SW
184
- // has a new in-memory Repo and reconnect all repo channels.
185
- setInterval(() => {
186
- void pingServiceWorker();
187
- }, 20_000);
188
- // Reconnect on future SW updates (added after setup so the initial
189
- // activation doesn't notify before callers subscribe).
190
- navigator.serviceWorker.addEventListener("controllerchange", function () {
191
- void reconnectRepoChannels("took control").catch((err) => {
192
- console.error("service worker reconnect failed", err);
193
- });
180
+ // A replacement service worker boots with the default cache name — re-send
181
+ // its configuration whenever a new one takes control.
182
+ navigator.serviceWorker.addEventListener("controllerchange", () => {
183
+ configureServiceWorker(navigator.serviceWorker.controller);
194
184
  });
195
185
  console.log("service worker alive, loading %c patchwork system ", "background: #fcf2f0; color: #333; border: 2px solid; border-radius: 4px");
196
186
  return {
187
+ connectClassicSync,
188
+ getRepoChannel,
197
189
  async subscribeToRepoChannel(listener) {
198
- const { port, workerInstanceChanged } = await openRepoChannel();
199
- if (workerInstanceChanged) {
200
- await reconnectRepoChannels("restarted");
201
- }
202
- repoChannelListeners.add(listener);
203
- try {
204
- await listener(port);
205
- }
206
- catch (err) {
207
- repoChannelListeners.delete(listener);
208
- throw err;
209
- }
210
- return () => repoChannelListeners.delete(listener);
190
+ // The automerge worker outlives the page, so unlike the old in-service-
191
+ // worker repo there's nothing to reconnect: one port, handed over once.
192
+ await listener(await openRepoChannel());
193
+ return () => { };
211
194
  },
212
195
  };
213
196
  }
package/dist/site.d.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 { type DocHandle, Repo, type AutomergeUrl, type StorageId } from "@automerge/vanillajs/slim";
15
15
  import { type AutomergeRepoKeyhive } from "@automerge/automerge-repo-keyhive";
@@ -17,7 +17,6 @@ import { ModuleWatcher } from "@inkandswitch/patchwork-filesystem";
17
17
  import { type AccountDoc } from "@inkandswitch/patchwork-plugins";
18
18
  import * as plugins from "@inkandswitch/patchwork-plugins";
19
19
  import type { ServiceWorkerRepoChannelListener } from "./types.js";
20
- import { SwLogReader } from "./sw-logger.js";
21
20
  declare global {
22
21
  interface Window {
23
22
  accountDocHandle: DocHandle<AccountDoc>;
@@ -32,10 +31,7 @@ declare global {
32
31
  plugins: typeof plugins;
33
32
  accountDocHandle: DocHandle<AccountDoc>;
34
33
  sw: {
35
- printLogs: (n?: number) => Promise<void>;
36
- tailLogs: (n?: number) => ReturnType<typeof SwLogReader.tail>;
37
- exportLogs: () => Promise<string>;
38
- clearLogs: () => Promise<void>;
34
+ connectClassicSync: (server?: string) => Promise<void>;
39
35
  subscribeToRepoChannel: (listener: ServiceWorkerRepoChannelListener) => Promise<() => void>;
40
36
  };
41
37
  };
package/dist/site.js 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 { IndexedDBStorageAdapter, initializeWasm, isValidAutomergeUrl, isValidDocumentId, MessageChannelNetworkAdapter, parseAutomergeUrl, Repo, stringifyAutomergeUrl, } from "@automerge/vanillajs/slim";
15
15
  import * as Automerge from "@automerge/automerge/slim";
@@ -25,7 +25,6 @@ import { registerRepoProviderElement } from "@inkandswitch/patchwork-providers";
25
25
  import { getRegistry, registerPlugins, resolveAccountHandle, unregisterPlugins, } from "@inkandswitch/patchwork-plugins";
26
26
  import * as plugins from "@inkandswitch/patchwork-plugins";
27
27
  import setupServiceWorker from "./setup.js";
28
- import { SwLogReader } from "./sw-logger.js";
29
28
  import debug from "debug";
30
29
  const log = debug("patchwork:bootloader:site");
31
30
  const DEFAULT_REMOTE_STORAGE_ID = "3760df37-a4c6-4f66-9ecd-732039a9385d";
@@ -54,19 +53,20 @@ export async function bootPatchworkSite(config) {
54
53
  if (!sw)
55
54
  throw new Error("Failed to set up service worker");
56
55
  let hive;
56
+ // Get the initial automerge-worker port via subscribeToRepoChannel,
57
+ // then pass it to keyhive init which wraps it in its own network adapter.
58
+ let resolvePort;
59
+ const portPromise = new Promise((r) => {
60
+ resolvePort = r;
61
+ });
62
+ await sw.subscribeToRepoChannel(resolvePort);
63
+ const workerPort = await portPromise;
57
64
  if (config.keyhive) {
58
65
  initKeyhiveWasm();
59
- // Get the initial SW port via subscribeToRepoChannel, then pass it
60
- // to keyhive init which wraps it in its own network adapter.
61
- let resolvePort;
62
- const portPromise = new Promise((r) => { resolvePort = r; });
63
- sw.subscribeToRepoChannel((port) => { resolvePort(port); });
64
- const swPort = await portPromise;
65
66
  hive = await initializeAutomergeRepoKeyhive({
66
67
  storage: new IndexedDBStorageAdapter(`${siteName}-keyhive`),
67
- peerIdSuffix: siteName +
68
- Math.random().toString(36).slice(2),
69
- networkAdapter: new MessageChannelNetworkAdapter(swPort),
68
+ peerIdSuffix: siteName + Math.random().toString(36).slice(2),
69
+ networkAdapter: new MessageChannelNetworkAdapter(workerPort),
70
70
  automaticArchiveIngestion: true,
71
71
  cachingMode: "periodic",
72
72
  onlyShareWithHardcodedServerPeerId: false,
@@ -81,31 +81,20 @@ export async function bootPatchworkSite(config) {
81
81
  idFactory: hive.idFactory,
82
82
  })
83
83
  : new Repo({
84
+ network: [new MessageChannelNetworkAdapter(workerPort)],
84
85
  storage: new IndexedDBStorageAdapter(),
85
86
  async sharePolicy(peerId) {
86
- return peerId.includes("service-worker");
87
+ return peerId.includes("automerge-worker");
87
88
  },
88
89
  enableRemoteHeadsGossiping: true,
89
90
  peerId: `${config.titleSuffix}-tab-${crypto.randomUUID()}`,
90
91
  });
91
92
  repo.subscribeToRemotes(config.remoteStorageIds ?? [DEFAULT_REMOTE_STORAGE_ID]);
93
+ await repo.networkSubsystem.whenReady();
92
94
  if (hive) {
93
- await repo.networkSubsystem.whenReady();
94
95
  hive.networkAdapter.syncKeyhive?.();
95
96
  }
96
- else {
97
- let activeServiceWorkerPort;
98
- const connectServiceWorkerPort = async (port) => {
99
- const previousPort = activeServiceWorkerPort;
100
- activeServiceWorkerPort = port;
101
- const net = new MessageChannelNetworkAdapter(port);
102
- repo.networkSubsystem.addNetworkAdapter(net);
103
- await net.whenReady();
104
- previousPort?.close();
105
- };
106
- await sw.subscribeToRepoChannel(connectServiceWorkerPort);
107
- }
108
- installDevConsoleGlobals(repo, hive);
97
+ installDevConsoleGlobals(repo, hive, sw.getRepoChannel);
109
98
  registerRepoProviderElement(repo);
110
99
  const rootElement = document.getElementById(config.rootElementId ?? "root");
111
100
  if (!rootElement) {
@@ -120,10 +109,12 @@ export async function bootPatchworkSite(config) {
120
109
  // datatype lives in that bundle today). The user's own module-settings URL
121
110
  // is added lazily once it appears on the account doc — see below.
122
111
  const moduleWatcher = new ModuleWatcher(repo, { system: defaultModulesUrl }, onModuleLoaded, unregisterPlugins);
123
- const accountDocHandle = await resolveAccountHandle(repo, {
112
+ const accountDocHandle = (await resolveAccountHandle(repo, {
124
113
  storageKey: config.accountStorageKey,
125
114
  hive,
126
- });
115
+ }));
116
+ // TODO: something we (Orion & pvh) changed in the types made this necessary
117
+ // fix this before merging to main!
127
118
  window.accountDocHandle = accountDocHandle;
128
119
  wireModuleSettingsWhenReady(accountDocHandle, moduleWatcher);
129
120
  primeRootElement(rootElement, accountDocHandle);
@@ -134,7 +125,7 @@ export async function bootPatchworkSite(config) {
134
125
  plugins,
135
126
  accountDocHandle,
136
127
  sw: {
137
- ...buildSwLogApi(),
128
+ connectClassicSync: sw.connectClassicSync,
138
129
  subscribeToRepoChannel: sw.subscribeToRepoChannel,
139
130
  },
140
131
  };
@@ -162,18 +153,14 @@ function resolveDefaultModulesUrl(builtin) {
162
153
  console.warn(`ignoring invalid defaultToolsUrl in localStorage: ${override}; using built-in default`);
163
154
  return builtin;
164
155
  }
165
- function installDevConsoleGlobals(repo, hive) {
156
+ function installDevConsoleGlobals(repo, hive, getRepoChannel) {
166
157
  window.repo = repo;
167
158
  window.Automerge = Automerge;
168
159
  window.AutomergeRepo = AutomergeRepo;
169
160
  if (hive) {
170
161
  window.hive = hive;
171
162
  }
172
- window.getRepoChannel = () => {
173
- const { port1, port2 } = new MessageChannel();
174
- navigator.serviceWorker.controller.postMessage({ type: "port" }, [port2]);
175
- return port1;
176
- };
163
+ window.getRepoChannel = getRepoChannel;
177
164
  }
178
165
  function onModuleLoaded(name, mod) {
179
166
  if (Array.isArray(mod.plugins)) {
@@ -235,24 +222,6 @@ function logToolRegistryWhenLoaded(moduleWatcher) {
235
222
  console.error("doneLoading rejected:", err);
236
223
  });
237
224
  }
238
- function buildSwLogApi() {
239
- return {
240
- printLogs: async (n = 200) => {
241
- const entries = await SwLogReader.tail(n);
242
- for (const e of entries) {
243
- const prefix = `[${e.ts}] [${e.level}]`;
244
- if (e.data !== undefined)
245
- log(prefix, e.msg, e.data);
246
- else
247
- log(prefix, e.msg);
248
- }
249
- log(`--- ${entries.length} entries ---`);
250
- },
251
- tailLogs: (n = 200) => SwLogReader.tail(n),
252
- exportLogs: () => SwLogReader.exportAll(),
253
- clearLogs: () => SwLogReader.clear(),
254
- };
255
- }
256
225
  const LOADING_STYLE_ID = "pw-bootloader-loading-styles";
257
226
  const LOADING_ELEMENT_ID = "pw-bootloader-loading";
258
227
  function showLoadingAnimation() {
@@ -0,0 +1,8 @@
1
+ /** localStorage key: optional override for the classic sync WebSocket URL. */
2
+ export declare const CLASSIC_SYNC_SERVER_KEY = "patchworkClassicSyncServer";
3
+ export declare const DEFAULT_CLASSIC_SYNC_SERVER = "wss://sync3.automerge.org";
4
+ export declare function readClassicSyncServer(storage?: Pick<Storage, "getItem">): string;
5
+ export type ConnectClassicSyncMessage = {
6
+ type: "connect-classic-sync";
7
+ server: string;
8
+ };
@@ -0,0 +1,13 @@
1
+ /** localStorage key: optional override for the classic sync WebSocket URL. */
2
+ export const CLASSIC_SYNC_SERVER_KEY = "patchworkClassicSyncServer";
3
+ export const DEFAULT_CLASSIC_SYNC_SERVER = "wss://sync3.automerge.org";
4
+ export function readClassicSyncServer(storage = globalThis.localStorage) {
5
+ const override = storage.getItem(CLASSIC_SYNC_SERVER_KEY)?.trim();
6
+ if (!override)
7
+ return DEFAULT_CLASSIC_SYNC_SERVER;
8
+ if (!/^wss?:\/\//.test(override)) {
9
+ console.warn(`ignoring invalid ${CLASSIC_SYNC_SERVER_KEY} in localStorage: ${override}; using ${DEFAULT_CLASSIC_SYNC_SERVER}`);
10
+ return DEFAULT_CLASSIC_SYNC_SERVER;
11
+ }
12
+ return override;
13
+ }
package/dist/types.d.ts CHANGED
@@ -1,11 +1,101 @@
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 declare const HANDOFF_CHANNEL = "@patchwork/handoff";
8
+ /**
9
+ * The special URL to resolve, plus enough of the {@link Request} the service
10
+ * worker is holding that the automerge worker can construct one that
11
+ * `cache.match`es it.
12
+ *
13
+ * Stale workers on either side of the channel can outlive a deploy, so the
14
+ * shape can only ever change additively: `url` must stay the http request
15
+ * URL old automerge workers decode the special URL out of, and new meaning
16
+ * goes in new fields old receivers ignore.
17
+ */
18
+ export interface HandoffRequest {
19
+ /**
20
+ * The URL of the request the service worker is holding (the encoded
21
+ * `https://…/automerge%3Aabc/…` form) — the cache key.
22
+ */
23
+ url: string;
24
+ /** the decoded special URL, e.g. `automerge:abc/some/path` */
25
+ handoffURL: string;
26
+ /**
27
+ * @deprecated A briefly-deployed shape put the special URL in `url` and
28
+ * the cache key here. Only read, never sent.
29
+ */
30
+ cacheKey?: string;
31
+ headers: Record<string, string>;
32
+ method: string;
33
+ destination: RequestDestination;
34
+ referrer: string;
35
+ }
36
+ /**
37
+ * Service worker → automerge worker: please resolve this request and put
38
+ * the response in my cache.
39
+ */
40
+ export interface HandoffRequestMessage {
41
+ id: string;
42
+ type: "request";
43
+ /** the current name of the service worker cache */
44
+ cachename: string;
45
+ request: HandoffRequest;
46
+ }
47
+ /**
48
+ * Automerge worker → service worker: the response is stored in the cache
49
+ * under the request you're holding. Serve `cache.match`.
50
+ */
51
+ export interface HandoffCachedMessage {
52
+ id: string;
53
+ type: "cached";
54
+ }
55
+ /**
56
+ * An inline response for things that shouldn't be cached: errors, redirects
57
+ * &c.
58
+ */
59
+ export interface HandoffResponse {
60
+ body?: string | Uint8Array<ArrayBuffer>;
61
+ /** defaults to 200 */
62
+ status?: number;
63
+ headers?: Record<string, string>;
64
+ }
65
+ /**
66
+ * Automerge worker → service worker: don't cache anything, serve this
67
+ * response directly.
68
+ */
69
+ export interface HandoffResponseMessage {
70
+ id: string;
71
+ type: "response";
72
+ response: HandoffResponse;
73
+ }
74
+ export type HandoffReplyMessage = HandoffCachedMessage | HandoffResponseMessage;
75
+ /**
76
+ * Automerge worker → world: broadcast once on startup so the service worker
77
+ * can re-send any handoff requests that raced the worker's boot.
78
+ */
79
+ export interface HandoffOnlineMessage {
80
+ type: "online";
81
+ }
1
82
  export type SetupServiceWorkerOptions = {
2
83
  /**
3
84
  * The public path to the service worker file.
4
85
  * Defaults to `/service-worker.js`
5
86
  */
6
87
  path?: string;
88
+ /**
89
+ * The public path to the automerge shared worker file.
90
+ * Defaults to `/automerge-worker.js`
91
+ */
92
+ workerPath?: string;
7
93
  };
8
94
  export type ServiceWorkerRepoChannelListener = (port: MessagePort) => void | Promise<void>;
9
95
  export type SetupServiceWorkerResult = {
96
+ /** Open a classic Automerge sync WebSocket from the automerge worker. */
97
+ connectClassicSync: (server?: string) => Promise<void>;
10
98
  subscribeToRepoChannel: (listener: ServiceWorkerRepoChannelListener) => Promise<() => void>;
99
+ /** Open a fresh repo sync port to the automerge worker (dev console). */
100
+ getRepoChannel: () => MessagePort;
11
101
  };
package/dist/types.js CHANGED
@@ -1 +1,7 @@
1
- export {};
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";
@@ -1,20 +1,36 @@
1
1
  import { builtins } from "./importmap-plugin.js";
2
+ // The service worker and the automerge shared worker are emitted as their
3
+ // own chunks. Their heavy imports are marked external and resolved to
4
+ // /packages/... URLs (both workers are created with type:"module", so the
5
+ // browser fetches those as regular network requests).
6
+ const workers = [
7
+ {
8
+ specifier: "@inkandswitch/patchwork-bootloader/service-worker",
9
+ fileName: "service-worker.js",
10
+ },
11
+ {
12
+ specifier: "@inkandswitch/patchwork-bootloader/automerge-worker",
13
+ fileName: "automerge-worker.js",
14
+ },
15
+ ];
2
16
  export function serviceworker() {
3
- let swEntryId;
17
+ const entryIds = new Set();
4
18
  return {
5
19
  name: "@patchwork/service-worker",
6
20
  enforce: "pre",
7
21
  async buildStart() {
8
- const resolved = await this.resolve("@inkandswitch/patchwork-bootloader/service-worker");
9
- swEntryId = resolved.id;
10
- this.emitFile({
11
- type: "chunk",
12
- id: resolved.id,
13
- fileName: "service-worker.js",
14
- });
22
+ for (const { specifier, fileName } of workers) {
23
+ const resolved = await this.resolve(specifier);
24
+ entryIds.add(resolved.id);
25
+ this.emitFile({
26
+ type: "chunk",
27
+ id: resolved.id,
28
+ fileName,
29
+ });
30
+ }
15
31
  },
16
32
  resolveId(source, importer) {
17
- if (importer && swEntryId && importer === swEntryId && source in builtins) {
33
+ if (importer && entryIds.has(importer) && source in builtins) {
18
34
  return { id: builtins[source], external: true };
19
35
  }
20
36
  },