@inkandswitch/patchwork-bootloader 0.6.2 → 0.7.0
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 +44 -0
- package/dist/automerge-protocol-handler-worker.js +315 -0
- package/dist/externals-list.js +1 -5
- package/dist/setup.d.ts +4 -7
- package/dist/setup.js +22 -395
- package/dist/shared-worker-lifecycle.d.ts +30 -0
- package/dist/shared-worker-lifecycle.js +242 -0
- package/dist/siblings.d.ts +20 -0
- package/dist/siblings.js +28 -0
- package/dist/types.d.ts +1 -75
- package/dist/types.js +0 -6
- package/dist/worker-control.d.ts +8 -0
- package/dist/worker-control.js +111 -0
- package/package.json +25 -20
- package/src/automerge-protocol-handler-worker.ts +421 -0
- package/src/externals-list.ts +1 -5
- package/src/setup.ts +34 -439
- package/src/shared-worker-lifecycle.ts +289 -0
- package/src/siblings.ts +32 -0
- package/src/types.ts +2 -108
- package/src/worker-control.ts +130 -0
- package/dist/automerge-worker.js +0 -811
- package/src/automerge-worker.ts +0 -1035
- /package/dist/{automerge-worker.d.ts → automerge-protocol-handler-worker.d.ts} +0 -0
|
@@ -0,0 +1,421 @@
|
|
|
1
|
+
// The Repo that resolves `automerge:` URLs for the service worker, in a
|
|
2
|
+
// SharedWorker: one instance serves every tab and lives as long as any tab
|
|
3
|
+
// does.
|
|
4
|
+
//
|
|
5
|
+
// It is a node like any tab's: the same IndexedDB, its own sync-server socket,
|
|
6
|
+
// and the siblings channel to the tabs. Resolving requests is its whole job.
|
|
7
|
+
// When the service worker misses the cache for a request that looks like a URL
|
|
8
|
+
// encoded URL, it broadcasts a HandoffRequestMessage on HANDOFF_CHANNEL; we
|
|
9
|
+
// resolve the automerge URL, write the response into the service worker's
|
|
10
|
+
// cache (keyed by a Request reconstructed to match the one it's holding), and
|
|
11
|
+
// reply on the same channel.
|
|
12
|
+
import { initializeWasm, hasHeads } from "@automerge/automerge/slim";
|
|
13
|
+
// eslint-disable-next-line
|
|
14
|
+
// @ts-ignore — initSync is a wasm-bindgen runtime helper not in the .d.ts
|
|
15
|
+
import { initSync as initSubductionSync } from "@automerge/automerge-subduction/slim";
|
|
16
|
+
import { MemorySigner } from "@automerge/automerge-subduction/slim";
|
|
17
|
+
|
|
18
|
+
import {
|
|
19
|
+
Repo,
|
|
20
|
+
isValidAutomergeUrl,
|
|
21
|
+
parseAutomergeUrl,
|
|
22
|
+
stringifyAutomergeUrl,
|
|
23
|
+
type AutomergeUrl,
|
|
24
|
+
type DocHandle,
|
|
25
|
+
type PeerId,
|
|
26
|
+
} from "@automerge/automerge-repo/slim";
|
|
27
|
+
import { resolvePath } from "@inkandswitch/patchwork-filesystem";
|
|
28
|
+
|
|
29
|
+
import { IndexedDBWorkerStorageAdapter } from "@automerge/automerge-repo-storage-indexeddb/IndexedDBWorkerStorageAdapter";
|
|
30
|
+
import { WebSocketWorkerClientAdapter } from "@automerge/automerge-repo-network-websocket";
|
|
31
|
+
import {
|
|
32
|
+
initializeAutomergeRepoKeyhive,
|
|
33
|
+
initKeyhiveWasm,
|
|
34
|
+
type AutomergeRepoKeyhive,
|
|
35
|
+
type SyncServerSelection,
|
|
36
|
+
} from "@automerge/automerge-repo-keyhive";
|
|
37
|
+
|
|
38
|
+
import { DEFAULT_CLASSIC_SYNC_SERVER } from "./sync-config.js";
|
|
39
|
+
import { siblingAdapters } from "./siblings.js";
|
|
40
|
+
import { keyhiveStorageName, storagePrefix } from "./storage.js";
|
|
41
|
+
import { startWorkerControl } from "./worker-control.js";
|
|
42
|
+
import {
|
|
43
|
+
HANDOFF_CHANNEL,
|
|
44
|
+
type HandoffCachedMessage,
|
|
45
|
+
type HandoffOnlineMessage,
|
|
46
|
+
type HandoffAbortMessage,
|
|
47
|
+
type HandoffRequestMessage,
|
|
48
|
+
type HandoffResponseMessage,
|
|
49
|
+
} from "./types.js";
|
|
50
|
+
|
|
51
|
+
declare const __SYNC_SERVER__: {
|
|
52
|
+
url: string;
|
|
53
|
+
keyhive?: SyncServerSelection;
|
|
54
|
+
};
|
|
55
|
+
|
|
56
|
+
const syncServer =
|
|
57
|
+
typeof __SYNC_SERVER__ !== "undefined"
|
|
58
|
+
? __SYNC_SERVER__
|
|
59
|
+
: { url: "wss://subduction.sync.inkandswitch.com" };
|
|
60
|
+
|
|
61
|
+
const RESOLVE_TIMEOUT_MS = 30_000;
|
|
62
|
+
|
|
63
|
+
const CACHEABLE_STATUSES = [200, 203, 204];
|
|
64
|
+
|
|
65
|
+
const control = startWorkerControl("automerge-protocol-handler-worker", {
|
|
66
|
+
onMessage: handleControlMessage,
|
|
67
|
+
});
|
|
68
|
+
const log = control.log;
|
|
69
|
+
|
|
70
|
+
// ── The repo ───────────────────────────────────────────────────────────
|
|
71
|
+
|
|
72
|
+
let repoPromise: Promise<Repo> | null = null;
|
|
73
|
+
|
|
74
|
+
function getRepo(): Promise<Repo> {
|
|
75
|
+
if (!repoPromise) {
|
|
76
|
+
repoPromise = buildRepo();
|
|
77
|
+
// Don't cache a rejection (e.g. the wasm fetch failed): clear the slot so
|
|
78
|
+
// the next caller retries from scratch.
|
|
79
|
+
repoPromise.catch(() => {
|
|
80
|
+
repoPromise = null;
|
|
81
|
+
});
|
|
82
|
+
}
|
|
83
|
+
return repoPromise;
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
async function buildRepo(): Promise<Repo> {
|
|
87
|
+
log("fetching wasm");
|
|
88
|
+
const [automergeWasm, subductionWasm] = await Promise.all([
|
|
89
|
+
fetch("/automerge.wasm").then((r) => r.arrayBuffer()),
|
|
90
|
+
fetch("/subduction.wasm").then((r) => r.arrayBuffer()),
|
|
91
|
+
]);
|
|
92
|
+
initSubductionSync(new Uint8Array(subductionWasm));
|
|
93
|
+
await initializeWasm(new Uint8Array(automergeWasm));
|
|
94
|
+
log("wasm initialized");
|
|
95
|
+
|
|
96
|
+
const { repo, hive } = syncServer.keyhive
|
|
97
|
+
? await buildKeyhiveRepo(syncServer.keyhive)
|
|
98
|
+
: { repo: buildPlainRepo() };
|
|
99
|
+
|
|
100
|
+
(self as any).repo = repo;
|
|
101
|
+
if (hive) (self as any).hive = hive;
|
|
102
|
+
return repo;
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
function buildPlainRepo(): Repo {
|
|
106
|
+
return new Repo({
|
|
107
|
+
signer: new MemorySigner(),
|
|
108
|
+
storage: new IndexedDBWorkerStorageAdapter(),
|
|
109
|
+
peerId:
|
|
110
|
+
`${storagePrefix}-resolver-${Math.random().toString(36).slice(2)}` as PeerId,
|
|
111
|
+
subductionWebsocketEndpoints: [syncServer.url],
|
|
112
|
+
subductionAdapters: siblingAdapters(),
|
|
113
|
+
enableRemoteHeadsGossiping: true,
|
|
114
|
+
});
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
async function buildKeyhiveRepo(
|
|
118
|
+
keyhiveSyncServer: SyncServerSelection
|
|
119
|
+
): Promise<{ repo: Repo; hive: AutomergeRepoKeyhive }> {
|
|
120
|
+
initKeyhiveWasm();
|
|
121
|
+
const { hive, repo } = await initializeAutomergeRepoKeyhive({
|
|
122
|
+
createRepo: (config) => new Repo(config),
|
|
123
|
+
storage: new IndexedDBWorkerStorageAdapter(keyhiveStorageName),
|
|
124
|
+
peerIdSuffix:
|
|
125
|
+
`${storagePrefix}-resolver` + Math.random().toString(36).slice(2),
|
|
126
|
+
automaticArchiveIngestion: true,
|
|
127
|
+
cachingMode: "periodic",
|
|
128
|
+
// ARK selects the relay via `syncServer`, which pairs the contact card with
|
|
129
|
+
// the matching peer id. Omitting it defaults to "subduction".
|
|
130
|
+
syncServer: keyhiveSyncServer,
|
|
131
|
+
repo: {
|
|
132
|
+
storage: new IndexedDBWorkerStorageAdapter(),
|
|
133
|
+
subductionWebsocketEndpoints: [syncServer.url],
|
|
134
|
+
subductionAdapters: siblingAdapters(),
|
|
135
|
+
enableRemoteHeadsGossiping: true,
|
|
136
|
+
},
|
|
137
|
+
});
|
|
138
|
+
|
|
139
|
+
hive.networkAdapter.whenReady().then(() => {
|
|
140
|
+
(hive.networkAdapter as any).syncKeyhive();
|
|
141
|
+
});
|
|
142
|
+
|
|
143
|
+
return { repo, hive };
|
|
144
|
+
}
|
|
145
|
+
|
|
146
|
+
// ── Classic sync ───────────────────────────────────────────────────────
|
|
147
|
+
|
|
148
|
+
let classicSyncServer = DEFAULT_CLASSIC_SYNC_SERVER;
|
|
149
|
+
let classicSyncAdapter: WebSocketWorkerClientAdapter | null = null;
|
|
150
|
+
let classicSyncConnect: Promise<void> | null = null;
|
|
151
|
+
|
|
152
|
+
function connectClassicSyncNetwork(server: string): Promise<void> {
|
|
153
|
+
const url = server.trim() || DEFAULT_CLASSIC_SYNC_SERVER;
|
|
154
|
+
if (classicSyncConnect && classicSyncServer === url)
|
|
155
|
+
return classicSyncConnect;
|
|
156
|
+
|
|
157
|
+
if (classicSyncAdapter && classicSyncServer !== url) {
|
|
158
|
+
classicSyncAdapter.disconnect();
|
|
159
|
+
classicSyncAdapter = null;
|
|
160
|
+
}
|
|
161
|
+
|
|
162
|
+
classicSyncServer = url;
|
|
163
|
+
const connecting = (async () => {
|
|
164
|
+
const repo = await getRepo();
|
|
165
|
+
if (!classicSyncAdapter) {
|
|
166
|
+
classicSyncAdapter = new WebSocketWorkerClientAdapter(url);
|
|
167
|
+
repo.networkSubsystem.addNetworkAdapter(classicSyncAdapter);
|
|
168
|
+
}
|
|
169
|
+
await classicSyncAdapter.whenReady();
|
|
170
|
+
log("classic sync connected", url);
|
|
171
|
+
})();
|
|
172
|
+
|
|
173
|
+
// Clear the memo on failure so a later attempt can retry, and swallow the
|
|
174
|
+
// rejection on this copy so it isn't reported as unhandled — callers get it
|
|
175
|
+
// from the promise we return.
|
|
176
|
+
classicSyncConnect = connecting;
|
|
177
|
+
connecting.catch(() => {
|
|
178
|
+
if (classicSyncConnect === connecting) classicSyncConnect = null;
|
|
179
|
+
});
|
|
180
|
+
return connecting;
|
|
181
|
+
}
|
|
182
|
+
|
|
183
|
+
// ── Control protocol ───────────────────────────────────────────────────
|
|
184
|
+
|
|
185
|
+
function handleControlMessage(
|
|
186
|
+
data: any,
|
|
187
|
+
controlPort: MessagePort,
|
|
188
|
+
event: MessageEvent
|
|
189
|
+
): void {
|
|
190
|
+
if (data?.type !== "connect-classic-sync") return;
|
|
191
|
+
const [replyPort] = event.ports;
|
|
192
|
+
const server =
|
|
193
|
+
typeof data.server === "string" ? data.server : DEFAULT_CLASSIC_SYNC_SERVER;
|
|
194
|
+
connectClassicSyncNetwork(server).then(
|
|
195
|
+
() => {
|
|
196
|
+
replyPort?.postMessage({ type: "connect-classic-sync-ready" });
|
|
197
|
+
replyPort?.close();
|
|
198
|
+
},
|
|
199
|
+
(err) => {
|
|
200
|
+
console.error("connectClassicSyncNetwork failed", err);
|
|
201
|
+
replyPort?.postMessage({
|
|
202
|
+
type: "connect-classic-sync-failed",
|
|
203
|
+
error: String(err),
|
|
204
|
+
});
|
|
205
|
+
replyPort?.close();
|
|
206
|
+
}
|
|
207
|
+
);
|
|
208
|
+
}
|
|
209
|
+
|
|
210
|
+
// ── Resolving ──────────────────────────────────────────────────────────
|
|
211
|
+
|
|
212
|
+
function waitForHeads(
|
|
213
|
+
handle: DocHandle<unknown>,
|
|
214
|
+
hexHeads: string[],
|
|
215
|
+
signal: AbortSignal
|
|
216
|
+
): Promise<boolean> {
|
|
217
|
+
if (hasHeads(handle.doc(), hexHeads)) return Promise.resolve(true);
|
|
218
|
+
if (signal.aborted) return Promise.resolve(false);
|
|
219
|
+
return new Promise((resolve) => {
|
|
220
|
+
const cleanup = () => {
|
|
221
|
+
handle.off("heads-changed", check);
|
|
222
|
+
signal.removeEventListener("abort", onAbort);
|
|
223
|
+
};
|
|
224
|
+
const check = () => {
|
|
225
|
+
if (!hasHeads(handle.doc(), hexHeads)) return;
|
|
226
|
+
cleanup();
|
|
227
|
+
resolve(true);
|
|
228
|
+
};
|
|
229
|
+
const onAbort = () => {
|
|
230
|
+
cleanup();
|
|
231
|
+
resolve(false);
|
|
232
|
+
};
|
|
233
|
+
handle.on("heads-changed", check);
|
|
234
|
+
signal.addEventListener("abort", onAbort);
|
|
235
|
+
// The heads may have landed between the check above and subscribing.
|
|
236
|
+
check();
|
|
237
|
+
});
|
|
238
|
+
}
|
|
239
|
+
|
|
240
|
+
/**
|
|
241
|
+
* Thrown instead of returning a Response when the request should fail as a
|
|
242
|
+
* network error rather than resolve to something the caller can memoize.
|
|
243
|
+
* See {@link HandoffAbortMessage}.
|
|
244
|
+
*/
|
|
245
|
+
class AbortHandoff extends Error {}
|
|
246
|
+
|
|
247
|
+
async function resolveAutomergeUrl(
|
|
248
|
+
automergeURL: URL,
|
|
249
|
+
signal: AbortSignal
|
|
250
|
+
): Promise<Response> {
|
|
251
|
+
const repo = await getRepo();
|
|
252
|
+
const [maybeAutomergeUrl, ...path] = automergeURL.href.split("/");
|
|
253
|
+
|
|
254
|
+
if (!isValidAutomergeUrl(maybeAutomergeUrl)) {
|
|
255
|
+
return new Response("invalid automerge url", { status: 400 });
|
|
256
|
+
}
|
|
257
|
+
|
|
258
|
+
if (path.length && !path[path.length - 1]) path.pop();
|
|
259
|
+
|
|
260
|
+
const { heads, hexHeads, documentId } = parseAutomergeUrl(maybeAutomergeUrl);
|
|
261
|
+
|
|
262
|
+
// todo, maybe a bad idea? maybe we should throw instead of es-module-caching
|
|
263
|
+
// the headless req
|
|
264
|
+
if (!heads) {
|
|
265
|
+
const folder = await repo.find(maybeAutomergeUrl, { signal });
|
|
266
|
+
const url = stringifyAutomergeUrl({ documentId, heads: folder.heads() });
|
|
267
|
+
const location = `/${encodeURIComponent(url)}${path.length ? `/${path.join("/")}` : ""}`;
|
|
268
|
+
return Response.redirect(location, 307);
|
|
269
|
+
}
|
|
270
|
+
|
|
271
|
+
const baseHandle = await repo.find(stringifyAutomergeUrl({ documentId }), {
|
|
272
|
+
signal,
|
|
273
|
+
});
|
|
274
|
+
if (!(await waitForHeads(baseHandle, hexHeads ?? [], signal))) {
|
|
275
|
+
throw new AbortHandoff(
|
|
276
|
+
`heads not found for ${maybeAutomergeUrl} within ${RESOLVE_TIMEOUT_MS}ms`
|
|
277
|
+
);
|
|
278
|
+
}
|
|
279
|
+
|
|
280
|
+
const resolved = await resolvePath(
|
|
281
|
+
repo,
|
|
282
|
+
baseHandle.view(heads),
|
|
283
|
+
path.map(decodeURIComponent)
|
|
284
|
+
);
|
|
285
|
+
if (!resolved) {
|
|
286
|
+
throw new Error(
|
|
287
|
+
`couldn't resolve ${path.join("/")} in folder at ${maybeAutomergeUrl}`
|
|
288
|
+
);
|
|
289
|
+
}
|
|
290
|
+
|
|
291
|
+
const body: BodyInit =
|
|
292
|
+
resolved.content instanceof Uint8Array
|
|
293
|
+
? (new Uint8Array(resolved.content) as BlobPart)
|
|
294
|
+
: resolved.content;
|
|
295
|
+
|
|
296
|
+
return new Response(body, {
|
|
297
|
+
status: 200,
|
|
298
|
+
headers: { "content-type": resolved.type },
|
|
299
|
+
});
|
|
300
|
+
}
|
|
301
|
+
|
|
302
|
+
const handoffChannel = new BroadcastChannel(HANDOFF_CHANNEL);
|
|
303
|
+
|
|
304
|
+
function replyToHandoff(id: string, status: number, body: string): void {
|
|
305
|
+
handoffChannel.postMessage({
|
|
306
|
+
id,
|
|
307
|
+
type: "response",
|
|
308
|
+
response: { status, body, headers: { "content-type": "text/plain" } },
|
|
309
|
+
} satisfies HandoffResponseMessage);
|
|
310
|
+
}
|
|
311
|
+
|
|
312
|
+
function impatience(limit: number) {
|
|
313
|
+
return new Promise<never>((_, reject) =>
|
|
314
|
+
setTimeout(
|
|
315
|
+
() => reject(new Error(`resolve timeout after ${limit}ms`)),
|
|
316
|
+
limit
|
|
317
|
+
)
|
|
318
|
+
);
|
|
319
|
+
}
|
|
320
|
+
|
|
321
|
+
async function handleHandoffRequest(message: HandoffRequestMessage) {
|
|
322
|
+
const { id, cachename, request } = message;
|
|
323
|
+
|
|
324
|
+
let handoff: URL;
|
|
325
|
+
try {
|
|
326
|
+
handoff = new URL(request.handoffURL);
|
|
327
|
+
} catch {
|
|
328
|
+
console.error("couldn't parse handoff url", request);
|
|
329
|
+
replyToHandoff(
|
|
330
|
+
id,
|
|
331
|
+
400,
|
|
332
|
+
`couldn't parse a special url out of ${request.url}`
|
|
333
|
+
);
|
|
334
|
+
return;
|
|
335
|
+
}
|
|
336
|
+
|
|
337
|
+
// Other handlers may be listening on the channel for other schemes, so stay
|
|
338
|
+
// quiet rather than clobbering their reply with an error.
|
|
339
|
+
if (handoff.protocol !== "automerge:") {
|
|
340
|
+
log(
|
|
341
|
+
`ignoring handoff ${id} for non-automerge url ${handoff}. not my circus, not my monkeys`
|
|
342
|
+
);
|
|
343
|
+
return;
|
|
344
|
+
}
|
|
345
|
+
|
|
346
|
+
let response: Response;
|
|
347
|
+
try {
|
|
348
|
+
log(`resolving handoff ${id} for ${handoff}`);
|
|
349
|
+
const signal = AbortSignal.timeout(RESOLVE_TIMEOUT_MS);
|
|
350
|
+
response = await Promise.race([
|
|
351
|
+
resolveAutomergeUrl(handoff, signal),
|
|
352
|
+
impatience(RESOLVE_TIMEOUT_MS),
|
|
353
|
+
]);
|
|
354
|
+
} catch (error) {
|
|
355
|
+
if (error instanceof AbortHandoff) {
|
|
356
|
+
handoffChannel.postMessage({
|
|
357
|
+
id,
|
|
358
|
+
type: "abort",
|
|
359
|
+
reason: error.message,
|
|
360
|
+
} satisfies HandoffAbortMessage);
|
|
361
|
+
return;
|
|
362
|
+
}
|
|
363
|
+
console.error(`error resolving ${request.url}`, error);
|
|
364
|
+
replyToHandoff(
|
|
365
|
+
id,
|
|
366
|
+
557,
|
|
367
|
+
error instanceof Error
|
|
368
|
+
? `${error.message}\n\n${error.stack}`
|
|
369
|
+
: String(error)
|
|
370
|
+
);
|
|
371
|
+
return;
|
|
372
|
+
}
|
|
373
|
+
|
|
374
|
+
try {
|
|
375
|
+
if (!CACHEABLE_STATUSES.includes(response.status)) {
|
|
376
|
+
// Errors, redirects and the like go back inline for the service worker to
|
|
377
|
+
// serve directly, so they aren't cached forever (still in esmodulecache,
|
|
378
|
+
// cleared after a refresh)
|
|
379
|
+
log(`responding inline to ${request.url} with ${response.status}`);
|
|
380
|
+
handoffChannel.postMessage({
|
|
381
|
+
id,
|
|
382
|
+
type: "response",
|
|
383
|
+
response: {
|
|
384
|
+
status: response.status,
|
|
385
|
+
headers: Object.fromEntries(response.headers.entries()),
|
|
386
|
+
body: response.body ? await response.text() : undefined,
|
|
387
|
+
},
|
|
388
|
+
} satisfies HandoffResponseMessage);
|
|
389
|
+
return;
|
|
390
|
+
}
|
|
391
|
+
|
|
392
|
+
// Reconstruct the request the service worker is holding so the entry matches
|
|
393
|
+
// its cache.match. `destination` isn't constructible but doesn't participate
|
|
394
|
+
// in cache matching.
|
|
395
|
+
const cacheKey = new Request(request.url, {
|
|
396
|
+
method: request.method,
|
|
397
|
+
headers: request.headers,
|
|
398
|
+
referrer: request.referrer,
|
|
399
|
+
});
|
|
400
|
+
const cache = await caches.open(cachename);
|
|
401
|
+
await cache.put(cacheKey, response);
|
|
402
|
+
log(`cached ${cacheKey.url} in ${cachename}`);
|
|
403
|
+
handoffChannel.postMessage({
|
|
404
|
+
id,
|
|
405
|
+
type: "cached",
|
|
406
|
+
} satisfies HandoffCachedMessage);
|
|
407
|
+
} catch (error) {
|
|
408
|
+
console.error(`failed to reply for ${request.url}`, error);
|
|
409
|
+
replyToHandoff(id, 558, String(error));
|
|
410
|
+
}
|
|
411
|
+
}
|
|
412
|
+
|
|
413
|
+
handoffChannel.addEventListener("message", (event) => {
|
|
414
|
+
if (event.data?.type === "request") {
|
|
415
|
+
void handleHandoffRequest(event.data as HandoffRequestMessage);
|
|
416
|
+
}
|
|
417
|
+
});
|
|
418
|
+
|
|
419
|
+
// Announce ourselves so the service worker can re-broadcast handoff requests
|
|
420
|
+
// sent while we were booting.
|
|
421
|
+
handoffChannel.postMessage({ type: "online" } satisfies HandoffOnlineMessage);
|
package/src/externals-list.ts
CHANGED
|
@@ -6,11 +6,7 @@ const externals = [
|
|
|
6
6
|
"@automerge/automerge/slim",
|
|
7
7
|
"@automerge/automerge-repo",
|
|
8
8
|
"@automerge/automerge-repo/slim",
|
|
9
|
-
|
|
10
|
-
// proxy entry and donate its port to the automerge worker (Chrome can't
|
|
11
|
-
// spawn workers from inside a SharedWorker). See setup.ts/automerge-worker.ts.
|
|
12
|
-
"@automerge/automerge-repo/worker-port",
|
|
13
|
-
"@automerge/automerge-repo/subduction-websocket-worker-shared",
|
|
9
|
+
"@automerge/automerge-repo-network-broadcastchannel",
|
|
14
10
|
"@automerge/automerge-repo-network-messagechannel",
|
|
15
11
|
"@automerge/automerge-repo-network-websocket",
|
|
16
12
|
"@automerge/automerge-repo-storage-indexeddb",
|