@inkandswitch/patchwork-bootloader 0.2.5 → 0.2.6

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 CHANGED
@@ -1,5 +1,13 @@
1
1
  # @inkandswitch/patchwork-bootloader
2
2
 
3
+ ## 0.2.6
4
+
5
+ ### Patch Changes
6
+
7
+ - Updated dependencies [db46689]
8
+ - @inkandswitch/patchwork-providers@0.2.0
9
+ - @inkandswitch/patchwork-elements@1.0.0
10
+
3
11
  ## 0.2.5
4
12
 
5
13
  ### Patch Changes
@@ -16,7 +16,7 @@ import { resolvePath } from "@inkandswitch/patchwork-filesystem";
16
16
  // Small adapters — bundled directly into the SW
17
17
  import { IndexedDBStorageAdapter } from "@automerge/automerge-repo-storage-indexeddb";
18
18
  import { MessageChannelNetworkAdapter } from "@automerge/automerge-repo-network-messagechannel";
19
- import { WebSocketClientAdapter } from "@automerge/automerge-repo-network-websocket";
19
+ import { initializeAutomergeRepoKeyhiveRust, initKeyhiveWasm, } from "@automerge/automerge-repo-keyhive";
20
20
  // TEMPORARY: enable debug npm module in SW context (no localStorage available)
21
21
  let cachename = "default";
22
22
  let debugging = false;
@@ -48,6 +48,7 @@ const slog = SwLogger.open().then((logger) => {
48
48
  logger.info("sw-logger initialized");
49
49
  return logger;
50
50
  });
51
+ const siteName = typeof __SITE_NAME__ !== "undefined" ? __SITE_NAME__ : "tiny-patchwork";
51
52
  const cacheableStatuses = [200, 203, 204, 206];
52
53
  function log(...args) {
53
54
  if (!debugging)
@@ -69,10 +70,11 @@ self.addEventListener("activate", async () => {
69
70
  await clearOldCaches();
70
71
  clients.claim();
71
72
  });
72
- let repoPromise = null;
73
- function getRepo() {
74
- if (!repoPromise) {
75
- const p = (async () => {
73
+ let repoHivePromise = null;
74
+ const useKeyhive = typeof __KEYHIVE__ !== "undefined" && __KEYHIVE__;
75
+ function getRepoHive() {
76
+ if (!repoHivePromise) {
77
+ repoHivePromise = (async () => {
76
78
  const logger = await slog;
77
79
  logger.info("getRepo: starting");
78
80
  logger.info("fetching wasm modules");
@@ -83,45 +85,106 @@ function getRepo() {
83
85
  initSubductionSync(new Uint8Array(sdnWasmBuf));
84
86
  await initializeWasm(new Uint8Array(amWasmBuf));
85
87
  logger.info("wasm initialized");
86
- const signer = await WebCryptoSigner.setup();
88
+ if (!useKeyhive) {
89
+ const signer = await WebCryptoSigner.setup();
90
+ const repo = new Repo({
91
+ storage: new IndexedDBStorageAdapter(),
92
+ signer,
93
+ peerId: ("service-worker-" +
94
+ Math.random().toString(36).slice(2)),
95
+ async sharePolicy(peerId) {
96
+ return peerId.includes("storage-server");
97
+ },
98
+ enableRemoteHeadsGossiping: true,
99
+ subductionWebsocketEndpoints: SUBDUCTION_ENDPOINTS,
100
+ });
101
+ self.repo = repo;
102
+ logger.info("repo constructed (no keyhive), waiting for network subsystem");
103
+ repo.networkSubsystem.whenReady().then(() => {
104
+ logger.info("repo network subsystem ready");
105
+ });
106
+ return { repo };
107
+ }
108
+ initKeyhiveWasm();
109
+ const keyhiveStorage = new IndexedDBStorageAdapter(`${siteName}-keyhive`);
110
+ // Keyhive bootstrap needs to run before Repo creation but
111
+ // the adapter needs the subduction instance from the Repo.
112
+ // A deferred promise breaks the cycle.
113
+ let resolveRepoSubduction;
114
+ const repoSubductionPromise = new Promise((resolve) => {
115
+ resolveRepoSubduction = resolve;
116
+ });
117
+ // We use the Rust variant of Keyhive initialization to talk
118
+ // to the Rust keyhive-enabled subduction sync server.
119
+ const hive = await initializeAutomergeRepoKeyhiveRust({
120
+ storage: keyhiveStorage,
121
+ peerIdSuffix: `${siteName}-worker` + Math.random().toString(36).slice(2),
122
+ subduction: repoSubductionPromise,
123
+ automaticArchiveIngestion: true,
124
+ cachingMode: "periodic",
125
+ });
126
+ const signer = await hive.constructSubductionSigner();
87
127
  const repo = new Repo({
88
128
  storage: new IndexedDBStorageAdapter(),
89
129
  signer,
90
- peerId: ("service-worker-" +
91
- (Math.random() * 10000).toString(36).slice(2)),
92
- async sharePolicy(peerId) {
93
- return peerId.includes("storage-server");
94
- },
95
- enableRemoteHeadsGossiping: true,
96
130
  subductionWebsocketEndpoints: SUBDUCTION_ENDPOINTS,
97
- network: [new WebSocketClientAdapter("wss://sync3.automerge.org")],
131
+ peerId: hive.peerId,
132
+ enableRemoteHeadsGossiping: true,
133
+ idFactory: hive.idFactory,
134
+ //network: [new WebSocketClientAdapter("wss://sync3.automerge.org")],
98
135
  });
136
+ repo.subduction.then(resolveRepoSubduction);
137
+ hive.linkRepo(repo);
99
138
  self.repo = repo;
139
+ self.hive = hive;
100
140
  logger.info("repo constructed, waiting for network subsystem");
101
- // Don't block getRepo() on whenReady() — the network subsystem starts
141
+ // Don't block getRepoHive() on whenReady() — the network subsystem starts
102
142
  // with only the subduction adapter, and the MessageChannel adapter is
103
- // added later via connectPort (which awaits getRepo). Blocking here
143
+ // added later via connectPort (which awaits getRepoHive). Blocking here
104
144
  // would deadlock that path and starve the fetch handler.
105
145
  repo.networkSubsystem.whenReady().then(() => {
106
146
  logger.info("repo network subsystem ready");
107
147
  });
108
- return repo;
148
+ hive.networkAdapter.whenReady().then(() => {
149
+ hive.networkAdapter.syncKeyhive();
150
+ });
151
+ return { hive, repo };
109
152
  })();
110
153
  // If construction fails (e.g. wasm fetch errors out because the SW was
111
154
  // terminated mid-flight), don't permanently cache the rejection — clear
112
155
  // the slot so the next caller can retry from scratch.
113
- p.catch(() => {
114
- if (repoPromise === p)
115
- repoPromise = null;
156
+ repoHivePromise.catch(() => {
157
+ repoHivePromise = null;
116
158
  });
117
- repoPromise = p;
118
159
  }
119
- return repoPromise;
160
+ return repoHivePromise;
120
161
  }
121
162
  // Connect client MessagePorts to the repo for sync
122
163
  async function connectPort(port) {
123
- const repo = await getRepo();
124
- repo.networkSubsystem.addNetworkAdapter(new MessageChannelNetworkAdapter(port, { useWeakRef: true }));
164
+ const { hive, repo } = await getRepoHive();
165
+ const networkAdapter = new MessageChannelNetworkAdapter(port, { useWeakRef: true });
166
+ if (!hive) {
167
+ repo.networkSubsystem.addNetworkAdapter(networkAdapter);
168
+ return;
169
+ }
170
+ const onlyShareWithHardcodedServerPeerId = false;
171
+ const periodicallyRequestKeyhiveSync = false;
172
+ const keyhiveNetworkAdapter = hive.createKeyhiveNetworkAdapter(networkAdapter, onlyShareWithHardcodedServerPeerId, periodicallyRequestKeyhiveSync, 2000);
173
+ keyhiveNetworkAdapter.on("message", async (msg) => {
174
+ if ((msg.type === "sync" || msg.type === "request") && msg.documentId) {
175
+ const handle = repo.handles[msg.documentId];
176
+ if (!handle || handle.state === "unavailable") {
177
+ const url = `automerge:${msg.documentId}`;
178
+ repo.findWithProgress(url);
179
+ repo.shareConfigChanged();
180
+ }
181
+ }
182
+ });
183
+ keyhiveNetworkAdapter.on("ingest-remote", () => {
184
+ hive.networkAdapter.syncKeyhive?.();
185
+ repo.shareConfigChanged();
186
+ });
187
+ repo.networkSubsystem.addNetworkAdapter(keyhiveNetworkAdapter);
125
188
  }
126
189
  self.addEventListener("message", async (event) => {
127
190
  if (event.data.type == "ping") {
@@ -178,7 +241,7 @@ self.addEventListener("message", async (event) => {
178
241
  });
179
242
  // ── Automerge URL resolution ───────────────────────────────────────────
180
243
  async function resolveAutomergeUrl(automergeURL) {
181
- const repo = await getRepo();
244
+ const { repo } = await getRepoHive();
182
245
  const href = automergeURL.href;
183
246
  const [maybeAutomergeUrl, ...path] = href.split("/");
184
247
  if (!isValidAutomergeUrl(maybeAutomergeUrl)) {
@@ -286,7 +349,11 @@ self.addEventListener("fetch", (fetchEvent) => {
286
349
  const message = error instanceof Error
287
350
  ? `${error.message}\n\n${error.stack}`
288
351
  : String(error);
289
- console.error(`service worker error resolving ${request.url}${specialURL ? ` (for: ${specialURL})` : ""}.\n${message}`);
352
+ const logger = await slog;
353
+ logger.error(`service worker error resolving ${request.url}${specialURL ? ` (for: ${specialURL})` : ""}`, {
354
+ message: error instanceof Error ? error.message : String(error),
355
+ stack: error instanceof Error ? error.stack : undefined,
356
+ });
290
357
  if (match)
291
358
  return match;
292
359
  return new Response(message, {
package/dist/site.d.ts CHANGED
@@ -12,6 +12,7 @@
12
12
  * only does SW registration and port handoff).
13
13
  */
14
14
  import { type DocHandle, Repo, type AutomergeUrl, type StorageId } from "@automerge/vanillajs/slim";
15
+ import { type AutomergeRepoKeyhive } from "@automerge/automerge-repo-keyhive";
15
16
  import { ModuleWatcher } from "@inkandswitch/patchwork-filesystem";
16
17
  import { type AccountDoc } from "@inkandswitch/patchwork-plugins";
17
18
  import * as plugins from "@inkandswitch/patchwork-plugins";
@@ -23,6 +24,8 @@ declare global {
23
24
  Automerge: typeof import("@automerge/automerge");
24
25
  AutomergeRepo: typeof import("@automerge/automerge-repo");
25
26
  repo: Repo;
27
+ hive?: AutomergeRepoKeyhive;
28
+ getRepoChannel: () => MessagePort;
26
29
  patchwork: {
27
30
  repo: Repo;
28
31
  packages: ModuleWatcher;
@@ -72,6 +75,12 @@ export interface SiteConfig {
72
75
  * Ink & Switch's production Subduction storage.
73
76
  */
74
77
  remoteStorageIds?: StorageId[];
78
+ /**
79
+ * When true, initialize keyhive for access control.
80
+ * The Repo will use keyhive's network adapter, peerId, and idFactory
81
+ * instead of a sharePolicy.
82
+ */
83
+ keyhive?: boolean;
75
84
  }
76
85
  export interface BootResult {
77
86
  repo: Repo;
package/dist/site.js CHANGED
@@ -14,12 +14,14 @@
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, initializeAutomergeRepoKeyhive, } from "@automerge/automerge-repo-keyhive";
17
18
  // eslint-disable-next-line
18
19
  // @ts-ignore — initSync is a wasm-bindgen runtime helper not in the .d.ts
19
20
  import { initSync as initSubductionSync } from "@automerge/automerge-subduction/slim";
21
+ const siteName = typeof __SITE_NAME__ !== "undefined" ? __SITE_NAME__ : "tiny-patchwork";
20
22
  import { ModuleWatcher } from "@inkandswitch/patchwork-filesystem";
21
23
  import { openDocument, registerPatchworkViewElement, } from "@inkandswitch/patchwork-elements";
22
- import { registerFallbackProviderElement, registerRepoProviderElement, } from "@inkandswitch/patchwork-providers";
24
+ import { registerRepoProviderElement } from "@inkandswitch/patchwork-providers";
23
25
  import { getRegistry, registerPlugins, resolveAccountHandle, unregisterPlugins, } from "@inkandswitch/patchwork-plugins";
24
26
  import * as plugins from "@inkandswitch/patchwork-plugins";
25
27
  import setupServiceWorker from "./setup.js";
@@ -48,43 +50,71 @@ export async function bootPatchworkSite(config) {
48
50
  log(`booting`, config);
49
51
  await initializeWasm(automergeWasm);
50
52
  initSubductionSync(subductionWasm);
51
- const repo = new Repo({
52
- storage: new IndexedDBStorageAdapter(),
53
- async sharePolicy(peerId) {
54
- return peerId.includes("service-worker");
55
- },
56
- enableRemoteHeadsGossiping: true,
57
- peerId: `${config.titleSuffix}-tab-${crypto.randomUUID()}`,
58
- });
59
- repo.subscribeToRemotes(config.remoteStorageIds ?? [DEFAULT_REMOTE_STORAGE_ID]);
60
53
  const sw = await setupServiceWorker();
61
54
  if (!sw)
62
55
  throw new Error("Failed to set up service worker");
63
- let activeServiceWorkerPort;
64
- const connectServiceWorkerPort = async (port) => {
65
- const previousPort = activeServiceWorkerPort;
66
- activeServiceWorkerPort = port;
67
- const net = new MessageChannelNetworkAdapter(port);
68
- repo.networkSubsystem.addNetworkAdapter(net);
69
- await net.whenReady();
70
- previousPort?.close();
71
- };
72
- await sw.subscribeToRepoChannel(connectServiceWorkerPort);
73
- installDevConsoleGlobals(repo);
56
+ let hive;
57
+ if (config.keyhive) {
58
+ initKeyhiveWasm();
59
+ // Get the initial SW port via subscribeToRepoChannel, then pass it
60
+ // to keyhive init which wraps it in its own network adapter.
61
+ let resolvePort;
62
+ const portPromise = new Promise((r) => { resolvePort = r; });
63
+ sw.subscribeToRepoChannel((port) => { resolvePort(port); });
64
+ const swPort = await portPromise;
65
+ hive = await initializeAutomergeRepoKeyhive({
66
+ storage: new IndexedDBStorageAdapter(`${siteName}-keyhive`),
67
+ peerIdSuffix: siteName +
68
+ Math.random().toString(36).slice(2),
69
+ networkAdapter: new MessageChannelNetworkAdapter(swPort),
70
+ automaticArchiveIngestion: true,
71
+ cachingMode: "periodic",
72
+ onlyShareWithHardcodedServerPeerId: false,
73
+ });
74
+ }
75
+ const repo = hive
76
+ ? new Repo({
77
+ storage: new IndexedDBStorageAdapter(),
78
+ enableRemoteHeadsGossiping: true,
79
+ network: [hive.networkAdapter],
80
+ peerId: hive.peerId,
81
+ idFactory: hive.idFactory,
82
+ })
83
+ : new Repo({
84
+ storage: new IndexedDBStorageAdapter(),
85
+ async sharePolicy(peerId) {
86
+ return peerId.includes("service-worker");
87
+ },
88
+ enableRemoteHeadsGossiping: true,
89
+ peerId: `${config.titleSuffix}-tab-${crypto.randomUUID()}`,
90
+ });
91
+ repo.subscribeToRemotes(config.remoteStorageIds ?? [DEFAULT_REMOTE_STORAGE_ID]);
92
+ if (hive) {
93
+ await repo.networkSubsystem.whenReady();
94
+ hive.networkAdapter.syncKeyhive?.();
95
+ }
96
+ else {
97
+ let activeServiceWorkerPort;
98
+ const connectServiceWorkerPort = async (port) => {
99
+ const previousPort = activeServiceWorkerPort;
100
+ activeServiceWorkerPort = port;
101
+ const net = new MessageChannelNetworkAdapter(port);
102
+ repo.networkSubsystem.addNetworkAdapter(net);
103
+ await net.whenReady();
104
+ previousPort?.close();
105
+ };
106
+ await sw.subscribeToRepoChannel(connectServiceWorkerPort);
107
+ }
108
+ installDevConsoleGlobals(repo, hive);
74
109
  registerRepoProviderElement(repo);
75
- registerFallbackProviderElement();
76
110
  const rootElement = document.getElementById(config.rootElementId ?? "root");
77
111
  if (!rootElement) {
78
112
  throw new Error(`bootPatchworkSite: no element with id="${config.rootElementId ?? "root"}"`);
79
113
  }
80
114
  const repoProvider = document.createElement("repo-provider");
81
- const fallbackProvider = document.createElement("fallback-provider");
82
- rootElement.parentElement.insertBefore(fallbackProvider, rootElement);
83
- fallbackProvider.appendChild(repoProvider);
115
+ rootElement.parentElement.insertBefore(repoProvider, rootElement);
84
116
  repoProvider.appendChild(rootElement);
85
- // Registers `<patchwork-view>` and the `<patchwork-view-legacy>` it
86
- // delegates to in one go.
87
- registerPatchworkViewElement();
117
+ registerPatchworkViewElement(hive ? { hive } : {});
88
118
  // The watcher is started with the site's default-tools bundle alone so that
89
119
  // `resolveAccountHandle` below has something to await on (the `account`
90
120
  // datatype lives in that bundle today). The user's own module-settings URL
@@ -92,6 +122,7 @@ export async function bootPatchworkSite(config) {
92
122
  const moduleWatcher = new ModuleWatcher(repo, { system: defaultModulesUrl }, onModuleLoaded, unregisterPlugins);
93
123
  const accountDocHandle = await resolveAccountHandle(repo, {
94
124
  storageKey: config.accountStorageKey,
125
+ hive,
95
126
  });
96
127
  window.accountDocHandle = accountDocHandle;
97
128
  wireModuleSettingsWhenReady(accountDocHandle, moduleWatcher);
@@ -131,10 +162,18 @@ function resolveDefaultModulesUrl(builtin) {
131
162
  console.warn(`ignoring invalid defaultToolsUrl in localStorage: ${override}; using built-in default`);
132
163
  return builtin;
133
164
  }
134
- function installDevConsoleGlobals(repo) {
165
+ function installDevConsoleGlobals(repo, hive) {
135
166
  window.repo = repo;
136
167
  window.Automerge = Automerge;
137
168
  window.AutomergeRepo = AutomergeRepo;
169
+ if (hive) {
170
+ window.hive = hive;
171
+ }
172
+ window.getRepoChannel = () => {
173
+ const { port1, port2 } = new MessageChannel();
174
+ navigator.serviceWorker.controller.postMessage({ type: "port" }, [port2]);
175
+ return port1;
176
+ };
138
177
  }
139
178
  function onModuleLoaded(name, mod) {
140
179
  if (Array.isArray(mod.plugins)) {
@@ -30,12 +30,18 @@ export function importmap(options) {
30
30
  preserveSignature: "strict",
31
31
  });
32
32
  }
33
- // Emit automerge wasm so the service worker can fetch it
34
- const wasmPath = require.resolve("@automerge/automerge/automerge.wasm");
33
+ // Emit automerge, keyhive, and subduction wasm so the service worker can fetch them
34
+ const automergeWasmPath = require.resolve("@automerge/automerge/automerge.wasm");
35
35
  this.emitFile({
36
36
  type: "asset",
37
37
  fileName: "automerge.wasm",
38
- source: readFileSync(wasmPath),
38
+ source: readFileSync(automergeWasmPath),
39
+ });
40
+ const keyhiveWasmPath = require.resolve("@keyhive/keyhive/keyhive_wasm.wasm");
41
+ this.emitFile({
42
+ type: "asset",
43
+ fileName: "keyhive_wasm.wasm",
44
+ source: readFileSync(keyhiveWasmPath),
39
45
  });
40
46
  // Emit subduction wasm so the service worker can fetch it
41
47
  const subdWasmPath = require.resolve("@automerge/automerge-subduction/wasm");
package/package.json CHANGED
@@ -1,11 +1,11 @@
1
1
  {
2
2
  "name": "@inkandswitch/patchwork-bootloader",
3
- "version": "0.2.5",
3
+ "version": "0.2.6",
4
4
  "author": "chee",
5
5
  "type": "module",
6
6
  "license": "MIT",
7
7
  "devDependencies": {
8
- "@automerge/automerge-repo-keyhive": "0.2.0-alpha.1d",
8
+ "@automerge/automerge-repo-keyhive": "0.3.0-alpha.sub.1",
9
9
  "esbuild": "^0.23.1",
10
10
  "rollup": "^4.53.3"
11
11
  },
@@ -42,25 +42,26 @@
42
42
  "dependencies": {
43
43
  "@automerge/automerge": "3.3.0-fragments.1",
44
44
  "@automerge/automerge-repo": "2.6.0-subduction.23",
45
- "@automerge/automerge-subduction": "0.12.1",
45
+ "@automerge/automerge-subduction": "0.13.0",
46
46
  "@automerge/automerge-repo-network-messagechannel": "2.6.0-subduction.23",
47
47
  "@automerge/automerge-repo-network-websocket": "2.6.0-subduction.23",
48
48
  "@automerge/automerge-repo-storage-indexeddb": "2.6.0-subduction.23",
49
49
  "@automerge/vanillajs": "2.6.0-subduction.23",
50
+ "@keyhive/keyhive": "0.0.0-alpha.56",
50
51
  "@types/debug": "^4.1.12",
51
52
  "debug": "^4.4.3",
52
53
  "resolve.exports": "^2.0.3",
53
54
  "service-worker-types": "npm:@types/serviceworker@^0.0.153",
54
55
  "tinyargs": "^0.1.4",
55
- "@inkandswitch/patchwork-elements": "^0.2.0",
56
+ "@inkandswitch/patchwork-elements": "^1.0.0",
56
57
  "@inkandswitch/patchwork-filesystem": "^0.0.8",
57
58
  "@inkandswitch/patchwork-plugins": "^0.0.11",
58
- "@inkandswitch/patchwork-providers": "^0.1.2"
59
+ "@inkandswitch/patchwork-providers": "^0.2.0"
59
60
  },
60
61
  "peerDependencies": {
61
62
  "@automerge/automerge": "3.3.0-fragments.1",
62
63
  "@automerge/automerge-repo": "2.6.0-subduction.23",
63
- "@automerge/automerge-repo-keyhive": "0.2.0-alpha.1d",
64
+ "@automerge/automerge-repo-keyhive": "0.3.0-alpha.sub.1",
64
65
  "@automerge/vanillajs": "2.6.0-subduction.23"
65
66
  },
66
67
  "scripts": {
@@ -19,7 +19,7 @@ import {
19
19
  isValidAutomergeUrl,
20
20
  parseAutomergeUrl,
21
21
  stringifyAutomergeUrl,
22
- type PeerId,
22
+ type AutomergeUrl,
23
23
  } from "@automerge/automerge-repo/slim";
24
24
  import { resolvePath } from "@inkandswitch/patchwork-filesystem";
25
25
 
@@ -27,8 +27,17 @@ import { resolvePath } from "@inkandswitch/patchwork-filesystem";
27
27
  import { IndexedDBStorageAdapter } from "@automerge/automerge-repo-storage-indexeddb";
28
28
  import { MessageChannelNetworkAdapter } from "@automerge/automerge-repo-network-messagechannel";
29
29
  import { WebSocketClientAdapter } from "@automerge/automerge-repo-network-websocket";
30
+ import {
31
+ initializeAutomergeRepoKeyhiveRust,
32
+ initKeyhiveWasm,
33
+ type AutomergeRepoKeyhiveRust,
34
+ } from "@automerge/automerge-repo-keyhive";
35
+
36
+ declare const __SITE_NAME__: string;
37
+ declare const __KEYHIVE__: boolean;
30
38
 
31
39
  // TEMPORARY: enable debug npm module in SW context (no localStorage available)
40
+
32
41
  let cachename = "default";
33
42
  let debugging = false;
34
43
  const workerInstanceId = crypto.randomUUID();
@@ -64,6 +73,8 @@ const slog = SwLogger.open().then((logger) => {
64
73
  return logger;
65
74
  });
66
75
 
76
+ const siteName = typeof __SITE_NAME__ !== "undefined" ? __SITE_NAME__ : "tiny-patchwork";
77
+
67
78
  const cacheableStatuses = [200, 203, 204, 206];
68
79
 
69
80
  function log(...args: any[]) {
@@ -95,11 +106,16 @@ self.addEventListener("activate", async () => {
95
106
  clients.claim();
96
107
  });
97
108
 
98
- let repoPromise: Promise<Repo> | null = null;
109
+ let repoHivePromise: Promise<{
110
+ repo: Repo;
111
+ hive?: AutomergeRepoKeyhiveRust;
112
+ }> | null = null;
113
+
114
+ const useKeyhive = typeof __KEYHIVE__ !== "undefined" && __KEYHIVE__;
99
115
 
100
- function getRepo() {
101
- if (!repoPromise) {
102
- const p: Promise<Repo> = (async () => {
116
+ function getRepoHive() {
117
+ if (!repoHivePromise) {
118
+ repoHivePromise = (async () => {
103
119
  const logger = await slog;
104
120
  logger.info("getRepo: starting");
105
121
 
@@ -112,51 +128,130 @@ function getRepo() {
112
128
  await initializeWasm(new Uint8Array(amWasmBuf));
113
129
  logger.info("wasm initialized");
114
130
 
115
- const signer = await WebCryptoSigner.setup();
131
+ if (!useKeyhive) {
132
+ const signer = await WebCryptoSigner.setup();
133
+
134
+ const repo = new Repo({
135
+ storage: new IndexedDBStorageAdapter(),
136
+ signer,
137
+ peerId: ("service-worker-" +
138
+ Math.random().toString(36).slice(2)) as import("@automerge/automerge-repo/slim").PeerId,
139
+ async sharePolicy(peerId) {
140
+ return peerId.includes("storage-server");
141
+ },
142
+ enableRemoteHeadsGossiping: true,
143
+ subductionWebsocketEndpoints: SUBDUCTION_ENDPOINTS,
144
+ });
145
+
146
+ (self as any).repo = repo;
147
+ logger.info("repo constructed (no keyhive), waiting for network subsystem");
148
+
149
+ repo.networkSubsystem.whenReady().then(() => {
150
+ logger.info("repo network subsystem ready");
151
+ });
152
+
153
+ return { repo };
154
+ }
155
+
156
+ initKeyhiveWasm();
157
+ const keyhiveStorage = new IndexedDBStorageAdapter(
158
+ `${siteName}-keyhive`
159
+ );
160
+
161
+ // Keyhive bootstrap needs to run before Repo creation but
162
+ // the adapter needs the subduction instance from the Repo.
163
+ // A deferred promise breaks the cycle.
164
+ let resolveRepoSubduction!: (s: any) => void;
165
+ const repoSubductionPromise = new Promise((resolve) => {
166
+ resolveRepoSubduction = resolve;
167
+ });
168
+
169
+ // We use the Rust variant of Keyhive initialization to talk
170
+ // to the Rust keyhive-enabled subduction sync server.
171
+ const hive = await initializeAutomergeRepoKeyhiveRust({
172
+ storage: keyhiveStorage,
173
+ peerIdSuffix:
174
+ `${siteName}-worker` + Math.random().toString(36).slice(2),
175
+ subduction: repoSubductionPromise as any,
176
+ automaticArchiveIngestion: true,
177
+ cachingMode: "periodic",
178
+ });
179
+
180
+ const signer = await hive.constructSubductionSigner();
116
181
 
117
182
  const repo = new Repo({
118
183
  storage: new IndexedDBStorageAdapter(),
119
184
  signer,
120
- peerId: ("service-worker-" +
121
- (Math.random() * 10000).toString(36).slice(2)) as PeerId,
122
- async sharePolicy(peerId) {
123
- return peerId.includes("storage-server");
124
- },
125
- enableRemoteHeadsGossiping: true,
126
185
  subductionWebsocketEndpoints: SUBDUCTION_ENDPOINTS,
127
- network: [new WebSocketClientAdapter("wss://sync3.automerge.org")],
186
+ peerId: hive.peerId,
187
+ enableRemoteHeadsGossiping: true,
188
+ idFactory: hive.idFactory,
189
+ //network: [new WebSocketClientAdapter("wss://sync3.automerge.org")],
128
190
  });
129
191
 
192
+ repo.subduction.then(resolveRepoSubduction);
193
+
194
+ hive.linkRepo(repo);
195
+
130
196
  (self as any).repo = repo;
197
+ (self as any).hive = hive;
131
198
  logger.info("repo constructed, waiting for network subsystem");
132
199
 
133
- // Don't block getRepo() on whenReady() — the network subsystem starts
200
+ // Don't block getRepoHive() on whenReady() — the network subsystem starts
134
201
  // with only the subduction adapter, and the MessageChannel adapter is
135
- // added later via connectPort (which awaits getRepo). Blocking here
202
+ // added later via connectPort (which awaits getRepoHive). Blocking here
136
203
  // would deadlock that path and starve the fetch handler.
137
204
  repo.networkSubsystem.whenReady().then(() => {
138
205
  logger.info("repo network subsystem ready");
139
206
  });
140
207
 
141
- return repo;
208
+ hive.networkAdapter.whenReady().then(() => {
209
+ (hive.networkAdapter as any).syncKeyhive();
210
+ });
211
+
212
+ return { hive, repo };
142
213
  })();
143
214
  // If construction fails (e.g. wasm fetch errors out because the SW was
144
215
  // terminated mid-flight), don't permanently cache the rejection — clear
145
216
  // the slot so the next caller can retry from scratch.
146
- p.catch(() => {
147
- if (repoPromise === p) repoPromise = null;
217
+ repoHivePromise.catch(() => {
218
+ repoHivePromise = null;
148
219
  });
149
- repoPromise = p;
150
220
  }
151
- return repoPromise;
221
+ return repoHivePromise;
152
222
  }
153
223
 
154
224
  // Connect client MessagePorts to the repo for sync
155
225
  async function connectPort(port: MessagePort) {
156
- const repo = await getRepo();
157
- repo.networkSubsystem.addNetworkAdapter(
158
- new MessageChannelNetworkAdapter(port, { useWeakRef: true })
159
- );
226
+ const { hive, repo } = await getRepoHive();
227
+ const networkAdapter = new MessageChannelNetworkAdapter(port, { useWeakRef: true });
228
+
229
+ if (!hive) {
230
+ repo.networkSubsystem.addNetworkAdapter(networkAdapter);
231
+ return;
232
+ }
233
+
234
+ const onlyShareWithHardcodedServerPeerId = false;
235
+ const periodicallyRequestKeyhiveSync = false;
236
+ const keyhiveNetworkAdapter = hive.createKeyhiveNetworkAdapter(networkAdapter, onlyShareWithHardcodedServerPeerId, periodicallyRequestKeyhiveSync, 2000);
237
+
238
+ keyhiveNetworkAdapter.on("message", async (msg: any) => {
239
+ if ((msg.type === "sync" || msg.type === "request") && msg.documentId) {
240
+ const handle = repo.handles[msg.documentId];
241
+ if (!handle || handle.state === "unavailable") {
242
+ const url = `automerge:${msg.documentId}` as AutomergeUrl;
243
+ repo.findWithProgress(url);
244
+ repo.shareConfigChanged();
245
+ }
246
+ }
247
+ });
248
+
249
+ (keyhiveNetworkAdapter as any).on("ingest-remote", () => {
250
+ (hive.networkAdapter as any).syncKeyhive?.();
251
+ repo.shareConfigChanged();
252
+ });
253
+
254
+ repo.networkSubsystem.addNetworkAdapter(keyhiveNetworkAdapter);
160
255
  }
161
256
 
162
257
  self.addEventListener("message", async (event) => {
@@ -219,7 +314,7 @@ self.addEventListener("message", async (event) => {
219
314
  // ── Automerge URL resolution ───────────────────────────────────────────
220
315
 
221
316
  async function resolveAutomergeUrl(automergeURL: URL): Promise<Response> {
222
- const repo = await getRepo();
317
+ const { repo } = await getRepoHive();
223
318
  const href = automergeURL.href;
224
319
  const [maybeAutomergeUrl, ...path] = href.split("/");
225
320
 
@@ -362,8 +457,13 @@ self.addEventListener("fetch", (fetchEvent: FetchEvent) => {
362
457
  error instanceof Error
363
458
  ? `${error.message}\n\n${error.stack}`
364
459
  : String(error);
365
- console.error(
366
- `service worker error resolving ${request.url}${specialURL ? ` (for: ${specialURL})` : ""}.\n${message}`
460
+ const logger = await slog;
461
+ logger.error(
462
+ `service worker error resolving ${request.url}${specialURL ? ` (for: ${specialURL})` : ""}`,
463
+ {
464
+ message: error instanceof Error ? error.message : String(error),
465
+ stack: error instanceof Error ? error.stack : undefined,
466
+ }
367
467
  );
368
468
  if (match) return match;
369
469
 
package/src/site.ts CHANGED
@@ -28,19 +28,25 @@ import {
28
28
  } from "@automerge/vanillajs/slim";
29
29
  import * as Automerge from "@automerge/automerge/slim";
30
30
  import * as AutomergeRepo from "@automerge/automerge-repo/slim";
31
+ import {
32
+ initKeyhiveWasm,
33
+ initializeAutomergeRepoKeyhive,
34
+ type AutomergeRepoKeyhive,
35
+ } from "@automerge/automerge-repo-keyhive";
31
36
  // eslint-disable-next-line
32
37
  // @ts-ignore — initSync is a wasm-bindgen runtime helper not in the .d.ts
33
38
  import { initSync as initSubductionSync } from "@automerge/automerge-subduction/slim";
34
39
 
40
+ declare const __SITE_NAME__: string;
41
+ const siteName =
42
+ typeof __SITE_NAME__ !== "undefined" ? __SITE_NAME__ : "tiny-patchwork";
43
+
35
44
  import { ModuleWatcher } from "@inkandswitch/patchwork-filesystem";
36
45
  import {
37
46
  openDocument,
38
47
  registerPatchworkViewElement,
39
48
  } from "@inkandswitch/patchwork-elements";
40
- import {
41
- registerFallbackProviderElement,
42
- registerRepoProviderElement,
43
- } from "@inkandswitch/patchwork-providers";
49
+ import { registerRepoProviderElement } from "@inkandswitch/patchwork-providers";
44
50
  import {
45
51
  type AccountDoc,
46
52
  type DatatypeDescription,
@@ -64,6 +70,8 @@ declare global {
64
70
  Automerge: typeof import("@automerge/automerge");
65
71
  AutomergeRepo: typeof import("@automerge/automerge-repo");
66
72
  repo: Repo;
73
+ hive?: AutomergeRepoKeyhive;
74
+ getRepoChannel: () => MessagePort;
67
75
  patchwork: {
68
76
  repo: Repo;
69
77
  packages: ModuleWatcher;
@@ -120,6 +128,13 @@ export interface SiteConfig {
120
128
  * Ink & Switch's production Subduction storage.
121
129
  */
122
130
  remoteStorageIds?: StorageId[];
131
+
132
+ /**
133
+ * When true, initialize keyhive for access control.
134
+ * The Repo will use keyhive's network adapter, peerId, and idFactory
135
+ * instead of a sharePolicy.
136
+ */
137
+ keyhive?: boolean;
123
138
  }
124
139
 
125
140
  export interface BootResult {
@@ -158,37 +173,74 @@ export async function bootPatchworkSite(
158
173
  await initializeWasm(automergeWasm);
159
174
  initSubductionSync(subductionWasm);
160
175
 
161
- const repo = new Repo({
162
- storage: new IndexedDBStorageAdapter(),
163
- async sharePolicy(peerId) {
164
- return peerId.includes("service-worker");
165
- },
166
- enableRemoteHeadsGossiping: true,
167
- peerId:
168
- `${config.titleSuffix}-tab-${crypto.randomUUID()}` as AutomergeRepo.PeerId,
169
- });
176
+ const sw = await setupServiceWorker();
177
+ if (!sw) throw new Error("Failed to set up service worker");
178
+
179
+ let hive: AutomergeRepoKeyhive | undefined;
180
+ if (config.keyhive) {
181
+ initKeyhiveWasm();
182
+
183
+ // Get the initial SW port via subscribeToRepoChannel, then pass it
184
+ // to keyhive init which wraps it in its own network adapter.
185
+ let resolvePort!: (port: MessagePort) => void;
186
+ const portPromise = new Promise<MessagePort>((r) => { resolvePort = r; });
187
+ sw.subscribeToRepoChannel((port) => { resolvePort(port); });
188
+ const swPort = await portPromise;
189
+
190
+ hive = await initializeAutomergeRepoKeyhive({
191
+ storage: new IndexedDBStorageAdapter(
192
+ `${siteName}-keyhive`
193
+ ),
194
+ peerIdSuffix:
195
+ siteName +
196
+ Math.random().toString(36).slice(2),
197
+ networkAdapter: new MessageChannelNetworkAdapter(swPort),
198
+ automaticArchiveIngestion: true,
199
+ cachingMode: "periodic",
200
+ onlyShareWithHardcodedServerPeerId: false,
201
+ });
202
+ }
170
203
 
204
+ const repo = hive
205
+ ? new Repo({
206
+ storage: new IndexedDBStorageAdapter(),
207
+ enableRemoteHeadsGossiping: true,
208
+ network: [hive.networkAdapter],
209
+ peerId: hive.peerId,
210
+ idFactory: hive.idFactory,
211
+ })
212
+ : new Repo({
213
+ storage: new IndexedDBStorageAdapter(),
214
+ async sharePolicy(peerId) {
215
+ return peerId.includes("service-worker");
216
+ },
217
+ enableRemoteHeadsGossiping: true,
218
+ peerId:
219
+ `${config.titleSuffix}-tab-${crypto.randomUUID()}` as AutomergeRepo.PeerId,
220
+ });
171
221
  repo.subscribeToRemotes(
172
222
  config.remoteStorageIds ?? [DEFAULT_REMOTE_STORAGE_ID]
173
223
  );
174
224
 
175
- const sw = await setupServiceWorker();
176
- if (!sw) throw new Error("Failed to set up service worker");
177
- let activeServiceWorkerPort: MessagePort | undefined;
178
- const connectServiceWorkerPort = async (port: MessagePort) => {
179
- const previousPort = activeServiceWorkerPort;
180
- activeServiceWorkerPort = port;
181
- const net = new MessageChannelNetworkAdapter(port);
182
- repo.networkSubsystem.addNetworkAdapter(net);
183
- await net.whenReady();
184
- previousPort?.close();
185
- };
186
- await sw.subscribeToRepoChannel(connectServiceWorkerPort);
225
+ if (hive) {
226
+ await repo.networkSubsystem.whenReady();
227
+ (hive.networkAdapter as any).syncKeyhive?.();
228
+ } else {
229
+ let activeServiceWorkerPort: MessagePort | undefined;
230
+ const connectServiceWorkerPort = async (port: MessagePort) => {
231
+ const previousPort = activeServiceWorkerPort;
232
+ activeServiceWorkerPort = port;
233
+ const net = new MessageChannelNetworkAdapter(port);
234
+ repo.networkSubsystem.addNetworkAdapter(net);
235
+ await net.whenReady();
236
+ previousPort?.close();
237
+ };
238
+ await sw.subscribeToRepoChannel(connectServiceWorkerPort);
239
+ }
187
240
 
188
- installDevConsoleGlobals(repo);
241
+ installDevConsoleGlobals(repo, hive);
189
242
 
190
243
  registerRepoProviderElement(repo);
191
- registerFallbackProviderElement();
192
244
 
193
245
  const rootElement = document.getElementById(config.rootElementId ?? "root");
194
246
  if (!rootElement) {
@@ -197,14 +249,10 @@ export async function bootPatchworkSite(
197
249
  );
198
250
  }
199
251
  const repoProvider = document.createElement("repo-provider");
200
- const fallbackProvider = document.createElement("fallback-provider");
201
- rootElement.parentElement!.insertBefore(fallbackProvider, rootElement);
202
- fallbackProvider.appendChild(repoProvider);
252
+ rootElement.parentElement!.insertBefore(repoProvider, rootElement);
203
253
  repoProvider.appendChild(rootElement);
204
254
 
205
- // Registers `<patchwork-view>` and the `<patchwork-view-legacy>` it
206
- // delegates to in one go.
207
- registerPatchworkViewElement();
255
+ registerPatchworkViewElement(hive ? { hive } : {});
208
256
 
209
257
  // The watcher is started with the site's default-tools bundle alone so that
210
258
  // `resolveAccountHandle` below has something to await on (the `account`
@@ -219,6 +267,7 @@ export async function bootPatchworkSite(
219
267
 
220
268
  const accountDocHandle = await resolveAccountHandle(repo, {
221
269
  storageKey: config.accountStorageKey,
270
+ hive,
222
271
  });
223
272
 
224
273
  window.accountDocHandle = accountDocHandle;
@@ -270,10 +319,21 @@ function resolveDefaultModulesUrl(builtin: AutomergeUrl): AutomergeUrl {
270
319
  return builtin;
271
320
  }
272
321
 
273
- function installDevConsoleGlobals(repo: Repo): void {
322
+ function installDevConsoleGlobals(
323
+ repo: Repo,
324
+ hive: AutomergeRepoKeyhive | undefined
325
+ ): void {
274
326
  window.repo = repo;
275
327
  window.Automerge = Automerge;
276
328
  window.AutomergeRepo = AutomergeRepo;
329
+ if (hive) {
330
+ window.hive = hive;
331
+ }
332
+ window.getRepoChannel = () => {
333
+ const { port1, port2 } = new MessageChannel();
334
+ navigator.serviceWorker.controller!.postMessage({ type: "port" }, [port2]);
335
+ return port1;
336
+ };
277
337
  }
278
338
 
279
339
  function onModuleLoaded(name: string, mod: any): void {
@@ -45,12 +45,22 @@ export function importmap(options?: PatchworkVitePluginOptions): Plugin {
45
45
  });
46
46
  }
47
47
 
48
- // Emit automerge wasm so the service worker can fetch it
49
- const wasmPath = require.resolve("@automerge/automerge/automerge.wasm");
48
+ // Emit automerge, keyhive, and subduction wasm so the service worker can fetch them
49
+ const automergeWasmPath = require.resolve(
50
+ "@automerge/automerge/automerge.wasm"
51
+ );
50
52
  this.emitFile({
51
53
  type: "asset",
52
54
  fileName: "automerge.wasm",
53
- source: readFileSync(wasmPath),
55
+ source: readFileSync(automergeWasmPath),
56
+ });
57
+ const keyhiveWasmPath = require.resolve(
58
+ "@keyhive/keyhive/keyhive_wasm.wasm"
59
+ );
60
+ this.emitFile({
61
+ type: "asset",
62
+ fileName: "keyhive_wasm.wasm",
63
+ source: readFileSync(keyhiveWasmPath),
54
64
  });
55
65
 
56
66
  // Emit subduction wasm so the service worker can fetch it