@inkandswitch/patchwork-bootloader 0.2.8 → 0.3.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/dist/automerge-worker.js +15 -34
- package/dist/externals.js +1 -0
- package/dist/service-worker.js +3 -1
- package/dist/site.d.ts +23 -7
- package/dist/site.js +75 -26
- package/package.json +13 -13
- package/src/automerge-worker.ts +15 -42
- package/src/externals.ts +1 -0
- package/src/service-worker.ts +3 -1
- package/src/site.ts +131 -45
package/dist/automerge-worker.js
CHANGED
|
@@ -23,7 +23,7 @@ import { resolvePath } from "@inkandswitch/patchwork-filesystem";
|
|
|
23
23
|
import { IndexedDBStorageAdapter } from "@automerge/automerge-repo-storage-indexeddb";
|
|
24
24
|
import { MessageChannelNetworkAdapter } from "@automerge/automerge-repo-network-messagechannel";
|
|
25
25
|
import { WebSocketClientAdapter } from "@automerge/automerge-repo-network-websocket";
|
|
26
|
-
import {
|
|
26
|
+
import { initializeAutomergeRepoKeyhiveRustWithRepo, initKeyhiveWasm, } from "@automerge/automerge-repo-keyhive";
|
|
27
27
|
import { HANDOFF_CHANNEL, } from "./types.js";
|
|
28
28
|
let debugging = false;
|
|
29
29
|
// Sync server selection. Sub is the default. Build with KEYHIVE_SYNC_SERVER=true
|
|
@@ -37,9 +37,6 @@ if (useKeyhiveSyncServer) {
|
|
|
37
37
|
KEYHIVE_SERVER_IDENTITY: "keyhive-sync",
|
|
38
38
|
};
|
|
39
39
|
}
|
|
40
|
-
// keyhive.sync.automerge.org's keyhive identity (issuer d7f41e6f…).
|
|
41
|
-
const KEYHIVE_SYNC_SERVER_PEER_ID = "1/Qebw9O69oH8T/ejYMhFup0tNBh69I3ytGqsmIl358=";
|
|
42
|
-
const KEYHIVE_SYNC_SERVER_CONTACT_CARD_JSON = '{"Rotate":{"payload":{"old":[73,163,230,244,111,233,153,119,133,211,134,237,111,36,52,131,22,50,54,144,150,45,227,235,128,36,33,217,190,198,55,75],"new":[109,115,204,144,178,114,182,238,113,124,4,139,249,76,220,44,128,104,194,68,187,184,82,241,94,145,104,198,159,122,186,43]},"issuer":[215,244,30,111,15,78,235,218,7,241,63,222,141,131,33,22,234,116,180,208,97,235,210,55,202,209,170,178,98,37,223,159],"signature":[178,64,85,76,51,199,196,151,129,14,191,53,127,191,34,223,97,238,95,109,118,179,152,17,205,188,204,177,116,166,147,231,192,201,48,137,19,214,180,45,108,104,34,8,14,63,115,139,215,142,4,179,233,89,150,218,174,168,107,23,8,109,228,6]}}';
|
|
43
40
|
const SUBDUCTION_ENDPOINTS = [
|
|
44
41
|
useKeyhiveSyncServer
|
|
45
42
|
? "wss://keyhive.sync.automerge.org"
|
|
@@ -122,40 +119,23 @@ function getRepoHive() {
|
|
|
122
119
|
return { repo };
|
|
123
120
|
}
|
|
124
121
|
initKeyhiveWasm();
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
let resolveRepoSubduction;
|
|
130
|
-
const repoSubductionPromise = new Promise((resolve) => {
|
|
131
|
-
resolveRepoSubduction = resolve;
|
|
132
|
-
});
|
|
133
|
-
// We use the Rust variant of Keyhive initialization to talk
|
|
134
|
-
// to the Rust keyhive-enabled subduction sync server.
|
|
135
|
-
const hive = await initializeAutomergeRepoKeyhiveRust({
|
|
136
|
-
storage: keyhiveStorage,
|
|
122
|
+
// ARK variant for talking to the keyhive-enabled subduction sync server.
|
|
123
|
+
const { hive, repo } = await initializeAutomergeRepoKeyhiveRustWithRepo({
|
|
124
|
+
createRepo: (config) => new Repo(config),
|
|
125
|
+
storage: new IndexedDBStorageAdapter(`${siteName}-keyhive`),
|
|
137
126
|
peerIdSuffix: `${siteName}-worker` + Math.random().toString(36).slice(2),
|
|
138
|
-
subduction: repoSubductionPromise,
|
|
139
127
|
automaticArchiveIngestion: true,
|
|
140
128
|
cachingMode: "periodic",
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
:
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
storage: new IndexedDBStorageAdapter(),
|
|
151
|
-
signer,
|
|
152
|
-
subductionWebsocketEndpoints: SUBDUCTION_ENDPOINTS,
|
|
153
|
-
peerId: hive.peerId,
|
|
154
|
-
enableRemoteHeadsGossiping: true,
|
|
155
|
-
idFactory: hive.idFactory,
|
|
129
|
+
// ARK selects the relay via `syncServer` ("keyhive" | "subduction"),
|
|
130
|
+
// which pairs the contact card with the matching peer id. Omitting it
|
|
131
|
+
// defaults to "subduction".
|
|
132
|
+
...(useKeyhiveSyncServer ? { syncServer: "keyhive" } : {}),
|
|
133
|
+
repo: {
|
|
134
|
+
storage: new IndexedDBStorageAdapter(),
|
|
135
|
+
subductionWebsocketEndpoints: SUBDUCTION_ENDPOINTS,
|
|
136
|
+
enableRemoteHeadsGossiping: true,
|
|
137
|
+
},
|
|
156
138
|
});
|
|
157
|
-
repo.subduction.then(resolveRepoSubduction);
|
|
158
|
-
hive.linkRepo(repo);
|
|
159
139
|
self.repo = repo;
|
|
160
140
|
self.hive = hive;
|
|
161
141
|
log("repo constructed, waiting for network subsystem");
|
|
@@ -239,6 +219,7 @@ async function connectPort(port, connection) {
|
|
|
239
219
|
}
|
|
240
220
|
});
|
|
241
221
|
keyhiveNetworkAdapter.on("ingest-remote", () => {
|
|
222
|
+
hive.notifySameAgentKeyhiveChange();
|
|
242
223
|
hive.networkAdapter.syncKeyhive?.();
|
|
243
224
|
repo.shareConfigChanged();
|
|
244
225
|
});
|
package/dist/externals.js
CHANGED
package/dist/service-worker.js
CHANGED
|
@@ -163,7 +163,9 @@ self.addEventListener("fetch", (fetchEvent) => {
|
|
|
163
163
|
if (response) {
|
|
164
164
|
if (cacheableStatuses.includes(response.status) &&
|
|
165
165
|
response.url.match(/^https?\:/)) {
|
|
166
|
-
await cache.put(request, response.clone())
|
|
166
|
+
await cache.put(request, response.clone()).catch((error) => {
|
|
167
|
+
log(`error caching ${request.url} in ${cachename}`, error);
|
|
168
|
+
});
|
|
167
169
|
}
|
|
168
170
|
else {
|
|
169
171
|
log(`skipping uncacheable response code from cache: ${response.status} for ${response.url}`);
|
package/dist/site.d.ts
CHANGED
|
@@ -40,16 +40,32 @@ declare global {
|
|
|
40
40
|
}
|
|
41
41
|
export interface SiteConfig {
|
|
42
42
|
/**
|
|
43
|
-
*
|
|
44
|
-
*
|
|
45
|
-
*
|
|
46
|
-
*
|
|
43
|
+
* The site's default tool bundle — the tools every user of this site gets
|
|
44
|
+
* out of the box. Must collectively contribute at least a
|
|
45
|
+
* `patchwork:datatype` registration for `"account"` (typically the one
|
|
46
|
+
* supplied by `@inkandswitch/patchwork-frame`).
|
|
47
|
+
*
|
|
48
|
+
* Each entry is a *module-list source* and may be either:
|
|
49
|
+
* - an Automerge module-settings doc URL (`automerge:...`), which is
|
|
50
|
+
* live-reloaded, or
|
|
51
|
+
* - an HTTP(S) URL (absolute or site-relative, e.g. `/modules.json`) to a
|
|
52
|
+
* static JSON manifest of the shape `{ modules: string[], branches? }`,
|
|
53
|
+
* fetched once at boot.
|
|
54
|
+
*
|
|
55
|
+
* The module URLs *inside* either kind of source may themselves be Automerge
|
|
56
|
+
* folder docs or plain HTTP(S) bundles, so deployment targets can be freely
|
|
57
|
+
* mixed.
|
|
47
58
|
*
|
|
48
59
|
* Can be overridden at runtime by setting `localStorage.defaultToolsUrl` to
|
|
49
|
-
* another automerge
|
|
50
|
-
* unpublished tool set.
|
|
60
|
+
* another `automerge:` URL or manifest URL — useful for local development
|
|
61
|
+
* against an unpublished tool set.
|
|
62
|
+
*/
|
|
63
|
+
defaultModules?: string | string[];
|
|
64
|
+
/**
|
|
65
|
+
* @deprecated Use {@link SiteConfig.defaultModules}. Retained for backwards
|
|
66
|
+
* compatibility with existing sites.
|
|
51
67
|
*/
|
|
52
|
-
defaultModulesUrl
|
|
68
|
+
defaultModulesUrl?: AutomergeUrl;
|
|
53
69
|
/**
|
|
54
70
|
* `localStorage` key under which this site remembers which account document
|
|
55
71
|
* belongs to the current user. Sites sharing an origin MUST use distinct
|
package/dist/site.js
CHANGED
|
@@ -14,11 +14,12 @@
|
|
|
14
14
|
import { IndexedDBStorageAdapter, initializeWasm, isValidAutomergeUrl, isValidDocumentId, MessageChannelNetworkAdapter, parseAutomergeUrl, Repo, stringifyAutomergeUrl, } from "@automerge/vanillajs/slim";
|
|
15
15
|
import * as Automerge from "@automerge/automerge/slim";
|
|
16
16
|
import * as AutomergeRepo from "@automerge/automerge-repo/slim";
|
|
17
|
-
import { initKeyhiveWasm,
|
|
17
|
+
import { initKeyhiveWasm, initializeAutomergeRepoKeyhiveWithRepo, } from "@automerge/automerge-repo-keyhive";
|
|
18
18
|
// eslint-disable-next-line
|
|
19
19
|
// @ts-ignore — initSync is a wasm-bindgen runtime helper not in the .d.ts
|
|
20
20
|
import { initSync as initSubductionSync } from "@automerge/automerge-subduction/slim";
|
|
21
21
|
const siteName = typeof __SITE_NAME__ !== "undefined" ? __SITE_NAME__ : "tiny-patchwork";
|
|
22
|
+
const useKeyhiveSyncServer = typeof __KEYHIVE_SYNC_SERVER__ !== "undefined" && __KEYHIVE_SYNC_SERVER__;
|
|
22
23
|
import { ModuleWatcher } from "@inkandswitch/patchwork-filesystem";
|
|
23
24
|
import { openDocument, registerPatchworkViewElement, } from "@inkandswitch/patchwork-elements";
|
|
24
25
|
import { registerRepoProviderElement } from "@inkandswitch/patchwork-providers";
|
|
@@ -44,7 +45,7 @@ const [automergeWasm, subductionWasm] = await Promise.all([
|
|
|
44
45
|
* do additional wiring after boot.
|
|
45
46
|
*/
|
|
46
47
|
export async function bootPatchworkSite(config) {
|
|
47
|
-
const
|
|
48
|
+
const defaultModuleSources = resolveDefaultModules(config);
|
|
48
49
|
showLoadingAnimation();
|
|
49
50
|
log(`booting`, config);
|
|
50
51
|
await initializeWasm(automergeWasm);
|
|
@@ -61,26 +62,28 @@ export async function bootPatchworkSite(config) {
|
|
|
61
62
|
});
|
|
62
63
|
await sw.subscribeToRepoChannel(resolvePort);
|
|
63
64
|
const workerPort = await portPromise;
|
|
65
|
+
let repo;
|
|
64
66
|
if (config.keyhive) {
|
|
65
67
|
initKeyhiveWasm();
|
|
66
|
-
hive = await
|
|
68
|
+
({ hive, repo } = await initializeAutomergeRepoKeyhiveWithRepo({
|
|
69
|
+
createRepo: (config) => new Repo(config),
|
|
67
70
|
storage: new IndexedDBStorageAdapter(`${siteName}-keyhive`),
|
|
68
71
|
peerIdSuffix: siteName + Math.random().toString(36).slice(2),
|
|
69
72
|
networkAdapter: new MessageChannelNetworkAdapter(workerPort),
|
|
70
73
|
automaticArchiveIngestion: true,
|
|
71
74
|
cachingMode: "periodic",
|
|
72
75
|
onlyShareWithHardcodedServerPeerId: false,
|
|
73
|
-
|
|
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
|
+
}));
|
|
74
84
|
}
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
storage: new IndexedDBStorageAdapter(),
|
|
78
|
-
enableRemoteHeadsGossiping: true,
|
|
79
|
-
network: [hive.networkAdapter],
|
|
80
|
-
peerId: hive.peerId,
|
|
81
|
-
idFactory: hive.idFactory,
|
|
82
|
-
})
|
|
83
|
-
: new Repo({
|
|
85
|
+
else {
|
|
86
|
+
repo = new Repo({
|
|
84
87
|
network: [new MessageChannelNetworkAdapter(workerPort)],
|
|
85
88
|
storage: new IndexedDBStorageAdapter(),
|
|
86
89
|
async sharePolicy(peerId) {
|
|
@@ -89,6 +92,7 @@ export async function bootPatchworkSite(config) {
|
|
|
89
92
|
enableRemoteHeadsGossiping: true,
|
|
90
93
|
peerId: `${config.titleSuffix}-tab-${crypto.randomUUID()}`,
|
|
91
94
|
});
|
|
95
|
+
}
|
|
92
96
|
repo.subscribeToRemotes(config.remoteStorageIds ?? [DEFAULT_REMOTE_STORAGE_ID]);
|
|
93
97
|
await repo.networkSubsystem.whenReady();
|
|
94
98
|
if (hive) {
|
|
@@ -110,7 +114,7 @@ export async function bootPatchworkSite(config) {
|
|
|
110
114
|
// `resolveAccountHandle` below has something to await on (the `account`
|
|
111
115
|
// datatype lives in that bundle today). The user's own module-settings URL
|
|
112
116
|
// is added lazily once it appears on the account doc — see below.
|
|
113
|
-
const moduleWatcher = new ModuleWatcher(repo,
|
|
117
|
+
const moduleWatcher = new ModuleWatcher(repo, buildSystemSources(defaultModuleSources), onModuleLoaded, unregisterPlugins);
|
|
114
118
|
const accountDocHandle = (await resolveAccountHandle(repo, {
|
|
115
119
|
storageKey: config.accountStorageKey,
|
|
116
120
|
hive,
|
|
@@ -142,18 +146,56 @@ export async function bootPatchworkSite(config) {
|
|
|
142
146
|
return { repo, moduleWatcher, accountDocHandle };
|
|
143
147
|
}
|
|
144
148
|
// ─── Internals ──────────────────────────────────────────────────────────
|
|
145
|
-
|
|
149
|
+
/**
|
|
150
|
+
* A module-list source is valid if it is an Automerge URL or looks like an
|
|
151
|
+
* HTTP(S)/site-relative manifest URL.
|
|
152
|
+
*/
|
|
153
|
+
function isValidModuleSource(source) {
|
|
154
|
+
if (isValidAutomergeUrl(source))
|
|
155
|
+
return true;
|
|
156
|
+
return (source.startsWith("/") ||
|
|
157
|
+
source.startsWith("http://") ||
|
|
158
|
+
source.startsWith("https://") ||
|
|
159
|
+
source.startsWith("./"));
|
|
160
|
+
}
|
|
161
|
+
/**
|
|
162
|
+
* Resolve the site's default module-list sources, honouring the
|
|
163
|
+
* `localStorage.defaultToolsUrl` dev override (which replaces the entire
|
|
164
|
+
* built-in default bundle).
|
|
165
|
+
*/
|
|
166
|
+
function resolveDefaultModules(config) {
|
|
167
|
+
const builtin = config.defaultModules ??
|
|
168
|
+
config.defaultModulesUrl ??
|
|
169
|
+
[];
|
|
170
|
+
const builtinList = (Array.isArray(builtin) ? builtin : [builtin]).filter(Boolean);
|
|
146
171
|
const override = globalThis.localStorage?.getItem("defaultToolsUrl");
|
|
147
|
-
if (
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
172
|
+
if (override) {
|
|
173
|
+
if (isValidModuleSource(override)) {
|
|
174
|
+
if (!builtinList.includes(override)) {
|
|
175
|
+
console.info(`using defaultToolsUrl override from localStorage: ${override}`);
|
|
176
|
+
}
|
|
177
|
+
return [override];
|
|
152
178
|
}
|
|
153
|
-
|
|
179
|
+
console.warn(`ignoring invalid defaultToolsUrl in localStorage: ${override}; using built-in default`);
|
|
180
|
+
}
|
|
181
|
+
if (builtinList.length === 0) {
|
|
182
|
+
throw new Error("bootPatchworkSite: no default module sources configured (set `defaultModules`)");
|
|
154
183
|
}
|
|
155
|
-
|
|
156
|
-
|
|
184
|
+
return builtinList;
|
|
185
|
+
}
|
|
186
|
+
/**
|
|
187
|
+
* Turn an ordered list of module-list sources into the name-keyed map the
|
|
188
|
+
* ModuleWatcher expects. The first source keeps the canonical `system` name;
|
|
189
|
+
* additional sources get suffixed names. None may be `user` (reserved for the
|
|
190
|
+
* per-account settings doc, which has branch-override precedence).
|
|
191
|
+
*/
|
|
192
|
+
function buildSystemSources(sources) {
|
|
193
|
+
const map = {};
|
|
194
|
+
sources.forEach((source, index) => {
|
|
195
|
+
const name = index === 0 ? "system" : `system-${index}`;
|
|
196
|
+
map[name] = source;
|
|
197
|
+
});
|
|
198
|
+
return map;
|
|
157
199
|
}
|
|
158
200
|
function installDevConsoleGlobals(repo, hive, getRepoChannel) {
|
|
159
201
|
window.repo = repo;
|
|
@@ -202,7 +244,7 @@ function primeRootElement(rootElement, accountDocHandle) {
|
|
|
202
244
|
const initialParams = new URLSearchParams(location.hash.slice(1));
|
|
203
245
|
if (initialParams.has("frame")) {
|
|
204
246
|
rootElement.setAttribute("tool-id", initialParams.get("frame"));
|
|
205
|
-
const docId = initialParams.get("doc");
|
|
247
|
+
const docId = initialParams.get("doc")?.replace(/^automerge:/, "");
|
|
206
248
|
const docUrl = docId
|
|
207
249
|
? stringifyAutomergeUrl({ documentId: docId })
|
|
208
250
|
: accountDocHandle.url;
|
|
@@ -368,15 +410,22 @@ function installHashRouting(params) {
|
|
|
368
410
|
}
|
|
369
411
|
return;
|
|
370
412
|
}
|
|
413
|
+
// Bare automerge URL in hash: /#automerge:<documentId>
|
|
414
|
+
if (isValidAutomergeUrl(hash)) {
|
|
415
|
+
const { documentId, heads } = parseAutomergeUrl(hash);
|
|
416
|
+
window.location.hash = "";
|
|
417
|
+
openDocument(rootElement, stringifyAutomergeUrl({ documentId, heads }));
|
|
418
|
+
return;
|
|
419
|
+
}
|
|
371
420
|
const params = new URLSearchParams(hash);
|
|
372
|
-
const documentId = params.get("doc");
|
|
421
|
+
const documentId = params.get("doc")?.replace(/^automerge:/, "");
|
|
373
422
|
const heads = params.get("heads")?.split("|");
|
|
374
423
|
const toolId = params.get("tool");
|
|
375
424
|
const title = params.get("title");
|
|
376
425
|
const type = params.get("type");
|
|
377
426
|
const frame = params.get("frame");
|
|
378
427
|
if (frame) {
|
|
379
|
-
const docUrl = params.get("doc") ?? accountDocHandle.url;
|
|
428
|
+
const docUrl = params.get("doc")?.replace(/^automerge:/, "") ?? accountDocHandle.url;
|
|
380
429
|
if (rootElement.getAttribute("tool-id") !== frame ||
|
|
381
430
|
rootElement.getAttribute("doc-url") !== docUrl) {
|
|
382
431
|
rootElement.setAttribute("tool-id", frame);
|
package/package.json
CHANGED
|
@@ -1,11 +1,11 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@inkandswitch/patchwork-bootloader",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.3.0",
|
|
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.6c",
|
|
9
9
|
"esbuild": "^0.23.1",
|
|
10
10
|
"rollup": "^4.61.1"
|
|
11
11
|
},
|
|
@@ -41,28 +41,28 @@
|
|
|
41
41
|
},
|
|
42
42
|
"dependencies": {
|
|
43
43
|
"@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-subduction": "0.
|
|
49
|
-
"@automerge/vanillajs": "2.6.0-subduction.
|
|
50
|
-
"@keyhive/keyhive": "0.
|
|
44
|
+
"@automerge/automerge-repo": "2.6.0-subduction.34",
|
|
45
|
+
"@automerge/automerge-repo-network-messagechannel": "2.6.0-subduction.34",
|
|
46
|
+
"@automerge/automerge-repo-network-websocket": "2.6.0-subduction.34",
|
|
47
|
+
"@automerge/automerge-repo-storage-indexeddb": "2.6.0-subduction.34",
|
|
48
|
+
"@automerge/automerge-subduction": "0.16.0",
|
|
49
|
+
"@automerge/vanillajs": "2.6.0-subduction.34",
|
|
50
|
+
"@keyhive/keyhive": "0.1.0-alpha.3",
|
|
51
51
|
"@types/debug": "^4.1.13",
|
|
52
52
|
"debug": "^4.4.3",
|
|
53
53
|
"resolve.exports": "^2.0.3",
|
|
54
54
|
"service-worker-types": "npm:@types/serviceworker@^0.0.153",
|
|
55
55
|
"tinyargs": "^0.1.4",
|
|
56
|
-
"@inkandswitch/patchwork-filesystem": "^0.0
|
|
56
|
+
"@inkandswitch/patchwork-filesystem": "^0.1.0",
|
|
57
57
|
"@inkandswitch/patchwork-plugins": "^0.0.11",
|
|
58
58
|
"@inkandswitch/patchwork-providers": "^0.3.0",
|
|
59
59
|
"@inkandswitch/patchwork-elements": "^2.0.0"
|
|
60
60
|
},
|
|
61
61
|
"peerDependencies": {
|
|
62
62
|
"@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.
|
|
63
|
+
"@automerge/automerge-repo": "2.6.0-subduction.34",
|
|
64
|
+
"@automerge/automerge-repo-keyhive": "0.3.0-alpha.sub.6c",
|
|
65
|
+
"@automerge/vanillajs": "2.6.0-subduction.34"
|
|
66
66
|
},
|
|
67
67
|
"scripts": {
|
|
68
68
|
"build": "tsc",
|
package/src/automerge-worker.ts
CHANGED
|
@@ -34,7 +34,7 @@ import { IndexedDBStorageAdapter } from "@automerge/automerge-repo-storage-index
|
|
|
34
34
|
import { MessageChannelNetworkAdapter } from "@automerge/automerge-repo-network-messagechannel";
|
|
35
35
|
import { WebSocketClientAdapter } from "@automerge/automerge-repo-network-websocket";
|
|
36
36
|
import {
|
|
37
|
-
|
|
37
|
+
initializeAutomergeRepoKeyhiveRustWithRepo,
|
|
38
38
|
initKeyhiveWasm,
|
|
39
39
|
type AutomergeRepoKeyhiveRust,
|
|
40
40
|
} from "@automerge/automerge-repo-keyhive";
|
|
@@ -68,12 +68,6 @@ if (useKeyhiveSyncServer) {
|
|
|
68
68
|
};
|
|
69
69
|
}
|
|
70
70
|
|
|
71
|
-
// keyhive.sync.automerge.org's keyhive identity (issuer d7f41e6f…).
|
|
72
|
-
const KEYHIVE_SYNC_SERVER_PEER_ID =
|
|
73
|
-
"1/Qebw9O69oH8T/ejYMhFup0tNBh69I3ytGqsmIl358=";
|
|
74
|
-
const KEYHIVE_SYNC_SERVER_CONTACT_CARD_JSON =
|
|
75
|
-
'{"Rotate":{"payload":{"old":[73,163,230,244,111,233,153,119,133,211,134,237,111,36,52,131,22,50,54,144,150,45,227,235,128,36,33,217,190,198,55,75],"new":[109,115,204,144,178,114,182,238,113,124,4,139,249,76,220,44,128,104,194,68,187,184,82,241,94,145,104,198,159,122,186,43]},"issuer":[215,244,30,111,15,78,235,218,7,241,63,222,141,131,33,22,234,116,180,208,97,235,210,55,202,209,170,178,98,37,223,159],"signature":[178,64,85,76,51,199,196,151,129,14,191,53,127,191,34,223,97,238,95,109,118,179,152,17,205,188,204,177,116,166,147,231,192,201,48,137,19,214,180,45,108,104,34,8,14,63,115,139,215,142,4,179,233,89,150,218,174,168,107,23,8,109,228,6]}}';
|
|
76
|
-
|
|
77
71
|
const SUBDUCTION_ENDPOINTS = [
|
|
78
72
|
useKeyhiveSyncServer
|
|
79
73
|
? "wss://keyhive.sync.automerge.org"
|
|
@@ -183,48 +177,26 @@ function getRepoHive() {
|
|
|
183
177
|
}
|
|
184
178
|
|
|
185
179
|
initKeyhiveWasm();
|
|
186
|
-
const keyhiveStorage = new IndexedDBStorageAdapter(`${siteName}-keyhive`);
|
|
187
|
-
|
|
188
|
-
// Keyhive bootstrap needs to run before Repo creation but
|
|
189
|
-
// the adapter needs the subduction instance from the Repo.
|
|
190
|
-
// A deferred promise breaks the cycle.
|
|
191
|
-
let resolveRepoSubduction!: (s: any) => void;
|
|
192
|
-
const repoSubductionPromise = new Promise((resolve) => {
|
|
193
|
-
resolveRepoSubduction = resolve;
|
|
194
|
-
});
|
|
195
180
|
|
|
196
|
-
//
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
storage:
|
|
181
|
+
// ARK variant for talking to the keyhive-enabled subduction sync server.
|
|
182
|
+
const { hive, repo } = await initializeAutomergeRepoKeyhiveRustWithRepo({
|
|
183
|
+
createRepo: (config) => new Repo(config),
|
|
184
|
+
storage: new IndexedDBStorageAdapter(`${siteName}-keyhive`),
|
|
200
185
|
peerIdSuffix:
|
|
201
186
|
`${siteName}-worker` + Math.random().toString(36).slice(2),
|
|
202
|
-
subduction: repoSubductionPromise as any,
|
|
203
187
|
automaticArchiveIngestion: true,
|
|
204
188
|
cachingMode: "periodic",
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
|
|
210
|
-
:
|
|
211
|
-
|
|
212
|
-
|
|
213
|
-
|
|
214
|
-
|
|
215
|
-
const repo = new Repo({
|
|
216
|
-
storage: new IndexedDBStorageAdapter(),
|
|
217
|
-
signer,
|
|
218
|
-
subductionWebsocketEndpoints: SUBDUCTION_ENDPOINTS,
|
|
219
|
-
peerId: hive.peerId,
|
|
220
|
-
enableRemoteHeadsGossiping: true,
|
|
221
|
-
idFactory: hive.idFactory,
|
|
189
|
+
// ARK selects the relay via `syncServer` ("keyhive" | "subduction"),
|
|
190
|
+
// which pairs the contact card with the matching peer id. Omitting it
|
|
191
|
+
// defaults to "subduction".
|
|
192
|
+
...(useKeyhiveSyncServer ? { syncServer: "keyhive" as const } : {}),
|
|
193
|
+
repo: {
|
|
194
|
+
storage: new IndexedDBStorageAdapter(),
|
|
195
|
+
subductionWebsocketEndpoints: SUBDUCTION_ENDPOINTS,
|
|
196
|
+
enableRemoteHeadsGossiping: true,
|
|
197
|
+
},
|
|
222
198
|
});
|
|
223
199
|
|
|
224
|
-
repo.subduction.then(resolveRepoSubduction);
|
|
225
|
-
|
|
226
|
-
hive.linkRepo(repo);
|
|
227
|
-
|
|
228
200
|
(self as any).repo = repo;
|
|
229
201
|
(self as any).hive = hive;
|
|
230
202
|
log("repo constructed, waiting for network subsystem");
|
|
@@ -336,6 +308,7 @@ async function connectPort(port: MessagePort, connection: Connection) {
|
|
|
336
308
|
});
|
|
337
309
|
|
|
338
310
|
(keyhiveNetworkAdapter as any).on("ingest-remote", () => {
|
|
311
|
+
hive.notifySameAgentKeyhiveChange();
|
|
339
312
|
(hive.networkAdapter as any).syncKeyhive?.();
|
|
340
313
|
repo.shareConfigChanged();
|
|
341
314
|
});
|
package/src/externals.ts
CHANGED
package/src/service-worker.ts
CHANGED
|
@@ -220,7 +220,9 @@ self.addEventListener("fetch", (fetchEvent: FetchEvent) => {
|
|
|
220
220
|
cacheableStatuses.includes(response.status) &&
|
|
221
221
|
response.url.match(/^https?\:/)
|
|
222
222
|
) {
|
|
223
|
-
await cache.put(request, response.clone())
|
|
223
|
+
await cache.put(request, response.clone()).catch((error) => {
|
|
224
|
+
log(`error caching ${request.url} in ${cachename}`, error);
|
|
225
|
+
});
|
|
224
226
|
} else {
|
|
225
227
|
log(
|
|
226
228
|
`skipping uncacheable response code from cache: ${response.status} for ${response.url}`
|
package/src/site.ts
CHANGED
|
@@ -30,7 +30,7 @@ import * as Automerge from "@automerge/automerge/slim";
|
|
|
30
30
|
import * as AutomergeRepo from "@automerge/automerge-repo/slim";
|
|
31
31
|
import {
|
|
32
32
|
initKeyhiveWasm,
|
|
33
|
-
|
|
33
|
+
initializeAutomergeRepoKeyhiveWithRepo,
|
|
34
34
|
type AutomergeRepoKeyhive,
|
|
35
35
|
} from "@automerge/automerge-repo-keyhive";
|
|
36
36
|
// eslint-disable-next-line
|
|
@@ -41,6 +41,14 @@ declare const __SITE_NAME__: string;
|
|
|
41
41
|
const siteName =
|
|
42
42
|
typeof __SITE_NAME__ !== "undefined" ? __SITE_NAME__ : "tiny-patchwork";
|
|
43
43
|
|
|
44
|
+
// Sync-server selection for keyhive. Defaults to "subduction". Build with
|
|
45
|
+
// KEYHIVE_SYNC_SERVER=true to target keyhive.sync.automerge.org. This must match
|
|
46
|
+
// the automerge-worker (SharedWorker) selection so the tab and the SW grant relay
|
|
47
|
+
// access to the same server.
|
|
48
|
+
declare const __KEYHIVE_SYNC_SERVER__: boolean;
|
|
49
|
+
const useKeyhiveSyncServer =
|
|
50
|
+
typeof __KEYHIVE_SYNC_SERVER__ !== "undefined" && __KEYHIVE_SYNC_SERVER__;
|
|
51
|
+
|
|
44
52
|
import { ModuleWatcher } from "@inkandswitch/patchwork-filesystem";
|
|
45
53
|
import {
|
|
46
54
|
openDocument,
|
|
@@ -89,16 +97,33 @@ declare global {
|
|
|
89
97
|
|
|
90
98
|
export interface SiteConfig {
|
|
91
99
|
/**
|
|
92
|
-
*
|
|
93
|
-
*
|
|
94
|
-
*
|
|
95
|
-
*
|
|
100
|
+
* The site's default tool bundle — the tools every user of this site gets
|
|
101
|
+
* out of the box. Must collectively contribute at least a
|
|
102
|
+
* `patchwork:datatype` registration for `"account"` (typically the one
|
|
103
|
+
* supplied by `@inkandswitch/patchwork-frame`).
|
|
104
|
+
*
|
|
105
|
+
* Each entry is a *module-list source* and may be either:
|
|
106
|
+
* - an Automerge module-settings doc URL (`automerge:...`), which is
|
|
107
|
+
* live-reloaded, or
|
|
108
|
+
* - an HTTP(S) URL (absolute or site-relative, e.g. `/modules.json`) to a
|
|
109
|
+
* static JSON manifest of the shape `{ modules: string[], branches? }`,
|
|
110
|
+
* fetched once at boot.
|
|
111
|
+
*
|
|
112
|
+
* The module URLs *inside* either kind of source may themselves be Automerge
|
|
113
|
+
* folder docs or plain HTTP(S) bundles, so deployment targets can be freely
|
|
114
|
+
* mixed.
|
|
96
115
|
*
|
|
97
116
|
* Can be overridden at runtime by setting `localStorage.defaultToolsUrl` to
|
|
98
|
-
* another automerge
|
|
99
|
-
* unpublished tool set.
|
|
117
|
+
* another `automerge:` URL or manifest URL — useful for local development
|
|
118
|
+
* against an unpublished tool set.
|
|
119
|
+
*/
|
|
120
|
+
defaultModules?: string | string[];
|
|
121
|
+
|
|
122
|
+
/**
|
|
123
|
+
* @deprecated Use {@link SiteConfig.defaultModules}. Retained for backwards
|
|
124
|
+
* compatibility with existing sites.
|
|
100
125
|
*/
|
|
101
|
-
defaultModulesUrl
|
|
126
|
+
defaultModulesUrl?: AutomergeUrl;
|
|
102
127
|
|
|
103
128
|
/**
|
|
104
129
|
* `localStorage` key under which this site remembers which account document
|
|
@@ -163,7 +188,7 @@ const [automergeWasm, subductionWasm] = await Promise.all([
|
|
|
163
188
|
export async function bootPatchworkSite(
|
|
164
189
|
config: SiteConfig
|
|
165
190
|
): Promise<BootResult> {
|
|
166
|
-
const
|
|
191
|
+
const defaultModuleSources = resolveDefaultModules(config);
|
|
167
192
|
showLoadingAnimation();
|
|
168
193
|
log(`booting`, config);
|
|
169
194
|
await initializeWasm(automergeWasm);
|
|
@@ -182,37 +207,38 @@ export async function bootPatchworkSite(
|
|
|
182
207
|
await sw.subscribeToRepoChannel(resolvePort);
|
|
183
208
|
const workerPort = await portPromise;
|
|
184
209
|
|
|
210
|
+
let repo: Repo;
|
|
185
211
|
if (config.keyhive) {
|
|
186
212
|
initKeyhiveWasm();
|
|
187
213
|
|
|
188
|
-
hive = await
|
|
214
|
+
({ hive, repo } = await initializeAutomergeRepoKeyhiveWithRepo({
|
|
215
|
+
createRepo: (config) => new Repo(config),
|
|
189
216
|
storage: new IndexedDBStorageAdapter(`${siteName}-keyhive`),
|
|
190
217
|
peerIdSuffix: siteName + Math.random().toString(36).slice(2),
|
|
191
218
|
networkAdapter: new MessageChannelNetworkAdapter(workerPort),
|
|
192
219
|
automaticArchiveIngestion: true,
|
|
193
220
|
cachingMode: "periodic",
|
|
194
221
|
onlyShareWithHardcodedServerPeerId: false,
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
? new Repo({
|
|
200
|
-
storage: new IndexedDBStorageAdapter(),
|
|
201
|
-
enableRemoteHeadsGossiping: true,
|
|
202
|
-
network: [hive.networkAdapter],
|
|
203
|
-
peerId: hive.peerId,
|
|
204
|
-
idFactory: hive.idFactory,
|
|
205
|
-
})
|
|
206
|
-
: new Repo({
|
|
207
|
-
network: [new MessageChannelNetworkAdapter(workerPort)],
|
|
222
|
+
// ARK selects the relay via `syncServer` ("keyhive" | "subduction").
|
|
223
|
+
// Defaults to "subduction".
|
|
224
|
+
...(useKeyhiveSyncServer ? { syncServer: "keyhive" as const } : {}),
|
|
225
|
+
repo: {
|
|
208
226
|
storage: new IndexedDBStorageAdapter(),
|
|
209
|
-
async sharePolicy(peerId) {
|
|
210
|
-
return peerId.includes("automerge-worker");
|
|
211
|
-
},
|
|
212
227
|
enableRemoteHeadsGossiping: true,
|
|
213
|
-
|
|
214
|
-
|
|
215
|
-
|
|
228
|
+
},
|
|
229
|
+
}));
|
|
230
|
+
} else {
|
|
231
|
+
repo = new Repo({
|
|
232
|
+
network: [new MessageChannelNetworkAdapter(workerPort)],
|
|
233
|
+
storage: new IndexedDBStorageAdapter(),
|
|
234
|
+
async sharePolicy(peerId) {
|
|
235
|
+
return peerId.includes("automerge-worker");
|
|
236
|
+
},
|
|
237
|
+
enableRemoteHeadsGossiping: true,
|
|
238
|
+
peerId:
|
|
239
|
+
`${config.titleSuffix}-tab-${crypto.randomUUID()}` as AutomergeRepo.PeerId,
|
|
240
|
+
});
|
|
241
|
+
}
|
|
216
242
|
repo.subscribeToRemotes(
|
|
217
243
|
config.remoteStorageIds ?? [DEFAULT_REMOTE_STORAGE_ID]
|
|
218
244
|
);
|
|
@@ -246,7 +272,7 @@ export async function bootPatchworkSite(
|
|
|
246
272
|
// is added lazily once it appears on the account doc — see below.
|
|
247
273
|
const moduleWatcher = new ModuleWatcher(
|
|
248
274
|
repo,
|
|
249
|
-
|
|
275
|
+
buildSystemSources(defaultModuleSources),
|
|
250
276
|
onModuleLoaded,
|
|
251
277
|
unregisterPlugins
|
|
252
278
|
);
|
|
@@ -290,21 +316,70 @@ export async function bootPatchworkSite(
|
|
|
290
316
|
|
|
291
317
|
// ─── Internals ──────────────────────────────────────────────────────────
|
|
292
318
|
|
|
293
|
-
|
|
319
|
+
/**
|
|
320
|
+
* A module-list source is valid if it is an Automerge URL or looks like an
|
|
321
|
+
* HTTP(S)/site-relative manifest URL.
|
|
322
|
+
*/
|
|
323
|
+
function isValidModuleSource(source: string): boolean {
|
|
324
|
+
if (isValidAutomergeUrl(source)) return true;
|
|
325
|
+
return (
|
|
326
|
+
source.startsWith("/") ||
|
|
327
|
+
source.startsWith("http://") ||
|
|
328
|
+
source.startsWith("https://") ||
|
|
329
|
+
source.startsWith("./")
|
|
330
|
+
);
|
|
331
|
+
}
|
|
332
|
+
|
|
333
|
+
/**
|
|
334
|
+
* Resolve the site's default module-list sources, honouring the
|
|
335
|
+
* `localStorage.defaultToolsUrl` dev override (which replaces the entire
|
|
336
|
+
* built-in default bundle).
|
|
337
|
+
*/
|
|
338
|
+
function resolveDefaultModules(config: SiteConfig): string[] {
|
|
339
|
+
const builtin =
|
|
340
|
+
config.defaultModules ??
|
|
341
|
+
config.defaultModulesUrl ??
|
|
342
|
+
[];
|
|
343
|
+
const builtinList = (Array.isArray(builtin) ? builtin : [builtin]).filter(
|
|
344
|
+
Boolean
|
|
345
|
+
);
|
|
346
|
+
|
|
294
347
|
const override = globalThis.localStorage?.getItem("defaultToolsUrl");
|
|
295
|
-
if (
|
|
296
|
-
|
|
297
|
-
|
|
298
|
-
|
|
299
|
-
|
|
300
|
-
|
|
348
|
+
if (override) {
|
|
349
|
+
if (isValidModuleSource(override)) {
|
|
350
|
+
if (!builtinList.includes(override)) {
|
|
351
|
+
console.info(
|
|
352
|
+
`using defaultToolsUrl override from localStorage: ${override}`
|
|
353
|
+
);
|
|
354
|
+
}
|
|
355
|
+
return [override];
|
|
301
356
|
}
|
|
302
|
-
|
|
357
|
+
console.warn(
|
|
358
|
+
`ignoring invalid defaultToolsUrl in localStorage: ${override}; using built-in default`
|
|
359
|
+
);
|
|
303
360
|
}
|
|
304
|
-
|
|
305
|
-
|
|
306
|
-
|
|
307
|
-
|
|
361
|
+
|
|
362
|
+
if (builtinList.length === 0) {
|
|
363
|
+
throw new Error(
|
|
364
|
+
"bootPatchworkSite: no default module sources configured (set `defaultModules`)"
|
|
365
|
+
);
|
|
366
|
+
}
|
|
367
|
+
return builtinList;
|
|
368
|
+
}
|
|
369
|
+
|
|
370
|
+
/**
|
|
371
|
+
* Turn an ordered list of module-list sources into the name-keyed map the
|
|
372
|
+
* ModuleWatcher expects. The first source keeps the canonical `system` name;
|
|
373
|
+
* additional sources get suffixed names. None may be `user` (reserved for the
|
|
374
|
+
* per-account settings doc, which has branch-override precedence).
|
|
375
|
+
*/
|
|
376
|
+
function buildSystemSources(sources: string[]): Record<string, string> {
|
|
377
|
+
const map: Record<string, string> = {};
|
|
378
|
+
sources.forEach((source, index) => {
|
|
379
|
+
const name = index === 0 ? "system" : `system-${index}`;
|
|
380
|
+
map[name] = source;
|
|
381
|
+
});
|
|
382
|
+
return map;
|
|
308
383
|
}
|
|
309
384
|
|
|
310
385
|
function installDevConsoleGlobals(
|
|
@@ -372,7 +447,7 @@ function primeRootElement(
|
|
|
372
447
|
const initialParams = new URLSearchParams(location.hash.slice(1));
|
|
373
448
|
if (initialParams.has("frame")) {
|
|
374
449
|
rootElement.setAttribute("tool-id", initialParams.get("frame")!);
|
|
375
|
-
const docId = initialParams.get("doc");
|
|
450
|
+
const docId = initialParams.get("doc")?.replace(/^automerge:/, "");
|
|
376
451
|
const docUrl = docId
|
|
377
452
|
? stringifyAutomergeUrl({ documentId: docId as DocumentId })
|
|
378
453
|
: accountDocHandle.url;
|
|
@@ -558,15 +633,26 @@ function installHashRouting(params: HashRoutingParams): void {
|
|
|
558
633
|
return;
|
|
559
634
|
}
|
|
560
635
|
|
|
636
|
+
// Bare automerge URL in hash: /#automerge:<documentId>
|
|
637
|
+
if (isValidAutomergeUrl(hash as AutomergeUrl)) {
|
|
638
|
+
const { documentId, heads } = parseAutomergeUrl(hash as AutomergeUrl);
|
|
639
|
+
window.location.hash = "";
|
|
640
|
+
openDocument(
|
|
641
|
+
rootElement,
|
|
642
|
+
stringifyAutomergeUrl({ documentId, heads })
|
|
643
|
+
);
|
|
644
|
+
return;
|
|
645
|
+
}
|
|
646
|
+
|
|
561
647
|
const params = new URLSearchParams(hash);
|
|
562
|
-
const documentId = params.get("doc");
|
|
648
|
+
const documentId = params.get("doc")?.replace(/^automerge:/, "");
|
|
563
649
|
const heads = params.get("heads")?.split("|") as UrlHeads | undefined;
|
|
564
650
|
const toolId = params.get("tool");
|
|
565
651
|
const title = params.get("title");
|
|
566
652
|
const type = params.get("type");
|
|
567
653
|
const frame = params.get("frame");
|
|
568
654
|
if (frame) {
|
|
569
|
-
const docUrl = params.get("doc") ?? accountDocHandle.url;
|
|
655
|
+
const docUrl = params.get("doc")?.replace(/^automerge:/, "") ?? accountDocHandle.url;
|
|
570
656
|
if (
|
|
571
657
|
rootElement.getAttribute("tool-id") !== frame ||
|
|
572
658
|
rootElement.getAttribute("doc-url") !== docUrl
|