Skip to content

Commit f065a8d

Browse files
expedealexjg
authored andcommitted
Add Subduction integration to automerge-repo
Integrates the Subduction sync engine (github.com/inkandswitch/subduction) directly into the core automerge-repo package as a new sync source alongside the legacy StorageSubsystem + NetworkSubsystem path. Why integrate rather than wrap: Subduction's sedimentree-based, fingerprint-driven sync model needs hooks into document lifecycle events that the existing DocumentSource interface already provides. Folding it into core (under packages/automerge-repo/src/subduction/) means consumers install one package and Wasm modules auto-init via the fullfat entrypoint. What lands in this commit: - src/subduction/source.ts -- SubductionSource implements DocumentSource. Owns the per-doc SedimentreeEntry map, manages a throttled #save with a single-flight gate, derives fragment boundary requests, and drains commits via flush(). - src/subduction/storage.ts -- SubductionStorageBridge wraps any StorageAdapterInterface to implement Subduction's SedimentreeStorage. Tracks pending writes per SedimentreeId so awaitSettled(sids?) can be scoped. - src/subduction/network.ts -- websocket-transport adapter for Subduction's Transport interface, with reconnect. - src/subduction/helpers.ts -- DocumentId <-> SedimentreeId conversions (zero-pad 16-byte DocIds to 32-byte SedimentreeIds); automergeMeta() symbol-introspection helper to access Automerge's internal handle. - Persistent and in-memory signers; isomorphic-ws transport. - IDB batching + SharedWorker compatibility for browser usage. - Ephemeral message support routed through Subduction so peers connected only via websocket still receive ephemerals. - Examples (react-counter, react-todo, react-use-presence, svelte-counter, sync-server) ported to the new repo. - Subduction-tagged npm dist-tag, Nix flake updates, .npmrc. Includes the Subduction batching API hookup (commits/fragments batched into a single round-trip on the wasm boundary) and NetworkAdapterInterface.state for observing adapter state changes from the bridge. Squashed from 21 work-in-progress commits (initial port through "Add NetworkAdapterInterface.state"); see refs/backup/subductionjs- original-tip for the unsquashed history.
1 parent 3075528 commit f065a8d

56 files changed

Lines changed: 3765 additions & 263 deletions

File tree

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

.npmrc

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,2 @@
1+
tag=subduction
2+
publish-branch=subduction-on-the-refactor-expede2

examples/react-counter/package.json

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,7 @@
22
"name": "@automerge/automerge-repo-demo-counter",
33
"repository": "https://github.com/automerge/automerge-repo/tree/master/examples/react-counter",
44
"private": true,
5-
"version": "2.6.0-alpha.0",
5+
"version": "2.6.0-subduction.7",
66
"type": "module",
77
"scripts": {
88
"dev": "vite --open",

examples/react-counter/src/main.tsx

Lines changed: 31 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -9,13 +9,21 @@ import {
99
IndexedDBStorageAdapter,
1010
RepoContext,
1111
} from "@automerge/react"
12+
// @ts-ignore — initSync is not in the type declarations but is exported at runtime
13+
import { initSync } from "@automerge/automerge-subduction/slim"
14+
// @ts-ignore — wasm-base64 has no type declarations
15+
import { wasmBase64 } from "@automerge/automerge-subduction/wasm-base64"
1216

13-
// We run the network & storage in a separate file and the tabs themselves are stateless and lightweight.
14-
// This means we only ever create one websocket connection to the sync server, we only do our writes in one place
15-
// (no race conditions) and we get local real-time sync without the overhead of broadcast channel.
16-
// The downside is that to debug any problems with the sync server you'll need to find the shared-worker and inspect it.
17-
// In Chrome-derived browsers the URL is chrome://inspect/#workers. In Firefox it's about:debugging#workers.
18-
// In Safari it's Develop > Show Web Inspector > Storage > IndexedDB > automerge-repo-demo-counter.
17+
// Initialize Subduction Wasm before constructing the Repo
18+
initSync(Uint8Array.from(atob(wasmBase64), c => c.charCodeAt(0)))
19+
20+
// We run the network & storage in a SharedWorker so we only create one
21+
// Subduction WebSocket connection to the sync server and get local
22+
// real-time sync via MessageChannel without BroadcastChannel overhead.
23+
//
24+
// To debug the shared worker:
25+
// Chrome: chrome://inspect/#workers
26+
// Firefox: about:debugging#workers
1927

2028
const sharedWorker = new SharedWorker(
2129
new URL("./shared-worker.ts", import.meta.url),
@@ -42,12 +50,27 @@ declare global {
4250
const rootDocUrl = `${document.location.hash.substring(1)}`
4351
let handle
4452
if (isValidAutomergeUrl(rootDocUrl)) {
45-
handle = await repo.find(rootDocUrl)
53+
// The SharedWorker connection may not be ready yet, so retry find()
54+
// until the document becomes available.
55+
const MAX_RETRIES = 10
56+
const RETRY_MS = 500
57+
for (let attempt = 0; ; attempt++) {
58+
try {
59+
handle = await repo.find(rootDocUrl)
60+
break
61+
} catch {
62+
if (attempt >= MAX_RETRIES)
63+
throw new Error(
64+
`Document ${rootDocUrl} unavailable after ${MAX_RETRIES} retries`
65+
)
66+
await new Promise(r => setTimeout(r, RETRY_MS))
67+
}
68+
}
4669
} else {
4770
handle = repo.create<{ count: number }>({ count: 0 })
4871
}
4972
const docUrl = (document.location.hash = handle.url)
50-
window.handle = handle // we'll use this later for experimentation
73+
window.handle = handle
5174
window.repo = repo
5275

5376
ReactDOM.createRoot(document.getElementById("root") as HTMLElement).render(

examples/react-counter/src/shared-worker.ts

Lines changed: 20 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -12,27 +12,31 @@ self.addEventListener("connect", (e: MessageEvent) => {
1212
// even if the event arrives first.
1313
// Ideally Chrome would fix this upstream but this isn't a terrible hack.
1414
const repoPromise = (async () => {
15-
const { Repo } = await import("@automerge/automerge-repo")
16-
const { IndexedDBStorageAdapter } = await import(
17-
"@automerge/automerge-repo-storage-indexeddb"
18-
)
19-
const { WebSocketClientAdapter } = await import(
20-
"@automerge/automerge-repo-network-websocket"
15+
const { Repo, IndexedDBStorageAdapter, MessageChannelNetworkAdapter } =
16+
await import("@automerge/react")
17+
18+
// @ts-ignore — initSync is not in the type declarations but is exported at runtime
19+
const { initSync } = await import("@automerge/automerge-subduction/slim")
20+
// @ts-ignore — wasm-base64 has no type declarations
21+
const { wasmBase64 } = await import(
22+
"@automerge/automerge-subduction/wasm-base64"
2123
)
22-
return new Repo({
23-
storage: new IndexedDBStorageAdapter(),
24-
network: [new WebSocketClientAdapter("ws://sync.automerge.org")],
25-
peerId: ("shared-worker-" + Math.round(Math.random() * 10000)) as any,
26-
sharePolicy: async peerId => peerId.includes("storage-server"),
27-
})
24+
initSync(Uint8Array.from(atob(wasmBase64), (c: string) => c.charCodeAt(0)))
25+
26+
return {
27+
repo: new Repo({
28+
storage: new IndexedDBStorageAdapter(),
29+
subductionWebsocketEndpoints: ["wss://subduction.sync.inkandswitch.com"],
30+
peerId: ("shared-worker-" + Math.round(Math.random() * 10000)) as any,
31+
sharePolicy: async peerId => peerId.includes("storage-server"),
32+
}),
33+
MessageChannelNetworkAdapter,
34+
}
2835
})()
2936

3037
async function configureRepoNetworkPort(port: MessagePort) {
31-
const repo = await repoPromise
38+
const { repo, MessageChannelNetworkAdapter } = await repoPromise
3239

33-
const { MessageChannelNetworkAdapter } = await import(
34-
"@automerge/automerge-repo-network-messagechannel"
35-
)
3640
// be careful to not accidentally create a strong reference to port
3741
// that will prevent dead ports from being garbage collected
3842
repo.networkSubsystem.addNetworkAdapter(

examples/react-todo/package.json

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,7 @@
22
"name": "@automerge/automerge-repo-demo-todo",
33
"repository": "https://github.com/automerge/automerge-repo/tree/master/examples/react-todo",
44
"private": true,
5-
"version": "2.6.0-alpha.0",
5+
"version": "2.6.0-subduction.7",
66
"type": "module",
77
"scripts": {
88
"dev": "vite --open",

examples/react-todo/src/main.tsx

Lines changed: 11 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -1,12 +1,14 @@
11
import {
2-
DocHandle,
32
Repo,
43
isValidAutomergeUrl,
5-
BroadcastChannelNetworkAdapter,
6-
WebSocketClientAdapter,
74
IndexedDBStorageAdapter,
85
RepoContext,
6+
BroadcastChannelNetworkAdapter,
97
} from "@automerge/react"
8+
// @ts-ignore — initSync is not in the type declarations but is exported at runtime
9+
import { initSync } from "@automerge/automerge-subduction/slim"
10+
// @ts-ignore — wasm-base64 has no type declarations
11+
import { wasmBase64 } from "@automerge/automerge-subduction/wasm-base64"
1012

1113
import React, { Suspense } from "react"
1214
import { ErrorBoundary } from "react-error-boundary"
@@ -15,17 +17,17 @@ import { App } from "./App.js"
1517
import { State } from "./types.js"
1618
import "./index.css"
1719

20+
// Initialize Subduction Wasm before constructing the Repo
21+
initSync(Uint8Array.from(atob(wasmBase64), c => c.charCodeAt(0)))
22+
1823
const repo = new Repo({
19-
network: [
20-
new BroadcastChannelNetworkAdapter(),
21-
new WebSocketClientAdapter("ws://localhost:3030"),
22-
],
2324
storage: new IndexedDBStorageAdapter("automerge-repo-demo-todo"),
25+
subductionWebsocketEndpoints: ["wss://subduction.sync.inkandswitch.com"],
2426
})
2527

2628
declare global {
2729
interface Window {
28-
handle: DocHandle<unknown>
30+
handle: any
2931
repo: Repo
3032
}
3133
}
@@ -38,7 +40,7 @@ if (isValidAutomergeUrl(rootDocUrl)) {
3840
handle = repo.create<State>({ todos: [] })
3941
}
4042
const docUrl = (document.location.hash = handle.url)
41-
window.handle = handle // we'll use this later for experimentation
43+
window.handle = handle
4244
window.repo = repo
4345

4446
ReactDOM.createRoot(document.getElementById("root") as HTMLElement).render(

examples/react-use-presence/package.json

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
{
22
"name": "automerge-use-awareness-example-project",
3-
"version": "2.6.0-alpha.0",
3+
"version": "2.6.0-subduction.7",
44
"private": true,
55
"repository": "https://github.com/automerge/automerge-repo/tree/master/examples/react-use-presence",
66
"type": "module",

examples/react-use-presence/src/App.tsx

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -18,7 +18,7 @@ export function App({ url }: { url: AutomergeUrl }) {
1818
const newCount = localState?.count
1919
const count = doc?.count ?? 0
2020

21-
const peers = peerStates.getStates()
21+
const peers = peerStates.peers
2222

2323
return (
2424
<div>
@@ -48,7 +48,7 @@ export function App({ url }: { url: AutomergeUrl }) {
4848
</div>
4949
<div>
5050
Peer states:
51-
{Object.values(peers).map(state => (
51+
{peers.map(state => (
5252
<span
5353
key={state.peerId}
5454
style={{ backgroundColor: "silver", marginRight: "2px" }}
@@ -79,7 +79,7 @@ export function App({ url }: { url: AutomergeUrl }) {
7979
{
8080
doc,
8181
localState,
82-
peerStates: Object.values(peers),
82+
peerStates: peers,
8383
},
8484
null,
8585
2

examples/react-use-presence/src/main.tsx

Lines changed: 8 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -4,14 +4,20 @@ import { App } from "./App"
44
import {
55
Repo,
66
isValidAutomergeUrl,
7-
BroadcastChannelNetworkAdapter,
87
IndexedDBStorageAdapter,
98
RepoContext,
109
} from "@automerge/react"
10+
// @ts-ignore — initSync is not in the type declarations but is exported at runtime
11+
import { initSync } from "@automerge/automerge-subduction/slim"
12+
// @ts-ignore — wasm-base64 has no type declarations
13+
import { wasmBase64 } from "@automerge/automerge-subduction/wasm-base64"
14+
15+
// Initialize Subduction Wasm before constructing the Repo
16+
initSync(Uint8Array.from(atob(wasmBase64), c => c.charCodeAt(0)))
1117

1218
const repo = new Repo({
1319
storage: new IndexedDBStorageAdapter("use-awareness-example"),
14-
network: [new BroadcastChannelNetworkAdapter()],
20+
subductionWebsocketEndpoints: ["wss://subduction.sync.inkandswitch.com"],
1521
})
1622

1723
const rootDocUrl = `${document.location.hash.substring(1)}`

examples/react-use-presence/vite.config.js

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,9 @@ import wasm from "vite-plugin-wasm"
44

55
export default defineConfig({
66
plugins: [react(), wasm()],
7+
build: {
8+
target: "esnext",
9+
},
710
worker: {
811
plugins: () => [wasm()],
912
},

0 commit comments

Comments
 (0)