@inkandswitch/patchwork 0.7.4 → 0.8.1
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 +49 -0
- package/dist/head.js +1 -5
- package/dist/index.d.ts +5 -4
- package/dist/index.js +37 -56
- package/dist/repo.d.ts +9 -17
- package/dist/repo.js +27 -56
- package/dist/site-kit/index.d.ts +1 -1
- package/dist/site-kit/options.d.ts +24 -15
- package/dist/site-kit/sync-servers.d.ts +1 -0
- package/dist/site-kit/sync-servers.js +19 -17
- package/dist/types.d.ts +14 -3
- package/dist/vite/config-plugin.js +5 -5
- package/dist/vite/patches-plugin.d.ts +2 -0
- package/dist/vite/patches-plugin.js +149 -0
- package/dist/vite/patchwork-plugin.d.ts +2 -1
- package/dist/vite/patchwork-plugin.js +3 -0
- package/dist/vite/service-worker-plugin.js +2 -2
- package/package.json +5 -5
- package/src/index.ts +56 -48
- package/src/repo.ts +36 -72
- package/src/site-kit/index.ts +1 -0
- package/src/site-kit/options.ts +26 -11
- package/src/site-kit/sync-servers.ts +20 -17
- package/src/types.ts +15 -8
- package/src/vite/config-plugin.ts +7 -12
- package/src/vite/patches-plugin.ts +184 -0
- package/src/vite/patchwork-plugin.ts +4 -0
- package/src/vite/service-worker-plugin.ts +3 -2
|
@@ -0,0 +1,149 @@
|
|
|
1
|
+
import { readFile } from "node:fs/promises";
|
|
2
|
+
import { join } from "node:path";
|
|
3
|
+
/**
|
|
4
|
+
* Source patches to @automerge/automerge-repo, applied as it passes through
|
|
5
|
+
* the bundler.
|
|
6
|
+
*
|
|
7
|
+
* The same edits live in this repo's `patches/` as a pnpm patch, which is what
|
|
8
|
+
* makes our own typecheck see the widened `role` type. A pnpm patch only
|
|
9
|
+
* exists in this repo's node_modules, though: a site that installs
|
|
10
|
+
* @inkandswitch/patchwork resolves automerge-repo out of its own tree and
|
|
11
|
+
* would bundle it unpatched. These run there too.
|
|
12
|
+
*
|
|
13
|
+
* Both edits are upstream-shaped and meant to be deleted once subduction takes
|
|
14
|
+
* them. Until then they are pinned to one automerge-repo version and every
|
|
15
|
+
* anchor has to match, so a dependency bump fails the build instead of quietly
|
|
16
|
+
* un-patching it.
|
|
17
|
+
*/
|
|
18
|
+
const AUTOMERGE_REPO = "@automerge/automerge-repo";
|
|
19
|
+
const VERSION = "2.6.0-subduction.48";
|
|
20
|
+
const PATCHES = {
|
|
21
|
+
"dist/subduction/AdapterConnections.js": [
|
|
22
|
+
{
|
|
23
|
+
find: ` if (role === "accept") {
|
|
24
|
+
await subduction.acceptTransport(transport, serviceName);
|
|
25
|
+
}
|
|
26
|
+
else {
|
|
27
|
+
await subduction.connectTransport(transport, serviceName);
|
|
28
|
+
}`,
|
|
29
|
+
replace: ` const initiate = role === "mesh" ? this.#localPeerId < peerId : role !== "accept";
|
|
30
|
+
if (initiate) {
|
|
31
|
+
await subduction.connectTransport(transport, serviceName);
|
|
32
|
+
}
|
|
33
|
+
else {
|
|
34
|
+
await subduction.acceptTransport(transport, serviceName);
|
|
35
|
+
}`,
|
|
36
|
+
},
|
|
37
|
+
],
|
|
38
|
+
"dist/subduction/SubductionConnections.js": [
|
|
39
|
+
{
|
|
40
|
+
find: ` if (state === "connecting")
|
|
41
|
+
return true;`,
|
|
42
|
+
replace: ` // "awaiting-reconnect" counts: the loop is between attempts, not
|
|
43
|
+
// given up, so a query should wait rather than report unavailable.
|
|
44
|
+
if (state === "connecting" || state === "awaiting-reconnect")
|
|
45
|
+
return true;`,
|
|
46
|
+
},
|
|
47
|
+
],
|
|
48
|
+
};
|
|
49
|
+
function match(id) {
|
|
50
|
+
const path = id.replace(/\\/g, "/").split("?")[0];
|
|
51
|
+
for (const file of Object.keys(PATCHES)) {
|
|
52
|
+
const suffix = `/${AUTOMERGE_REPO}/${file}`;
|
|
53
|
+
if (path.endsWith(suffix)) {
|
|
54
|
+
return {
|
|
55
|
+
file,
|
|
56
|
+
root: path.slice(0, -suffix.length) + `/${AUTOMERGE_REPO}`,
|
|
57
|
+
};
|
|
58
|
+
}
|
|
59
|
+
}
|
|
60
|
+
}
|
|
61
|
+
const versions = new Map();
|
|
62
|
+
async function assertVersion(root, file) {
|
|
63
|
+
let version = versions.get(root);
|
|
64
|
+
if (!version) {
|
|
65
|
+
version = readFile(join(root, "package.json"), "utf8").then((json) => JSON.parse(json).version, (error) => {
|
|
66
|
+
throw new Error(`@inkandswitch/patchwork: couldn't read ${root}/package.json to ` +
|
|
67
|
+
`check the version the source patches are written against. ` +
|
|
68
|
+
`Has the package's layout changed? (${error})`);
|
|
69
|
+
});
|
|
70
|
+
versions.set(root, version);
|
|
71
|
+
}
|
|
72
|
+
if ((await version) !== VERSION) {
|
|
73
|
+
throw new Error(`@inkandswitch/patchwork: ${AUTOMERGE_REPO} is ${await version}, and the ` +
|
|
74
|
+
`source patches in patches-plugin.ts are written against ${VERSION}. ` +
|
|
75
|
+
`Re-check them against the new version (${file} is one of the files ` +
|
|
76
|
+
`they edit), then bump VERSION.`);
|
|
77
|
+
}
|
|
78
|
+
}
|
|
79
|
+
function apply(code, file) {
|
|
80
|
+
return PATCHES[file].reduce((code, { find, replace }) => {
|
|
81
|
+
if (code.includes(replace))
|
|
82
|
+
return code;
|
|
83
|
+
const matches = code.split(find).length - 1;
|
|
84
|
+
if (matches !== 1) {
|
|
85
|
+
throw new Error(`@inkandswitch/patchwork: the source patch for ${AUTOMERGE_REPO}'s ` +
|
|
86
|
+
`${file} matched ${matches} times, expected 1. The file is the ` +
|
|
87
|
+
`version it says it is, so the patch needs rewriting against it.`);
|
|
88
|
+
}
|
|
89
|
+
return code.replace(find, replace);
|
|
90
|
+
}, code);
|
|
91
|
+
}
|
|
92
|
+
async function patchFile(id) {
|
|
93
|
+
const found = match(id);
|
|
94
|
+
if (!found)
|
|
95
|
+
return;
|
|
96
|
+
await assertVersion(found.root, found.file);
|
|
97
|
+
return apply(await readFile(id, "utf8"), found.file);
|
|
98
|
+
}
|
|
99
|
+
export function patches() {
|
|
100
|
+
const seen = new Set();
|
|
101
|
+
let serve = false;
|
|
102
|
+
return {
|
|
103
|
+
name: "@patchwork/patches",
|
|
104
|
+
enforce: "pre",
|
|
105
|
+
// Dep pre-bundling runs esbuild directly, outside the plugin pipeline that
|
|
106
|
+
// applies `transform`. Without this the dev server would serve an
|
|
107
|
+
// unpatched automerge-repo while the build patched it.
|
|
108
|
+
config() {
|
|
109
|
+
return {
|
|
110
|
+
optimizeDeps: {
|
|
111
|
+
esbuildOptions: {
|
|
112
|
+
plugins: [
|
|
113
|
+
{
|
|
114
|
+
name: "patchwork-automerge-repo-patches",
|
|
115
|
+
setup(build) {
|
|
116
|
+
build.onLoad({ filter: /automerge-repo[\\/]dist[\\/]subduction[\\/]/ }, async ({ path }) => {
|
|
117
|
+
const contents = await patchFile(path);
|
|
118
|
+
return contents ? { contents, loader: "js" } : undefined;
|
|
119
|
+
});
|
|
120
|
+
},
|
|
121
|
+
},
|
|
122
|
+
],
|
|
123
|
+
},
|
|
124
|
+
},
|
|
125
|
+
};
|
|
126
|
+
},
|
|
127
|
+
configResolved(config) {
|
|
128
|
+
serve = config.command === "serve";
|
|
129
|
+
},
|
|
130
|
+
async transform(code, id) {
|
|
131
|
+
const found = match(id);
|
|
132
|
+
if (!found)
|
|
133
|
+
return;
|
|
134
|
+
await assertVersion(found.root, found.file);
|
|
135
|
+
seen.add(found.file);
|
|
136
|
+
return apply(code, found.file);
|
|
137
|
+
},
|
|
138
|
+
buildEnd() {
|
|
139
|
+
if (serve)
|
|
140
|
+
return;
|
|
141
|
+
const missing = Object.keys(PATCHES).filter((file) => !seen.has(file));
|
|
142
|
+
if (missing.length) {
|
|
143
|
+
throw new Error(`@inkandswitch/patchwork: ${AUTOMERGE_REPO}'s ${missing.join(", ")} ` +
|
|
144
|
+
`never reached the bundler, so the source patches for them didn't ` +
|
|
145
|
+
`apply. Has the package's layout changed?`);
|
|
146
|
+
}
|
|
147
|
+
},
|
|
148
|
+
};
|
|
149
|
+
}
|
|
@@ -24,6 +24,7 @@ import type { PatchworkSiteOptions } from "../site-kit/options.js";
|
|
|
24
24
|
*/
|
|
25
25
|
export default function patchwork(options?: PatchworkVitePluginOptions): Plugin<any>[];
|
|
26
26
|
export { importmap, builtins, devDependencyId } from "./importmap-plugin.js";
|
|
27
|
+
export { patches } from "./patches-plugin.js";
|
|
27
28
|
export { serviceworker, workers } from "./service-worker-plugin.js";
|
|
28
29
|
export { config, buildDefines, wasm } from "./config-plugin.js";
|
|
29
30
|
export { dev } from "./dev-plugin.js";
|
|
@@ -43,7 +44,7 @@ export type ImportMap = {
|
|
|
43
44
|
};
|
|
44
45
|
};
|
|
45
46
|
export type { PatchworkStaticSource } from "./static-plugin.js";
|
|
46
|
-
export type { PatchworkSiteOptions, PatchworkIconsOptions, PatchworkHtmlOptions, PatchworkNetlifyOptions, PatchworkKeyhiveSyncServer, PatchworkSyncServersOptions, } from "../site-kit/options.js";
|
|
47
|
+
export type { PatchworkSiteOptions, PatchworkIconsOptions, PatchworkHtmlOptions, PatchworkNetlifyOptions, PatchworkKeyhiveSyncServer, PatchworkKeyhiveOptions, PatchworkSyncServersOptions, } from "../site-kit/options.js";
|
|
47
48
|
export interface PatchworkVitePluginOptions extends PatchworkSiteOptions {
|
|
48
49
|
importmap?: ImportMap;
|
|
49
50
|
/**
|
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import { importmap } from "./importmap-plugin.js";
|
|
2
|
+
import { patches } from "./patches-plugin.js";
|
|
2
3
|
import { serviceworker } from "./service-worker-plugin.js";
|
|
3
4
|
import { config, wasm } from "./config-plugin.js";
|
|
4
5
|
import { dev } from "./dev-plugin.js";
|
|
@@ -38,6 +39,7 @@ export default function patchwork(options) {
|
|
|
38
39
|
manifest(options),
|
|
39
40
|
netlify(options),
|
|
40
41
|
importmap(options),
|
|
42
|
+
patches(),
|
|
41
43
|
serviceworker(),
|
|
42
44
|
dev(options),
|
|
43
45
|
statics(options),
|
|
@@ -45,6 +47,7 @@ export default function patchwork(options) {
|
|
|
45
47
|
].filter((plugin) => plugin != null);
|
|
46
48
|
}
|
|
47
49
|
export { importmap, builtins, devDependencyId } from "./importmap-plugin.js";
|
|
50
|
+
export { patches } from "./patches-plugin.js";
|
|
48
51
|
export { serviceworker, workers } from "./service-worker-plugin.js";
|
|
49
52
|
export { config, buildDefines, wasm } from "./config-plugin.js";
|
|
50
53
|
export { dev } from "./dev-plugin.js";
|
|
@@ -16,8 +16,8 @@ export const workers = [
|
|
|
16
16
|
fileName: "service-worker.js",
|
|
17
17
|
},
|
|
18
18
|
{
|
|
19
|
-
specifier: "@inkandswitch/patchwork-bootloader/automerge-worker",
|
|
20
|
-
fileName: "automerge-worker.js",
|
|
19
|
+
specifier: "@inkandswitch/patchwork-bootloader/automerge-protocol-handler-worker",
|
|
20
|
+
fileName: "automerge-protocol-handler-worker.js",
|
|
21
21
|
},
|
|
22
22
|
{
|
|
23
23
|
specifier: "@inkandswitch/patchwork-bootloader/module-loader-worker",
|
package/package.json
CHANGED
|
@@ -5,7 +5,7 @@
|
|
|
5
5
|
"url": "git+https://github.com/inkandswitch/patchwork-system.git",
|
|
6
6
|
"directory": "core/patchwork"
|
|
7
7
|
},
|
|
8
|
-
"version": "0.
|
|
8
|
+
"version": "0.8.1",
|
|
9
9
|
"author": "Ink & Switch",
|
|
10
10
|
"type": "module",
|
|
11
11
|
"license": "MIT",
|
|
@@ -72,7 +72,7 @@
|
|
|
72
72
|
"./global.css": "./dist/global.css"
|
|
73
73
|
},
|
|
74
74
|
"dependencies": {
|
|
75
|
-
"@automerge/automerge": "3.
|
|
75
|
+
"@automerge/automerge": "3.4.1",
|
|
76
76
|
"@automerge/automerge-repo": "2.6.0-subduction.48",
|
|
77
77
|
"@automerge/automerge-repo-keyhive": "0.5.0-alpha.7",
|
|
78
78
|
"@automerge/automerge-repo-storage-indexeddb": "2.6.0-subduction.48",
|
|
@@ -83,10 +83,10 @@
|
|
|
83
83
|
"esbuild": "^0.23.1",
|
|
84
84
|
"sharp": "^0.35.3",
|
|
85
85
|
"vite-plugin-wasm": "^3.6.0",
|
|
86
|
-
"@inkandswitch/patchwork-bootloader": "^0.
|
|
86
|
+
"@inkandswitch/patchwork-bootloader": "^0.7.1",
|
|
87
87
|
"@inkandswitch/patchwork-elements": "^6.0.2",
|
|
88
|
-
"@inkandswitch/patchwork-filesystem": "^0.2.
|
|
89
|
-
"@inkandswitch/patchwork-plugins": "^1.2.
|
|
88
|
+
"@inkandswitch/patchwork-filesystem": "^0.2.9",
|
|
89
|
+
"@inkandswitch/patchwork-plugins": "^1.2.4",
|
|
90
90
|
"@inkandswitch/patchwork-providers": "^0.5.2"
|
|
91
91
|
},
|
|
92
92
|
"devDependencies": {
|
package/src/index.ts
CHANGED
|
@@ -1,8 +1,9 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* One import for a Patchwork site.
|
|
3
3
|
*
|
|
4
|
-
* `setup(options)` constructs the Repo, wires up the
|
|
5
|
-
* loads plugins via the
|
|
4
|
+
* `setup(options)` constructs the Repo, wires up the
|
|
5
|
+
* automerge-protocol-handler-worker port, loads plugins via the
|
|
6
|
+
* ModuleWatcher, resolves the user's account document,
|
|
6
7
|
* installs the router, and resolves with the site's runtime API — `repo`,
|
|
7
8
|
* `create`, `open`, `find`, `packages`, `plugins`, `sw` — which is what a
|
|
8
9
|
* site assigns to `window.patchwork`.
|
|
@@ -15,12 +16,13 @@
|
|
|
15
16
|
* Pulls in DOM- and plugin-layer dependencies, so it is for a browser site's
|
|
16
17
|
* `main.ts` only. Non-UI consumers should import
|
|
17
18
|
* `@inkandswitch/patchwork-bootloader` directly, which does SW registration
|
|
18
|
-
* and the automerge-worker handoff and nothing else.
|
|
19
|
+
* and the automerge-protocol-handler-worker handoff and nothing else.
|
|
19
20
|
*/
|
|
20
21
|
import {
|
|
21
22
|
type AutomergeUrl,
|
|
22
23
|
type DocHandle,
|
|
23
|
-
|
|
24
|
+
type DocumentId,
|
|
25
|
+
type StorageId,
|
|
24
26
|
Repo,
|
|
25
27
|
} from "@automerge/vanillajs/slim";
|
|
26
28
|
import * as Automerge from "@automerge/automerge/slim";
|
|
@@ -52,13 +54,9 @@ import type {
|
|
|
52
54
|
Patchwork,
|
|
53
55
|
PatchworkOptions,
|
|
54
56
|
SignerIdentity,
|
|
57
|
+
SyncStateDocMessage,
|
|
55
58
|
} from "./types.js";
|
|
56
|
-
import {
|
|
57
|
-
createRepo,
|
|
58
|
-
firstRepoPort,
|
|
59
|
-
initWasm,
|
|
60
|
-
removeAdapterFor,
|
|
61
|
-
} from "./repo.js";
|
|
59
|
+
import { createRepo, initWasm } from "./repo.js";
|
|
62
60
|
import { createRouter, type Router } from "./router.js";
|
|
63
61
|
import { createDefaultAccount } from "./createAccount.js";
|
|
64
62
|
|
|
@@ -130,46 +128,13 @@ async function doSetup(options: PatchworkOptions): Promise<Patchwork> {
|
|
|
130
128
|
let hive: AutomergeRepoKeyhive | undefined;
|
|
131
129
|
let repo: Repo;
|
|
132
130
|
let signerIdentity: SignerIdentity | undefined;
|
|
133
|
-
// Called with a fresh port when the automerge worker dies and is recreated.
|
|
134
|
-
// Assigned once the repo exists.
|
|
135
|
-
let onWorkerPortRenewed: ((port: MessagePort) => void) | undefined;
|
|
136
131
|
|
|
137
132
|
if (options.repo) {
|
|
138
133
|
log("using provided Repo");
|
|
139
134
|
repo = options.repo;
|
|
140
135
|
hive = options.hive;
|
|
141
136
|
} else {
|
|
142
|
-
|
|
143
|
-
if (onWorkerPortRenewed) onWorkerPortRenewed(port);
|
|
144
|
-
else {
|
|
145
|
-
console.warn(
|
|
146
|
-
"automerge worker port renewed before the repo existed; dropping it"
|
|
147
|
-
);
|
|
148
|
-
}
|
|
149
|
-
});
|
|
150
|
-
|
|
151
|
-
let workerAdapter = new MessageChannelNetworkAdapter(workerPort);
|
|
152
|
-
({ repo, hive, signerIdentity } = await createRepo(workerAdapter));
|
|
153
|
-
|
|
154
|
-
// The worker was recreated with cold state: wire the repo onto the fresh
|
|
155
|
-
// port and drop the adapter stranded on the dead one.
|
|
156
|
-
const bootHive = hive;
|
|
157
|
-
onWorkerPortRenewed = (port) => {
|
|
158
|
-
const fresh = new MessageChannelNetworkAdapter(port);
|
|
159
|
-
// Mirror the boot wiring: a keyhive repo talks to the worker through a
|
|
160
|
-
// keyhive adapter wrapped around the message channel.
|
|
161
|
-
const registered = bootHive
|
|
162
|
-
? bootHive.createKeyhiveNetworkAdapter(fresh, {
|
|
163
|
-
onlyShareWithSyncServer: false,
|
|
164
|
-
periodicallyRequestSync: false,
|
|
165
|
-
syncRequestInterval: 2000,
|
|
166
|
-
})
|
|
167
|
-
: fresh;
|
|
168
|
-
repo.networkSubsystem.addNetworkAdapter(registered as any);
|
|
169
|
-
removeAdapterFor(repo, workerAdapter, registered);
|
|
170
|
-
workerAdapter = fresh;
|
|
171
|
-
lifecycleLog("repo re-wired to the recreated automerge worker");
|
|
172
|
-
};
|
|
137
|
+
({ repo, hive, signerIdentity } = await createRepo());
|
|
173
138
|
}
|
|
174
139
|
|
|
175
140
|
// Dev-console / tool-runtime globals (e2e and loaded tools read these). The
|
|
@@ -181,8 +146,6 @@ async function doSetup(options: PatchworkOptions): Promise<Patchwork> {
|
|
|
181
146
|
AutomergeRepo as typeof import("@automerge/automerge-repo");
|
|
182
147
|
if (hive) window.hive = hive;
|
|
183
148
|
|
|
184
|
-
await repo.networkSubsystem.whenReady();
|
|
185
|
-
log("networkSubsystem ready");
|
|
186
149
|
(hive?.networkAdapter as any)?.syncKeyhive?.();
|
|
187
150
|
|
|
188
151
|
registerRepoProviderElement(repo as any);
|
|
@@ -261,8 +224,8 @@ async function doSetup(options: PatchworkOptions): Promise<Patchwork> {
|
|
|
261
224
|
plugins,
|
|
262
225
|
sw: {
|
|
263
226
|
connectClassicSync: sw.connectClassicSync,
|
|
264
|
-
|
|
265
|
-
|
|
227
|
+
subscribeSyncState: (documentId, listener) =>
|
|
228
|
+
subscribeSyncState(repo, documentId, listener),
|
|
266
229
|
},
|
|
267
230
|
|
|
268
231
|
async create<D>(type: string, init?: (doc: D) => void) {
|
|
@@ -433,6 +396,50 @@ function installLifecycleLogging(): void {
|
|
|
433
396
|
);
|
|
434
397
|
}
|
|
435
398
|
|
|
399
|
+
// The tab's own Repo hears the server's heads directly, so this is a filter
|
|
400
|
+
// over its `subduction-remote-heads` event. The current value is replayed
|
|
401
|
+
// from the handle's sync info, when the server has reported any.
|
|
402
|
+
function subscribeSyncState(
|
|
403
|
+
repo: Repo,
|
|
404
|
+
documentId: string,
|
|
405
|
+
listener: (update: SyncStateDocMessage) => void
|
|
406
|
+
): () => void {
|
|
407
|
+
const onHeads = (payload: {
|
|
408
|
+
documentId: string;
|
|
409
|
+
storageId: string;
|
|
410
|
+
heads: readonly string[];
|
|
411
|
+
timestamp: number;
|
|
412
|
+
}) => {
|
|
413
|
+
if (payload.documentId !== documentId) return;
|
|
414
|
+
listener({
|
|
415
|
+
type: "sync-state",
|
|
416
|
+
documentId,
|
|
417
|
+
storageId: payload.storageId,
|
|
418
|
+
heads: [...payload.heads],
|
|
419
|
+
timestamp: payload.timestamp,
|
|
420
|
+
});
|
|
421
|
+
};
|
|
422
|
+
repo.on("subduction-remote-heads", onHeads);
|
|
423
|
+
|
|
424
|
+
void (async () => {
|
|
425
|
+
const handle = repo.handles[documentId as DocumentId];
|
|
426
|
+
if (!handle) return;
|
|
427
|
+
for (const storageId of await repo.connectedSubductionPeerIds()) {
|
|
428
|
+
const info = handle.getSyncInfo(storageId as StorageId);
|
|
429
|
+
if (info) {
|
|
430
|
+
onHeads({
|
|
431
|
+
documentId,
|
|
432
|
+
storageId,
|
|
433
|
+
heads: info.lastHeads,
|
|
434
|
+
timestamp: info.lastSyncTimestamp,
|
|
435
|
+
});
|
|
436
|
+
}
|
|
437
|
+
}
|
|
438
|
+
})();
|
|
439
|
+
|
|
440
|
+
return () => repo.off("subduction-remote-heads", onHeads);
|
|
441
|
+
}
|
|
442
|
+
|
|
436
443
|
// ── Named exports ────────────────────────────────────────────────────────
|
|
437
444
|
|
|
438
445
|
export { createRepo, initWasm } from "./repo.js";
|
|
@@ -448,4 +455,5 @@ export type {
|
|
|
448
455
|
PatchworkOptions,
|
|
449
456
|
ServiceWorkerApi,
|
|
450
457
|
SignerIdentity,
|
|
458
|
+
SyncStateDocMessage,
|
|
451
459
|
} from "./types.js";
|
package/src/repo.ts
CHANGED
|
@@ -1,22 +1,17 @@
|
|
|
1
|
-
import {
|
|
2
|
-
initializeWasm,
|
|
3
|
-
MessageChannelNetworkAdapter,
|
|
4
|
-
Repo,
|
|
5
|
-
type AutomergeUrl,
|
|
6
|
-
} from "@automerge/vanillajs/slim";
|
|
1
|
+
import { initializeWasm, Repo } from "@automerge/vanillajs/slim";
|
|
7
2
|
import { IndexedDBWorkerStorageAdapter } from "@automerge/automerge-repo-storage-indexeddb/IndexedDBWorkerStorageAdapter";
|
|
3
|
+
import { siblingAdapters } from "@inkandswitch/patchwork-bootloader/siblings";
|
|
4
|
+
import { loadOrCreateSigner } from "@inkandswitch/patchwork-bootloader/signer";
|
|
8
5
|
import * as AutomergeRepo from "@automerge/automerge-repo/slim";
|
|
9
6
|
import {
|
|
10
7
|
initKeyhiveWasm,
|
|
11
|
-
|
|
12
|
-
type
|
|
8
|
+
initializeAutomergeRepoKeyhive,
|
|
9
|
+
type AutomergeRepoKeyhive,
|
|
13
10
|
type SyncServerSelection,
|
|
14
11
|
} from "@automerge/automerge-repo-keyhive";
|
|
15
12
|
// eslint-disable-next-line
|
|
16
13
|
// @ts-ignore — initSync is a wasm-bindgen runtime helper not in the .d.ts
|
|
17
14
|
import { initSync as initSubductionSync } from "@automerge/automerge-subduction/slim";
|
|
18
|
-
import { MemorySigner } from "@automerge/automerge-subduction/slim";
|
|
19
|
-
import setupServiceWorker from "@inkandswitch/patchwork-bootloader";
|
|
20
15
|
import {
|
|
21
16
|
keyhiveStorageName,
|
|
22
17
|
storagePrefix,
|
|
@@ -29,6 +24,7 @@ const log = debug("patchwork:setup:repo");
|
|
|
29
24
|
declare const __SYNC_SERVER__: {
|
|
30
25
|
url: string;
|
|
31
26
|
keyhive?: SyncServerSelection;
|
|
27
|
+
useIdFactory?: boolean;
|
|
32
28
|
};
|
|
33
29
|
const syncServer =
|
|
34
30
|
typeof __SYNC_SERVER__ !== "undefined"
|
|
@@ -53,28 +49,40 @@ export function initWasm(): Promise<void> {
|
|
|
53
49
|
return wasmReady;
|
|
54
50
|
}
|
|
55
51
|
|
|
56
|
-
export
|
|
57
|
-
workerAdapter: MessageChannelNetworkAdapter
|
|
58
|
-
): Promise<{
|
|
52
|
+
export type TabRepo = {
|
|
59
53
|
repo: Repo;
|
|
60
|
-
hive?:
|
|
54
|
+
hive?: AutomergeRepoKeyhive;
|
|
61
55
|
signerIdentity?: SignerIdentity;
|
|
62
|
-
}
|
|
56
|
+
};
|
|
57
|
+
|
|
58
|
+
/**
|
|
59
|
+
* The tab's own node: this origin's IndexedDB, a socket to the sync server,
|
|
60
|
+
* and the siblings channel to every other Repo on the origin. Nothing is
|
|
61
|
+
* shared with other tabs except the database underneath.
|
|
62
|
+
*/
|
|
63
|
+
export async function createRepo(): Promise<TabRepo> {
|
|
63
64
|
if (syncServer.keyhive) {
|
|
64
65
|
log("setting up keyhive");
|
|
65
66
|
initKeyhiveWasm();
|
|
66
|
-
const { hive, repo } = await
|
|
67
|
-
|
|
67
|
+
const { hive, repo } = await initializeAutomergeRepoKeyhive({
|
|
68
|
+
// ARK injects an `idFactory` deriving document ids from keyhive. A site
|
|
69
|
+
// can opt out of it with `keyhive: { useIdFactory: false }`.
|
|
70
|
+
createRepo: ({ idFactory, ...repoConfig }) =>
|
|
71
|
+
new Repo(
|
|
72
|
+
syncServer.useIdFactory === false
|
|
73
|
+
? repoConfig
|
|
74
|
+
: { ...repoConfig, idFactory }
|
|
75
|
+
),
|
|
68
76
|
storage: new IndexedDBWorkerStorageAdapter(keyhiveStorageName),
|
|
69
77
|
peerIdSuffix: storagePrefix + Math.random().toString(36).slice(2),
|
|
70
|
-
networkAdapter: workerAdapter,
|
|
71
78
|
automaticArchiveIngestion: true,
|
|
72
79
|
cachingMode: "periodic",
|
|
73
|
-
onlyShareWithSyncServer: false,
|
|
74
80
|
// ARK selects the relay via `syncServer`, defaulting to "subduction".
|
|
75
81
|
syncServer: syncServer.keyhive,
|
|
76
82
|
repo: {
|
|
77
83
|
storage: new IndexedDBWorkerStorageAdapter(),
|
|
84
|
+
subductionWebsocketEndpoints: [syncServer.url],
|
|
85
|
+
subductionAdapters: siblingAdapters(),
|
|
78
86
|
enableRemoteHeadsGossiping: true,
|
|
79
87
|
},
|
|
80
88
|
});
|
|
@@ -82,20 +90,19 @@ export async function createRepo(
|
|
|
82
90
|
return { repo, hive };
|
|
83
91
|
}
|
|
84
92
|
|
|
85
|
-
//
|
|
86
|
-
//
|
|
87
|
-
//
|
|
88
|
-
const
|
|
93
|
+
// The signer is explicit rather than the Repo's internal default so that
|
|
94
|
+
// every tab on this origin signs as the same peer, and so the identity the
|
|
95
|
+
// tab presents to the server can be shown on window.patchwork.
|
|
96
|
+
const storage = new IndexedDBWorkerStorageAdapter();
|
|
97
|
+
const signer = await loadOrCreateSigner(storage);
|
|
89
98
|
const repo = new Repo({
|
|
90
|
-
network: [workerAdapter],
|
|
91
|
-
storage: new IndexedDBWorkerStorageAdapter(),
|
|
92
99
|
signer,
|
|
93
|
-
|
|
94
|
-
return peerId.includes("automerge-worker");
|
|
95
|
-
},
|
|
96
|
-
enableRemoteHeadsGossiping: true,
|
|
100
|
+
storage,
|
|
97
101
|
peerId:
|
|
98
102
|
`${storagePrefix}-tab-${crypto.randomUUID()}` as AutomergeRepo.PeerId,
|
|
103
|
+
subductionWebsocketEndpoints: [syncServer.url],
|
|
104
|
+
subductionAdapters: siblingAdapters(),
|
|
105
|
+
enableRemoteHeadsGossiping: true,
|
|
99
106
|
});
|
|
100
107
|
const signerIdentity = {
|
|
101
108
|
peerId: signer.peerId().toString(),
|
|
@@ -108,46 +115,3 @@ export async function createRepo(
|
|
|
108
115
|
log("repo created, tab subduction identity:", signerIdentity);
|
|
109
116
|
return { repo, signerIdentity };
|
|
110
117
|
}
|
|
111
|
-
|
|
112
|
-
/**
|
|
113
|
-
* Resolve with the first repo port the worker delivers, calling `onRenewed` for
|
|
114
|
-
* every later one.
|
|
115
|
-
*
|
|
116
|
-
* subscribeToRepoChannel is deliberately not awaited: it resolves only after
|
|
117
|
-
* the boot channel's port-ready handshake, which can take its full 30s timeout
|
|
118
|
-
* against a stranded worker connection. Boot blocks on the first *delivered*
|
|
119
|
-
* port instead — if the boot channel stalls, worker recovery hands the listener
|
|
120
|
-
* a good port long before that timeout.
|
|
121
|
-
*/
|
|
122
|
-
export function firstRepoPort(
|
|
123
|
-
sw: Awaited<ReturnType<typeof setupServiceWorker>>,
|
|
124
|
-
onRenewed: (port: MessagePort) => void
|
|
125
|
-
): Promise<MessagePort> {
|
|
126
|
-
return new Promise<MessagePort>((resolve) => {
|
|
127
|
-
let seen = false;
|
|
128
|
-
void sw.subscribeToRepoChannel((port) => {
|
|
129
|
-
if (seen) return onRenewed(port);
|
|
130
|
-
seen = true;
|
|
131
|
-
resolve(port);
|
|
132
|
-
});
|
|
133
|
-
});
|
|
134
|
-
}
|
|
135
|
-
|
|
136
|
-
/** Drop the adapter sitting on the dead worker port, leaving `keep` in place. */
|
|
137
|
-
export function removeAdapterFor(
|
|
138
|
-
repo: Repo,
|
|
139
|
-
stale: MessageChannelNetworkAdapter,
|
|
140
|
-
keep: unknown
|
|
141
|
-
): void {
|
|
142
|
-
for (const adapter of [...repo.networkSubsystem.adapters]) {
|
|
143
|
-
if (adapter === keep) continue;
|
|
144
|
-
// The keyhive wrapper keeps the wrapped adapter on `.networkAdapter`.
|
|
145
|
-
const base = (adapter as any).networkAdapter ?? adapter;
|
|
146
|
-
if (base !== stale) continue;
|
|
147
|
-
try {
|
|
148
|
-
repo.networkSubsystem.removeNetworkAdapter(adapter as any);
|
|
149
|
-
} catch (err) {
|
|
150
|
-
console.error("failed to remove stale worker network adapter", err);
|
|
151
|
-
}
|
|
152
|
-
}
|
|
153
|
-
}
|
package/src/site-kit/index.ts
CHANGED
package/src/site-kit/options.ts
CHANGED
|
@@ -36,19 +36,31 @@ export interface PatchworkNetlifyOptions {
|
|
|
36
36
|
immutableAssets?: boolean;
|
|
37
37
|
}
|
|
38
38
|
|
|
39
|
-
type PatchworkPrimarySyncServerOptions =
|
|
40
|
-
| { subduction?: string; keyhive?: never }
|
|
41
|
-
| { subduction?: never; keyhive: PatchworkKeyhiveSyncServer };
|
|
42
|
-
|
|
43
39
|
export type PatchworkKeyhiveSyncServer =
|
|
44
40
|
| "keyhive"
|
|
45
41
|
| "subduction"
|
|
46
42
|
| ({ url: string } & SyncServerIdentity);
|
|
47
43
|
|
|
44
|
+
export interface PatchworkKeyhiveOptions {
|
|
45
|
+
/**
|
|
46
|
+
* Which relay ARK registers and grants access to. Default `"subduction"`.
|
|
47
|
+
* A custom identity also carries the WebSocket URL to reach it on.
|
|
48
|
+
*/
|
|
49
|
+
syncServer?: PatchworkKeyhiveSyncServer;
|
|
50
|
+
/**
|
|
51
|
+
* Use the `idFactory` ARK injects into the repo config, which derives
|
|
52
|
+
* document ids from keyhive. Default true. `false` drops it and lets the
|
|
53
|
+
* Repo generate ids its own way.
|
|
54
|
+
*/
|
|
55
|
+
useIdFactory?: boolean;
|
|
56
|
+
}
|
|
57
|
+
|
|
48
58
|
export type PatchworkSyncServersOptions = {
|
|
49
59
|
/** wss:// URL for the legacy automerge-repo sync-server channel (connected on demand via connectClassicSync). Default: wss://sync3.automerge.org. Pass false to skip its preconnect hint. */
|
|
50
60
|
classic?: string | false;
|
|
51
|
-
|
|
61
|
+
/** wss:// URL for the subduction channel. Default: wss://subduction.sync.inkandswitch.com. Overrides the URL a named `keyhive.syncServer` would otherwise imply. */
|
|
62
|
+
subduction?: string;
|
|
63
|
+
};
|
|
52
64
|
|
|
53
65
|
export const DEFAULT_TITLE = "Patchwork";
|
|
54
66
|
|
|
@@ -81,12 +93,15 @@ export interface PatchworkSiteOptions {
|
|
|
81
93
|
backgroundColor?: string;
|
|
82
94
|
|
|
83
95
|
/**
|
|
84
|
-
*
|
|
85
|
-
*
|
|
86
|
-
*
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
96
|
+
* Enables keyhive for this build. `true` takes every default; an object
|
|
97
|
+
* picks the relay and turns individual behaviour off. Omitted or `false`
|
|
98
|
+
* builds a plain subduction repo.
|
|
99
|
+
*/
|
|
100
|
+
keyhive?: boolean | PatchworkKeyhiveOptions;
|
|
101
|
+
|
|
102
|
+
/**
|
|
103
|
+
* The sync-server URLs for this build. Also emitted as connection hints;
|
|
104
|
+
* pass `false` to keep the URLs but skip the hints.
|
|
90
105
|
*/
|
|
91
106
|
syncServers?: false | PatchworkSyncServersOptions;
|
|
92
107
|
|