@inkandswitch/patchwork-bootloader 0.3.0 → 0.3.2
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 +20 -0
- package/dist/automerge-worker.js +468 -9
- package/dist/externals.js +1 -2
- package/dist/module-loader-worker.d.ts +1 -0
- package/dist/module-loader-worker.js +55 -0
- package/dist/module-loader.d.ts +13 -0
- package/dist/module-loader.js +72 -0
- package/dist/service-worker.js +58 -6
- package/dist/setup.d.ts +6 -1
- package/dist/setup.js +177 -2
- package/dist/site.d.ts +7 -7
- package/dist/site.js +194 -71
- package/dist/types.d.ts +72 -0
- package/dist/types.js +6 -0
- package/dist/vite/service-worker-plugin.js +4 -0
- package/package.json +18 -14
- package/src/automerge-worker.ts +512 -9
- package/src/externals.ts +1 -2
- package/src/module-loader-worker.ts +68 -0
- package/src/module-loader.ts +85 -0
- package/src/service-worker.ts +78 -6
- package/src/setup.ts +194 -7
- package/src/site.ts +230 -92
- package/src/types.ts +97 -0
- package/src/vite/service-worker-plugin.ts +4 -0
package/dist/site.js
CHANGED
|
@@ -11,24 +11,26 @@
|
|
|
11
11
|
* site's `main.ts`. Non-UI consumers should import the package default (which
|
|
12
12
|
* only does SW registration and the automerge-worker handoff).
|
|
13
13
|
*/
|
|
14
|
-
import {
|
|
14
|
+
import { initializeWasm, isValidAutomergeUrl, isValidDocumentId, MessageChannelNetworkAdapter, parseAutomergeUrl, Repo, stringifyAutomergeUrl, } from "@automerge/vanillajs/slim";
|
|
15
|
+
import { IndexedDBWorkerStorageAdapter } from "@automerge/automerge-repo-storage-indexeddb/IndexedDBWorkerStorageAdapter";
|
|
15
16
|
import * as Automerge from "@automerge/automerge/slim";
|
|
16
17
|
import * as AutomergeRepo from "@automerge/automerge-repo/slim";
|
|
17
18
|
import { initKeyhiveWasm, initializeAutomergeRepoKeyhiveWithRepo, } from "@automerge/automerge-repo-keyhive";
|
|
18
19
|
// eslint-disable-next-line
|
|
19
20
|
// @ts-ignore — initSync is a wasm-bindgen runtime helper not in the .d.ts
|
|
20
21
|
import { initSync as initSubductionSync } from "@automerge/automerge-subduction/slim";
|
|
22
|
+
import { MemorySigner } from "@automerge/automerge-subduction/slim";
|
|
21
23
|
const siteName = typeof __SITE_NAME__ !== "undefined" ? __SITE_NAME__ : "tiny-patchwork";
|
|
22
24
|
const useKeyhiveSyncServer = typeof __KEYHIVE_SYNC_SERVER__ !== "undefined" && __KEYHIVE_SYNC_SERVER__;
|
|
23
25
|
import { ModuleWatcher } from "@inkandswitch/patchwork-filesystem";
|
|
26
|
+
import { importAutomergeModuleViaWorker } from "./module-loader.js";
|
|
24
27
|
import { openDocument, registerPatchworkViewElement, } from "@inkandswitch/patchwork-elements";
|
|
25
28
|
import { registerRepoProviderElement } from "@inkandswitch/patchwork-providers";
|
|
26
29
|
import { getRegistry, registerPlugins, resolveAccountHandle, unregisterPlugins, } from "@inkandswitch/patchwork-plugins";
|
|
27
30
|
import * as plugins from "@inkandswitch/patchwork-plugins";
|
|
28
|
-
import setupServiceWorker from "./setup.js";
|
|
31
|
+
import setupServiceWorker, { lifecycleLoggingEnabled, } from "./setup.js";
|
|
29
32
|
import debug from "debug";
|
|
30
33
|
const log = debug("patchwork:bootloader:site");
|
|
31
|
-
const DEFAULT_REMOTE_STORAGE_ID = "3760df37-a4c6-4f66-9ecd-732039a9385d";
|
|
32
34
|
// Legacy big-patchwork hash shape: `slug--<documentId>[?=type]`.
|
|
33
35
|
const BIG_PATCHWORK_HASH_REGEX = /(?<title>[A-Za-z0-9-]+)--(?<docId>[1-9A-HJ-NP-Za-km-z]+)(?<type>\?=[^&?]+)?/;
|
|
34
36
|
const [automergeWasm, subductionWasm] = await Promise.all([
|
|
@@ -48,53 +50,88 @@ export async function bootPatchworkSite(config) {
|
|
|
48
50
|
const defaultModuleSources = resolveDefaultModules(config);
|
|
49
51
|
showLoadingAnimation();
|
|
50
52
|
log(`booting`, config);
|
|
53
|
+
installLifecycleLogging();
|
|
51
54
|
await initializeWasm(automergeWasm);
|
|
52
55
|
initSubductionSync(subductionWasm);
|
|
56
|
+
log("enabling workers");
|
|
53
57
|
const sw = await setupServiceWorker();
|
|
54
58
|
if (!sw)
|
|
55
59
|
throw new Error("Failed to set up service worker");
|
|
60
|
+
log("workers ready");
|
|
56
61
|
let hive;
|
|
57
|
-
// Get the initial automerge-worker port via subscribeToRepoChannel,
|
|
58
|
-
// then pass it to keyhive init which wraps it in its own network adapter.
|
|
59
|
-
let resolvePort;
|
|
60
|
-
const portPromise = new Promise((r) => {
|
|
61
|
-
resolvePort = r;
|
|
62
|
-
});
|
|
63
|
-
await sw.subscribeToRepoChannel(resolvePort);
|
|
64
|
-
const workerPort = await portPromise;
|
|
65
62
|
let repo;
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
onlyShareWithHardcodedServerPeerId: false,
|
|
76
|
-
// ARK selects the relay via `syncServer` ("keyhive" | "subduction").
|
|
77
|
-
// Defaults to "subduction".
|
|
78
|
-
...(useKeyhiveSyncServer ? { syncServer: "keyhive" } : {}),
|
|
79
|
-
repo: {
|
|
80
|
-
storage: new IndexedDBStorageAdapter(),
|
|
81
|
-
enableRemoteHeadsGossiping: true,
|
|
82
|
-
},
|
|
83
|
-
}));
|
|
63
|
+
let tabSignerIdentity;
|
|
64
|
+
// If a Repo is already on `window` — an embedding context provided one before
|
|
65
|
+
// this entry ran — reuse it and its keyhive instead of standing up a fresh
|
|
66
|
+
// realm-local Repo, so we share the same documents and sync/keyhive context.
|
|
67
|
+
// Otherwise create our own below.
|
|
68
|
+
if (window.repo) {
|
|
69
|
+
log("using existing Repo from window");
|
|
70
|
+
repo = window.repo;
|
|
71
|
+
hive = window.hive;
|
|
84
72
|
}
|
|
85
73
|
else {
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
},
|
|
92
|
-
enableRemoteHeadsGossiping: true,
|
|
93
|
-
peerId: `${config.titleSuffix}-tab-${crypto.randomUUID()}`,
|
|
74
|
+
// Get the initial automerge-worker port via subscribeToRepoChannel,
|
|
75
|
+
// then pass it to keyhive init which wraps it in its own network adapter.
|
|
76
|
+
let resolvePort;
|
|
77
|
+
const portPromise = new Promise((r) => {
|
|
78
|
+
resolvePort = r;
|
|
94
79
|
});
|
|
80
|
+
log("subscribing to repo channel");
|
|
81
|
+
await sw.subscribeToRepoChannel(resolvePort);
|
|
82
|
+
log("repo channel subscribed");
|
|
83
|
+
const workerPort = await portPromise;
|
|
84
|
+
if (config.keyhive) {
|
|
85
|
+
log("setting up keyhive");
|
|
86
|
+
initKeyhiveWasm();
|
|
87
|
+
({ hive, repo } = await initializeAutomergeRepoKeyhiveWithRepo({
|
|
88
|
+
createRepo: (config) => new Repo(config),
|
|
89
|
+
storage: new IndexedDBWorkerStorageAdapter(`${siteName}-keyhive`),
|
|
90
|
+
peerIdSuffix: siteName + Math.random().toString(36).slice(2),
|
|
91
|
+
networkAdapter: new MessageChannelNetworkAdapter(workerPort),
|
|
92
|
+
automaticArchiveIngestion: true,
|
|
93
|
+
cachingMode: "periodic",
|
|
94
|
+
onlyShareWithHardcodedServerPeerId: false,
|
|
95
|
+
// ARK selects the relay via `syncServer` ("keyhive" | "subduction").
|
|
96
|
+
// Defaults to "subduction".
|
|
97
|
+
...(useKeyhiveSyncServer ? { syncServer: "keyhive" } : {}),
|
|
98
|
+
repo: {
|
|
99
|
+
storage: new IndexedDBWorkerStorageAdapter(),
|
|
100
|
+
enableRemoteHeadsGossiping: true,
|
|
101
|
+
},
|
|
102
|
+
}));
|
|
103
|
+
log("keyhive setup complete");
|
|
104
|
+
}
|
|
105
|
+
else {
|
|
106
|
+
log("creating repo");
|
|
107
|
+
// Pass an explicit signer (instead of the Repo's internal default) so we
|
|
108
|
+
// can expose tab signer identity on window.patchwork for dev inspection.
|
|
109
|
+
// The tab never connects via Subduction (no endpoints/adapters), so this
|
|
110
|
+
// id never goes on the wire.
|
|
111
|
+
const tabSigner = new MemorySigner();
|
|
112
|
+
repo = new Repo({
|
|
113
|
+
network: [new MessageChannelNetworkAdapter(workerPort)],
|
|
114
|
+
storage: new IndexedDBWorkerStorageAdapter(),
|
|
115
|
+
signer: tabSigner,
|
|
116
|
+
async sharePolicy(peerId) {
|
|
117
|
+
return peerId.includes("automerge-worker");
|
|
118
|
+
},
|
|
119
|
+
enableRemoteHeadsGossiping: true,
|
|
120
|
+
peerId: `${config.titleSuffix}-tab-${crypto.randomUUID()}`,
|
|
121
|
+
});
|
|
122
|
+
tabSignerIdentity = {
|
|
123
|
+
peerId: tabSigner.peerId().toString(),
|
|
124
|
+
verifyingKey: tabSigner.verifyingKey().toHex(),
|
|
125
|
+
};
|
|
126
|
+
console.log("[patchwork] tab subduction identity:", tabSignerIdentity);
|
|
127
|
+
log("repo created");
|
|
128
|
+
}
|
|
95
129
|
}
|
|
96
|
-
|
|
130
|
+
log("popping repo on window");
|
|
131
|
+
window.repo = repo;
|
|
132
|
+
log("await repo.networkSubsystem.whenReady()");
|
|
97
133
|
await repo.networkSubsystem.whenReady();
|
|
134
|
+
log("networkSubsystem ready");
|
|
98
135
|
if (hive) {
|
|
99
136
|
hive.networkAdapter.syncKeyhive?.();
|
|
100
137
|
}
|
|
@@ -114,7 +151,10 @@ export async function bootPatchworkSite(config) {
|
|
|
114
151
|
// `resolveAccountHandle` below has something to await on (the `account`
|
|
115
152
|
// datatype lives in that bundle today). The user's own module-settings URL
|
|
116
153
|
// is added lazily once it appears on the account doc — see below.
|
|
117
|
-
const moduleWatcher = new ModuleWatcher(repo, buildSystemSources(defaultModuleSources), onModuleLoaded, unregisterPlugins
|
|
154
|
+
const moduleWatcher = new ModuleWatcher(repo, buildSystemSources(defaultModuleSources), onModuleLoaded, unregisterPlugins,
|
|
155
|
+
// Discover an Automerge package's plugin descriptors off the main thread;
|
|
156
|
+
// each plugin's load() re-imports the package (at heads) on this thread.
|
|
157
|
+
importAutomergeModuleViaWorker);
|
|
118
158
|
const accountDocHandle = (await resolveAccountHandle(repo, {
|
|
119
159
|
storageKey: config.accountStorageKey,
|
|
120
160
|
hive,
|
|
@@ -130,9 +170,11 @@ export async function bootPatchworkSite(config) {
|
|
|
130
170
|
packages: moduleWatcher,
|
|
131
171
|
plugins,
|
|
132
172
|
accountDocHandle,
|
|
173
|
+
...(tabSignerIdentity ? { signer: tabSignerIdentity } : {}),
|
|
133
174
|
sw: {
|
|
134
175
|
connectClassicSync: sw.connectClassicSync,
|
|
135
176
|
subscribeToRepoChannel: sw.subscribeToRepoChannel,
|
|
177
|
+
subscribeSyncState: sw.subscribeSyncState,
|
|
136
178
|
},
|
|
137
179
|
};
|
|
138
180
|
window.uncache = uncache;
|
|
@@ -164,9 +206,7 @@ function isValidModuleSource(source) {
|
|
|
164
206
|
* built-in default bundle).
|
|
165
207
|
*/
|
|
166
208
|
function resolveDefaultModules(config) {
|
|
167
|
-
const builtin = config.defaultModules ??
|
|
168
|
-
config.defaultModulesUrl ??
|
|
169
|
-
[];
|
|
209
|
+
const builtin = config.defaultModules ?? config.defaultModulesUrl ?? [];
|
|
170
210
|
const builtinList = (Array.isArray(builtin) ? builtin : [builtin]).filter(Boolean);
|
|
171
211
|
const override = globalThis.localStorage?.getItem("defaultToolsUrl");
|
|
172
212
|
if (override) {
|
|
@@ -206,6 +246,33 @@ function installDevConsoleGlobals(repo, hive, getRepoChannel) {
|
|
|
206
246
|
}
|
|
207
247
|
window.getRepoChannel = getRepoChannel;
|
|
208
248
|
}
|
|
249
|
+
/**
|
|
250
|
+
* Log this tab's Page Lifecycle + connectivity transitions (visibility,
|
|
251
|
+
* freeze, bfcache, online/offline) so they line up against the SharedWorker's
|
|
252
|
+
* sync-socket reaps. [lifecycle]-tagged, on by default.
|
|
253
|
+
*/
|
|
254
|
+
function installLifecycleLogging() {
|
|
255
|
+
if (typeof document === "undefined")
|
|
256
|
+
return;
|
|
257
|
+
const opts = { capture: true };
|
|
258
|
+
const note = (label, extra) => {
|
|
259
|
+
if (!lifecycleLoggingEnabled())
|
|
260
|
+
return;
|
|
261
|
+
const msg = `[lifecycle] ${new Date().toISOString()} ${label}`;
|
|
262
|
+
if (extra === undefined)
|
|
263
|
+
console.info(msg);
|
|
264
|
+
else
|
|
265
|
+
console.info(msg, extra);
|
|
266
|
+
};
|
|
267
|
+
document.addEventListener("visibilitychange", () => note(`visibilitychange → ${document.visibilityState}`), opts);
|
|
268
|
+
document.addEventListener("freeze", () => note("freeze (tab suspended)"), opts);
|
|
269
|
+
document.addEventListener("resume", () => note("resume (tab unsuspended)"), opts);
|
|
270
|
+
window.addEventListener("pageshow", e => note("pageshow", { persisted: e.persisted }), opts);
|
|
271
|
+
window.addEventListener("pagehide", e => note("pagehide", { persisted: e.persisted }), opts);
|
|
272
|
+
window.addEventListener("online", () => note("online"), opts);
|
|
273
|
+
window.addEventListener("offline", () => note("offline"), opts);
|
|
274
|
+
note(`lifecycle logging installed (visibilityState=${document.visibilityState}, hasFocus=${document.hasFocus()})`);
|
|
275
|
+
}
|
|
209
276
|
function onModuleLoaded(name, mod) {
|
|
210
277
|
if (Array.isArray(mod.plugins)) {
|
|
211
278
|
log(`registering ${mod.plugins.length} plugin(s) from ${name.slice(0, 30)}...`, mod.plugins.map((p) => `${p.type}:${p.id}`));
|
|
@@ -244,10 +311,7 @@ function primeRootElement(rootElement, accountDocHandle) {
|
|
|
244
311
|
const initialParams = new URLSearchParams(location.hash.slice(1));
|
|
245
312
|
if (initialParams.has("frame")) {
|
|
246
313
|
rootElement.setAttribute("tool-id", initialParams.get("frame"));
|
|
247
|
-
const
|
|
248
|
-
const docUrl = docId
|
|
249
|
-
? stringifyAutomergeUrl({ documentId: docId })
|
|
250
|
-
: accountDocHandle.url;
|
|
314
|
+
const docUrl = docParamToUrl(initialParams.get("doc")) ?? accountDocHandle.url;
|
|
251
315
|
rootElement.setAttribute("doc-url", docUrl);
|
|
252
316
|
}
|
|
253
317
|
else {
|
|
@@ -333,20 +397,55 @@ async function uncache(match) {
|
|
|
333
397
|
}
|
|
334
398
|
}
|
|
335
399
|
}
|
|
400
|
+
// Keys whose values are automerge URLs — we keep the `:` (and any `#`/`|`
|
|
401
|
+
// heads) literal rather than percent-encoding them so links stay readable.
|
|
402
|
+
const RAW_HASH_KEYS = new Set(["doc", "package"]);
|
|
403
|
+
// Emit hash params in a stable order so re-serializing the same logical
|
|
404
|
+
// params yields a byte-identical string (avoids spurious `hashchange`).
|
|
405
|
+
const HASH_KEY_ORDER = ["doc", "package", "tool", "type", "title", "frame"];
|
|
406
|
+
function serializeHashParams(params) {
|
|
407
|
+
const emitted = new Set();
|
|
408
|
+
const parts = [];
|
|
409
|
+
const emit = (key) => {
|
|
410
|
+
if (emitted.has(key))
|
|
411
|
+
return;
|
|
412
|
+
const value = params.get(key);
|
|
413
|
+
if (!value)
|
|
414
|
+
return;
|
|
415
|
+
emitted.add(key);
|
|
416
|
+
parts.push(`${key}=${RAW_HASH_KEYS.has(key) ? value : encodeURIComponent(value)}`);
|
|
417
|
+
};
|
|
418
|
+
for (const key of HASH_KEY_ORDER)
|
|
419
|
+
emit(key);
|
|
420
|
+
for (const key of params.keys())
|
|
421
|
+
emit(key);
|
|
422
|
+
return parts.join("&");
|
|
423
|
+
}
|
|
424
|
+
/**
|
|
425
|
+
* Coerce a `doc=` hash param to a full automerge URL. Accepts a full URL
|
|
426
|
+
* (`automerge:<id>[#heads]`) or a bare document id for backwards
|
|
427
|
+
* compatibility with older links.
|
|
428
|
+
*/
|
|
429
|
+
function docParamToUrl(docParam) {
|
|
430
|
+
if (!docParam)
|
|
431
|
+
return undefined;
|
|
432
|
+
if (isValidAutomergeUrl(docParam))
|
|
433
|
+
return docParam;
|
|
434
|
+
const documentId = docParam.replace(/^automerge:/, "");
|
|
435
|
+
if (isValidDocumentId(documentId)) {
|
|
436
|
+
return stringifyAutomergeUrl({ documentId: documentId });
|
|
437
|
+
}
|
|
438
|
+
return undefined;
|
|
439
|
+
}
|
|
336
440
|
function installHashRouting(params) {
|
|
337
441
|
const { rootElement, repo, accountDocHandle, moduleWatcher, titleSuffix } = params;
|
|
338
|
-
rootElement.addEventListener("patchwork:no-tool", (event) => {
|
|
339
|
-
moduleWatcher.loadSuggestedImportUrl(event.detail.url);
|
|
340
|
-
});
|
|
341
442
|
rootElement.addEventListener("patchwork:open-document", async (event) => {
|
|
342
443
|
const params = new URLSearchParams(window.location.hash.slice(1));
|
|
343
444
|
const { url, toolId, type, title } = event.detail;
|
|
344
|
-
|
|
345
|
-
|
|
346
|
-
|
|
347
|
-
|
|
348
|
-
else
|
|
349
|
-
params.delete("heads");
|
|
445
|
+
// `doc` is now the full automerge URL (heads, if any, live in it). The
|
|
446
|
+
// in-use package is recorded separately in `package=` when a tool mounts.
|
|
447
|
+
params.delete("heads");
|
|
448
|
+
params.set("doc", url);
|
|
350
449
|
if (toolId)
|
|
351
450
|
params.set("tool", toolId);
|
|
352
451
|
else
|
|
@@ -359,9 +458,9 @@ function installHashRouting(params) {
|
|
|
359
458
|
params.set("type", type);
|
|
360
459
|
else
|
|
361
460
|
params.delete("type");
|
|
362
|
-
window.location.hash = params
|
|
461
|
+
window.location.hash = serializeHashParams(params);
|
|
363
462
|
try {
|
|
364
|
-
const docHandle = await repo.find(
|
|
463
|
+
const docHandle = await repo.find(url);
|
|
365
464
|
const doc = docHandle.doc();
|
|
366
465
|
const docType = type || doc?.["@patchwork"]?.type;
|
|
367
466
|
if (!docType)
|
|
@@ -379,6 +478,29 @@ function installHashRouting(params) {
|
|
|
379
478
|
console.error("Failed to update document title", e);
|
|
380
479
|
}
|
|
381
480
|
});
|
|
481
|
+
// When a tool mounts we know the package actually rendering the top-level
|
|
482
|
+
// document, so record its (heads-pinned) importUrl in `package=`.
|
|
483
|
+
const recordInUsePackage = (event) => {
|
|
484
|
+
const detail = event.detail;
|
|
485
|
+
if (!detail || !("url" in detail) || !detail.importUrl)
|
|
486
|
+
return;
|
|
487
|
+
const params = new URLSearchParams(window.location.hash.slice(1));
|
|
488
|
+
const docUrl = docParamToUrl(params.get("doc"));
|
|
489
|
+
if (!docUrl)
|
|
490
|
+
return;
|
|
491
|
+
// Ignore nested branch / side-by-side views: only the top-level doc's
|
|
492
|
+
// package belongs in the hash.
|
|
493
|
+
if (parseAutomergeUrl(docUrl).documentId !==
|
|
494
|
+
parseAutomergeUrl(detail.url).documentId) {
|
|
495
|
+
return;
|
|
496
|
+
}
|
|
497
|
+
if (params.get("package") === detail.importUrl)
|
|
498
|
+
return;
|
|
499
|
+
params.set("package", detail.importUrl);
|
|
500
|
+
// Replace rather than push: recording the in-use package shouldn't add a
|
|
501
|
+
// back-button entry (and replaceState avoids a `hashchange` round-trip).
|
|
502
|
+
history.replaceState(null, "", `#${serializeHashParams(params)}`);
|
|
503
|
+
};
|
|
382
504
|
let firstMount = true;
|
|
383
505
|
const reveal = () => {
|
|
384
506
|
if (!firstMount)
|
|
@@ -388,6 +510,7 @@ function installHashRouting(params) {
|
|
|
388
510
|
hideLoadingAnimation();
|
|
389
511
|
};
|
|
390
512
|
rootElement.addEventListener("patchwork:mounted", (event) => {
|
|
513
|
+
recordInUsePackage(event);
|
|
391
514
|
handleHashChange();
|
|
392
515
|
if (event.target !== rootElement)
|
|
393
516
|
return;
|
|
@@ -412,34 +535,34 @@ function installHashRouting(params) {
|
|
|
412
535
|
}
|
|
413
536
|
// Bare automerge URL in hash: /#automerge:<documentId>
|
|
414
537
|
if (isValidAutomergeUrl(hash)) {
|
|
415
|
-
const
|
|
538
|
+
const url = hash;
|
|
416
539
|
window.location.hash = "";
|
|
417
|
-
openDocument(rootElement,
|
|
540
|
+
openDocument(rootElement, url);
|
|
418
541
|
return;
|
|
419
542
|
}
|
|
420
543
|
const params = new URLSearchParams(hash);
|
|
421
|
-
const
|
|
422
|
-
const
|
|
544
|
+
const docUrl = docParamToUrl(params.get("doc"));
|
|
545
|
+
const packageUrl = params.get("package");
|
|
423
546
|
const toolId = params.get("tool");
|
|
424
547
|
const title = params.get("title");
|
|
425
548
|
const type = params.get("type");
|
|
426
549
|
const frame = params.get("frame");
|
|
550
|
+
// Load the package that produced this document so its tool is available
|
|
551
|
+
// even when it isn't in the user's module settings.
|
|
552
|
+
if (packageUrl) {
|
|
553
|
+
void moduleWatcher.loadModules([packageUrl]);
|
|
554
|
+
}
|
|
427
555
|
if (frame) {
|
|
428
|
-
const
|
|
556
|
+
const frameDocUrl = docUrl ?? accountDocHandle.url;
|
|
429
557
|
if (rootElement.getAttribute("tool-id") !== frame ||
|
|
430
|
-
rootElement.getAttribute("doc-url") !==
|
|
558
|
+
rootElement.getAttribute("doc-url") !== frameDocUrl) {
|
|
431
559
|
rootElement.setAttribute("tool-id", frame);
|
|
432
|
-
rootElement.setAttribute("doc-url",
|
|
560
|
+
rootElement.setAttribute("doc-url", frameDocUrl);
|
|
433
561
|
}
|
|
434
562
|
}
|
|
435
|
-
if (
|
|
563
|
+
if (docUrl) {
|
|
436
564
|
rootElement.dispatchEvent(new CustomEvent("patchwork:open-document", {
|
|
437
|
-
detail: {
|
|
438
|
-
url: stringifyAutomergeUrl({ documentId, heads }),
|
|
439
|
-
toolId,
|
|
440
|
-
title,
|
|
441
|
-
type,
|
|
442
|
-
},
|
|
565
|
+
detail: { url: docUrl, toolId, title, type },
|
|
443
566
|
}));
|
|
444
567
|
}
|
|
445
568
|
};
|
package/dist/types.d.ts
CHANGED
|
@@ -5,6 +5,68 @@
|
|
|
5
5
|
* reintroduced when either of them restarts — and so tabs can listen in.
|
|
6
6
|
*/
|
|
7
7
|
export declare const HANDOFF_CHANNEL = "@patchwork/handoff";
|
|
8
|
+
/**
|
|
9
|
+
* BroadcastChannel on which the automerge shared worker announces remote
|
|
10
|
+
* heads it learns about from the sync server. Any tab can listen to stay
|
|
11
|
+
* informed of sync progress without repo-to-repo gossiping.
|
|
12
|
+
*/
|
|
13
|
+
export declare const SYNCSTATE_CHANNEL = "@patchwork/syncstate";
|
|
14
|
+
/**
|
|
15
|
+
* Worker → tabs: the worker's Subduction link to the sync server flipped.
|
|
16
|
+
* `serverPeerIds` are the directly-connected sync-server peer ids (their
|
|
17
|
+
* verifying keys), so a tab can tell which peer rows are *the server* and
|
|
18
|
+
* judge "synced" against them specifically.
|
|
19
|
+
*/
|
|
20
|
+
export interface SyncStateConnectionMessage {
|
|
21
|
+
type: "connection";
|
|
22
|
+
connected: boolean;
|
|
23
|
+
serverPeerIds: string[];
|
|
24
|
+
}
|
|
25
|
+
/**
|
|
26
|
+
* Worker → tabs: the shared worker's own Subduction identity, so a tab can
|
|
27
|
+
* tell which peer rows are "us". `peerId` is `signer.peerId().toString()` (the
|
|
28
|
+
* value that shows up as a peer id); `verifyingKey` is its hex Ed25519 key.
|
|
29
|
+
*/
|
|
30
|
+
export interface SyncStateWhoAmIMessage {
|
|
31
|
+
type: "whoami";
|
|
32
|
+
peerId: string;
|
|
33
|
+
verifyingKey: string;
|
|
34
|
+
}
|
|
35
|
+
export type SyncStateBroadcast = SyncStateConnectionMessage | SyncStateWhoAmIMessage;
|
|
36
|
+
/**
|
|
37
|
+
* Tab → worker: please replay the current global sync signals (whoami +
|
|
38
|
+
* connection) so a freshly-opened tab can orient immediately. Per-document
|
|
39
|
+
* heads are no longer replayed here — a tab subscribes to the specific docs it
|
|
40
|
+
* cares about over its control port instead (see {@link SyncSubscribeMessage}).
|
|
41
|
+
*/
|
|
42
|
+
export interface SyncStateRequestMessage {
|
|
43
|
+
type: "request";
|
|
44
|
+
/** @deprecated ignored — per-doc state is delivered via sync-sub now. */
|
|
45
|
+
documentId?: string;
|
|
46
|
+
}
|
|
47
|
+
/** Tab → worker: start pushing me this document's heads (replays current state). */
|
|
48
|
+
export interface SyncSubscribeMessage {
|
|
49
|
+
type: "sync-sub";
|
|
50
|
+
documentId: string;
|
|
51
|
+
}
|
|
52
|
+
/** Tab → worker: stop pushing me this document's heads. */
|
|
53
|
+
export interface SyncUnsubscribeMessage {
|
|
54
|
+
type: "sync-unsub";
|
|
55
|
+
documentId: string;
|
|
56
|
+
}
|
|
57
|
+
/**
|
|
58
|
+
* Worker → tab (control port): a peer's heads for a subscribed document — the
|
|
59
|
+
* worker's own (keyed by its peerId) or a Subduction peer's (keyed by its
|
|
60
|
+
* verifying-key storageId). Same payload as the old broadcast remote-heads
|
|
61
|
+
* message, but delivered only to the tabs that asked for this document.
|
|
62
|
+
*/
|
|
63
|
+
export interface SyncStateDocMessage {
|
|
64
|
+
type: "sync-state";
|
|
65
|
+
documentId: string;
|
|
66
|
+
storageId: string;
|
|
67
|
+
heads: string[];
|
|
68
|
+
timestamp: number;
|
|
69
|
+
}
|
|
8
70
|
/**
|
|
9
71
|
* The special URL to resolve, plus enough of the {@link Request} the service
|
|
10
72
|
* worker is holding that the automerge worker can construct one that
|
|
@@ -93,9 +155,19 @@ export type SetupServiceWorkerOptions = {
|
|
|
93
155
|
};
|
|
94
156
|
export type ServiceWorkerRepoChannelListener = (port: MessagePort) => void | Promise<void>;
|
|
95
157
|
export type SetupServiceWorkerResult = {
|
|
158
|
+
shared?: SharedWorker;
|
|
159
|
+
kill?: () => void;
|
|
96
160
|
/** Open a classic Automerge sync WebSocket from the automerge worker. */
|
|
97
161
|
connectClassicSync: (server?: string) => Promise<void>;
|
|
98
162
|
subscribeToRepoChannel: (listener: ServiceWorkerRepoChannelListener) => Promise<() => void>;
|
|
99
163
|
/** Open a fresh repo sync port to the automerge worker (dev console). */
|
|
100
164
|
getRepoChannel: () => MessagePort;
|
|
165
|
+
/**
|
|
166
|
+
* Watch one document's sync heads (this tab's own and each Subduction peer's,
|
|
167
|
+
* as the worker learns them). Calls `listener` on every update for that doc,
|
|
168
|
+
* replaying the current state on subscribe. Returns an unsubscribe function;
|
|
169
|
+
* the worker stops pushing the doc once the last local watcher drops it (and
|
|
170
|
+
* automatically if this tab goes away).
|
|
171
|
+
*/
|
|
172
|
+
subscribeSyncState: (documentId: string, listener: (update: SyncStateDocMessage) => void) => () => void;
|
|
101
173
|
};
|
package/dist/types.js
CHANGED
|
@@ -5,3 +5,9 @@
|
|
|
5
5
|
* reintroduced when either of them restarts — and so tabs can listen in.
|
|
6
6
|
*/
|
|
7
7
|
export const HANDOFF_CHANNEL = "@patchwork/handoff";
|
|
8
|
+
/**
|
|
9
|
+
* BroadcastChannel on which the automerge shared worker announces remote
|
|
10
|
+
* heads it learns about from the sync server. Any tab can listen to stay
|
|
11
|
+
* informed of sync progress without repo-to-repo gossiping.
|
|
12
|
+
*/
|
|
13
|
+
export const SYNCSTATE_CHANNEL = "@patchwork/syncstate";
|
|
@@ -12,6 +12,10 @@ const workers = [
|
|
|
12
12
|
specifier: "@inkandswitch/patchwork-bootloader/automerge-worker",
|
|
13
13
|
fileName: "automerge-worker.js",
|
|
14
14
|
},
|
|
15
|
+
{
|
|
16
|
+
specifier: "@inkandswitch/patchwork-bootloader/module-loader-worker",
|
|
17
|
+
fileName: "module-loader-worker.js",
|
|
18
|
+
},
|
|
15
19
|
];
|
|
16
20
|
export function serviceworker() {
|
|
17
21
|
const entryIds = new Set();
|
package/package.json
CHANGED
|
@@ -1,11 +1,11 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@inkandswitch/patchwork-bootloader",
|
|
3
|
-
"version": "0.3.
|
|
3
|
+
"version": "0.3.2",
|
|
4
4
|
"author": "chee",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"license": "MIT",
|
|
7
7
|
"devDependencies": {
|
|
8
|
-
"@automerge/automerge-repo-keyhive": "0.3.0-alpha.sub.
|
|
8
|
+
"@automerge/automerge-repo-keyhive": "0.3.0-alpha.sub.8b",
|
|
9
9
|
"esbuild": "^0.23.1",
|
|
10
10
|
"rollup": "^4.61.1"
|
|
11
11
|
},
|
|
@@ -30,6 +30,10 @@
|
|
|
30
30
|
"import": "./dist/automerge-worker.js",
|
|
31
31
|
"types": "./dist/automerge-worker.d.ts"
|
|
32
32
|
},
|
|
33
|
+
"./module-loader-worker": {
|
|
34
|
+
"import": "./dist/module-loader-worker.js",
|
|
35
|
+
"types": "./dist/module-loader-worker.d.ts"
|
|
36
|
+
},
|
|
33
37
|
"./site": {
|
|
34
38
|
"import": "./dist/site.js",
|
|
35
39
|
"types": "./dist/site.d.ts"
|
|
@@ -41,28 +45,28 @@
|
|
|
41
45
|
},
|
|
42
46
|
"dependencies": {
|
|
43
47
|
"@automerge/automerge": "3.3.0-fragments.1",
|
|
44
|
-
"@automerge/automerge-repo": "2.6.0-subduction.
|
|
45
|
-
"@automerge/automerge-repo-network-messagechannel": "2.6.0-subduction.
|
|
46
|
-
"@automerge/automerge-repo-network-websocket": "2.6.0-subduction.
|
|
47
|
-
"@automerge/automerge-repo-storage-indexeddb": "2.6.0-subduction.
|
|
48
|
+
"@automerge/automerge-repo": "2.6.0-subduction.39",
|
|
49
|
+
"@automerge/automerge-repo-network-messagechannel": "2.6.0-subduction.39",
|
|
50
|
+
"@automerge/automerge-repo-network-websocket": "2.6.0-subduction.39",
|
|
51
|
+
"@automerge/automerge-repo-storage-indexeddb": "2.6.0-subduction.39",
|
|
48
52
|
"@automerge/automerge-subduction": "0.16.0",
|
|
49
|
-
"@automerge/vanillajs": "2.6.0-subduction.
|
|
50
|
-
"@keyhive/keyhive": "0.1.0-alpha.
|
|
53
|
+
"@automerge/vanillajs": "2.6.0-subduction.39",
|
|
54
|
+
"@keyhive/keyhive": "0.1.0-alpha.5",
|
|
51
55
|
"@types/debug": "^4.1.13",
|
|
52
56
|
"debug": "^4.4.3",
|
|
53
57
|
"resolve.exports": "^2.0.3",
|
|
54
58
|
"service-worker-types": "npm:@types/serviceworker@^0.0.153",
|
|
55
59
|
"tinyargs": "^0.1.4",
|
|
56
|
-
"@inkandswitch/patchwork-
|
|
60
|
+
"@inkandswitch/patchwork-elements": "^2.0.0",
|
|
61
|
+
"@inkandswitch/patchwork-filesystem": "^0.1.1",
|
|
57
62
|
"@inkandswitch/patchwork-plugins": "^0.0.11",
|
|
58
|
-
"@inkandswitch/patchwork-providers": "^0.3.0"
|
|
59
|
-
"@inkandswitch/patchwork-elements": "^2.0.0"
|
|
63
|
+
"@inkandswitch/patchwork-providers": "^0.3.0"
|
|
60
64
|
},
|
|
61
65
|
"peerDependencies": {
|
|
62
66
|
"@automerge/automerge": "3.3.0-fragments.1",
|
|
63
|
-
"@automerge/automerge-repo": "2.6.0-subduction.
|
|
64
|
-
"@automerge/automerge-repo-keyhive": "0.3.0-alpha.sub.
|
|
65
|
-
"@automerge/vanillajs": "2.6.0-subduction.
|
|
67
|
+
"@automerge/automerge-repo": "2.6.0-subduction.39",
|
|
68
|
+
"@automerge/automerge-repo-keyhive": "0.3.0-alpha.sub.8b",
|
|
69
|
+
"@automerge/vanillajs": "2.6.0-subduction.39"
|
|
66
70
|
},
|
|
67
71
|
"scripts": {
|
|
68
72
|
"build": "tsc",
|