Skip to content

Commit e8bc6b1

Browse files
committed
Sync resources between android instances, ditch DHT
1 parent 20c99c2 commit e8bc6b1

40 files changed

Lines changed: 2649 additions & 732 deletions

.claude/settings.local.json

Lines changed: 10 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -78,7 +78,16 @@
7878
"Read(//opt/flutter/bin/**)",
7979
"Read(//Users/joep/**)",
8080
"Bash(mise which:*)",
81-
"Bash(mise ls:*)"
81+
"Bash(mise ls:*)",
82+
"Bash(export PATH=\"/Users/joep/.local/share/mise/installs/flutter/3.22.1-stable/bin:$PATH\")",
83+
"Bash(flutter build:*)",
84+
"Bash(cargo doc:*)",
85+
"Bash(grep -rn \"pub fn.*change\\\\|pub fn.*history\\\\|pub fn.*oplog\\\\|get_all_change\" /Users/joep/.cargo/registry/src/*/loro-1.10.8/src/)",
86+
"WebSearch",
87+
"Bash(find /Users/joep/.cargo/registry/src/index.crates.io-1949cf8c6b5b557f/simple-dns-0.9.3/ -name \"*.rs\" -exec grep -l \"enum RData\" {} \\\\;)",
88+
"Bash(cargo test *)",
89+
"WebFetch(domain:docs.iroh.computer)",
90+
"Bash(grep -v \"^W/atomic\\\\|^$\")"
8291
]
8392
}
8493
}

AGENTS.md

Lines changed: 35 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -33,11 +33,12 @@ In E2E tests, most specs use `test.beforeEach(before)` from `test-utils.ts`, whi
3333
Atomic Server is a graph database with real-time sync, built on **Loro CRDT** for conflict-free collaborative editing.
3434

3535
### Crates
36-
- **`atomic_lib`** (`lib/`) — Core library. WASM-compatible (no `ring`, no `rt-multi-thread`). Contains Resource, Commit, Store, Loro integration, WS client, connected Client API.
37-
- **`atomic-server`** (`server/`) — Actix-web HTTP/WS server. Uses `atomic_lib` + sled DB + search (tantivy).
36+
- **`atomic_lib`** (`lib/`) — Core library. WASM-compatible (no `ring`, no `rt-multi-thread`). Contains Resource, Commit, Store, Loro integration, Iroh P2P sync, WS client, connected Client API.
37+
- **`atomic-server`** (`server/`) — Actix-web HTTP/WS server. Uses `atomic_lib` + redb + search (tantivy).
3838
- **`@tomic/lib`** (`browser/lib/`) — TypeScript client library.
3939
- **`@tomic/react`** (`browser/react/`) — React hooks.
4040
- **`data-browser`** (`browser/data-browser/`) — The web app (React + TipTap + Loro).
41+
- **`flutter/`** — Cross-platform canvas app (Android/iOS/Web). Uses `flutter_rust_bridge` to call `atomic_lib`. See `flutter/README.md` and `flutter/AGENTS.md`.
4142

4243
### Data model
4344
- **Resource** = property-value pairs with a Subject URL, backed by a Loro CRDT document.
@@ -139,12 +140,41 @@ Real-time: `useLoroSync` hook → `LORO_SYNC_UPDATE` WebSocket.
139140

140141
Loro OpLog time-travel: `doc.getAllChanges()` → sort → `doc.checkout(frontiers)` per version. Instant, no network round-trips.
141142

143+
## Iroh P2P Sync
144+
145+
Devices sync via [Iroh](https://iroh.computer) QUIC connections. The transport is in `lib/src/sync/`:
146+
147+
- **`peer.rs`** — Iroh endpoint, Router (must stay alive for incoming connections), persistent NodeID (secret key stored in redb), known peers list.
148+
- **`engine.rs`** — Transport-agnostic sync engine. Compares Loro version vectors, computes diffs, imports snapshots. Used by both WS and Iroh.
149+
- **`protocol.rs`** — Binary frame encoding: AUTH, SYNC, SYNC_DIFF, SYNC_PUSH, SYNC_OK, GET, UPDATE.
150+
151+
### Sync flow (QR pairing)
152+
1. Both devices start Iroh (`peer::start()`) → get persistent NodeID, connect to n0 relay
153+
2. Device A shows QR code containing `did:ad:node:<nodeId>`
154+
3. Device B scans QR → calls `peer_sync(nodeId)``sync_drive_with_peer()`
155+
4. B→A: AUTH, SYNC (with B's version vectors)
156+
5. A→B: SYNC_DIFF (what to push/pull), SYNC_PUSH (A's data)
157+
6. B→A: SYNC_PUSH (B's data for A's pull list)
158+
7. Both devices now have each other's data
159+
160+
### Key details
161+
- The `Router` must be kept alive globally (`ROUTER` static) — dropping it stops incoming connections.
162+
- After sending the final SYNC_PUSH, call `send.finish()` + short delay so the server processes it before the connection drops.
163+
- Loro snapshots are stored in `Tree::LoroSnapshots` keyed by `Subject::pure_id()` (strips query params/drive hints).
164+
- `collect_drive_subjects()` and `build_drive_vvs()` must use `pure_id()` consistently to match snapshot keys.
165+
166+
### Node identity
167+
- `did:ad:node:<hex>` — URI format for Iroh NodeIDs, used in QR codes and UI.
168+
- NodeIDs are persistent — derived from a secret key stored in redb (`Tree::PluginMeta`).
169+
- Known peers are also stored in `Tree::PluginMeta` as a JSON array.
170+
142171
## Testing
143172

144173
```
145174
cargo test -p atomic_lib --no-default-features # 76 tests
146175
cargo test -p atomic-server --lib # 23 tests
147176
cargo test -p atomic-server --test sync # E2E: real server, 2 agents, WS sync
177+
cargo test -p atomic_lib --features "iroh,discovery,db-redb" --lib -- sync::tests # Iroh sync tests
148178
cd browser/lib && pnpm test # 29 JS tests
149179
cd browser && pnpm run -r build # Full workspace build
150180
```
@@ -157,3 +187,6 @@ cd browser && pnpm run -r build # Full workspace build
157187
4. **CSP**: Includes `'wasm-unsafe-eval'` for Loro WASM in browser.
158188
5. **`build.rs`**: Watches `lib/src`, `react/src`, `data-browser/src`. Delete `server/assets_tmp` to force JS rebuild.
159189
6. **`CommitBuilder.push_propval`**: Starts from empty, not from resource's existing value. Load current value first if appending.
190+
7. **`flutter/rust/`** is excluded from the Cargo workspace (`Cargo.toml` `exclude`). It has its own `Cargo.toml` with a path dep to `../../lib`.
191+
8. **NDK 27 `llvm-strip`** corrupts large `.so` files. Debug builds use `doNotStrip '**/*.so'` in both `app/build.gradle` and `rust_builder/android/build.gradle`.
192+
9. **Iroh Router lifetime**: `peer::start()` returns a Router that must be kept alive. Dropping it silently stops incoming connections — no error, just "failed connecting to remote endpoint" on the other side.

Cargo.lock

Lines changed: 38 additions & 3 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

Cargo.toml

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -9,5 +9,8 @@ members = [
99
"plugin-examples/test-plugin",
1010
"atomic-plugin",
1111
]
12+
exclude = [
13+
"flutter/rust",
14+
]
1215

1316
# Tauri build is deprecated, see

browser/data-browser/src/locales/de.po

Lines changed: 45 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -419,6 +419,7 @@ msgstr "Kein Code-Verifizierer gefunden"
419419

420420
#: src/components/forms/SearchBox/SearchBox.tsx
421421
#: src/routes/LinkOpenRouter.tsx
422+
#: src/routes/SyncRoute.tsx
422423
msgid "Error"
423424
msgstr "Fehler"
424425

@@ -765,10 +766,14 @@ msgstr "Akzeptieren als {0}"
765766

766767
#. placeholder {0}: JSON.stringify(error)
767768
#. placeholder {0}: searchError.message
769+
#. placeholder {0}: data.error
770+
#. placeholder {0}: e
768771
#. placeholder {0}: resource.error.message
769772
#. placeholder {0}: resource.error.message
770773
#: src/components/forms/Field.tsx
771774
#: src/components/forms/SearchBox/SearchBoxWindow.tsx
775+
#: src/routes/SyncRoute.tsx
776+
#: src/routes/SyncRoute.tsx
772777
#: src/views/ResourceLine.tsx
773778
#: src/views/ResourceRow.tsx
774779
msgid "Error: {0}"
@@ -4749,6 +4754,8 @@ msgstr ""
47494754

47504755
#: src/components/SideBar/SyncMenuItem.tsx
47514756
#: src/routes/SyncRoute.tsx
4757+
#: src/routes/SyncRoute.tsx
4758+
#: src/routes/SyncRoute.tsx
47524759
msgid "Sync"
47534760
msgstr ""
47544761

@@ -4853,6 +4860,7 @@ msgstr ""
48534860

48544861
#: src/components/SideBar/SyncMenuItem.tsx
48554862
#: src/routes/SyncRoute.tsx
4863+
#: src/routes/SyncRoute.tsx
48564864
msgid "Syncing..."
48574865
msgstr ""
48584866

@@ -5040,6 +5048,7 @@ msgstr ""
50405048
msgid "Connected to {0}"
50415049
msgstr ""
50425050

5051+
#: src/routes/SyncRoute.tsx
50435052
#: src/routes/SyncRoute.tsx
50445053
msgid "+ Add"
50455054
msgstr ""
@@ -5057,6 +5066,41 @@ msgstr ""
50575066
msgid "How to run your own server"
50585067
msgstr ""
50595068

5069+
#~ msgid "Peer ID"
5070+
#~ msgstr ""
5071+
5072+
#. placeholder {0}: data.count
5073+
#. placeholder {1}: data.count !== 1 ? 's' : ''
5074+
#: src/routes/SyncRoute.tsx
5075+
msgid "Synced {0} resource{1}"
5076+
msgstr ""
5077+
5078+
#~ msgid "Peer sync"
5079+
#~ msgstr ""
5080+
5081+
#~ msgid "Paste device ID to sync with"
5082+
#~ msgstr ""
5083+
5084+
#: src/routes/SyncRoute.tsx
5085+
msgid "Peers"
5086+
msgstr ""
5087+
5088+
#: src/routes/SyncRoute.tsx
5089+
msgid "No peers connected"
5090+
msgstr ""
5091+
5092+
#~ msgid "Paste device ID"
5093+
#~ msgstr ""
5094+
5095+
#: src/routes/SyncRoute.tsx
5096+
msgid "Node DID"
5097+
msgstr ""
5098+
5099+
#. placeholder {0}: irohNodeId.slice(0, 12)
5100+
#: src/routes/SyncRoute.tsx
5101+
msgid "did:ad:node:{0}..."
5102+
msgstr ""
5103+
50605104
#: src/routes/SyncRoute.tsx
5061-
msgid "Peer ID"
5105+
msgid "Paste did:ad:node:..."
50625106
msgstr ""

browser/data-browser/src/locales/en.po

Lines changed: 46 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -23,10 +23,14 @@ msgstr "Upload"
2323

2424
#. placeholder {0}: JSON.stringify(error)
2525
#. placeholder {0}: searchError.message
26+
#. placeholder {0}: data.error
27+
#. placeholder {0}: e
2628
#. placeholder {0}: resource.error.message
2729
#. placeholder {0}: resource.error.message
2830
#: src/components/forms/Field.tsx
2931
#: src/components/forms/SearchBox/SearchBoxWindow.tsx
32+
#: src/routes/SyncRoute.tsx
33+
#: src/routes/SyncRoute.tsx
3034
#: src/views/ResourceLine.tsx
3135
#: src/views/ResourceRow.tsx
3236
msgid "Error: {0}"
@@ -1895,6 +1899,7 @@ msgstr "No code verifier found"
18951899

18961900
#: src/components/forms/SearchBox/SearchBox.tsx
18971901
#: src/routes/LinkOpenRouter.tsx
1902+
#: src/routes/SyncRoute.tsx
18981903
msgid "Error"
18991904
msgstr "Error"
19001905

@@ -4709,6 +4714,8 @@ msgstr "Failed to decode Loro snapshot"
47094714

47104715
#: src/components/SideBar/SyncMenuItem.tsx
47114716
#: src/routes/SyncRoute.tsx
4717+
#: src/routes/SyncRoute.tsx
4718+
#: src/routes/SyncRoute.tsx
47124719
msgid "Sync"
47134720
msgstr "Sync"
47144721

@@ -4815,6 +4822,7 @@ msgstr "In sync"
48154822

48164823
#: src/components/SideBar/SyncMenuItem.tsx
48174824
#: src/routes/SyncRoute.tsx
4825+
#: src/routes/SyncRoute.tsx
48184826
msgid "Syncing..."
48194827
msgstr "Syncing..."
48204828

@@ -5004,6 +5012,7 @@ msgstr "Running in offline mode. Reconnect in the sync menu."
50045012
msgid "Connected to {0}"
50055013
msgstr "Connected to {0}"
50065014

5015+
#: src/routes/SyncRoute.tsx
50075016
#: src/routes/SyncRoute.tsx
50085017
msgid "+ Add"
50095018
msgstr "+ Add"
@@ -5021,6 +5030,41 @@ msgstr "Server settings have moved to the{0} <0>Sync page</0>."
50215030
msgid "How to run your own server"
50225031
msgstr "How to run your own server"
50235032

5033+
#~ msgid "Peer ID"
5034+
#~ msgstr "Peer ID"
5035+
5036+
#. placeholder {0}: data.count
5037+
#. placeholder {1}: data.count !== 1 ? 's' : ''
5038+
#: src/routes/SyncRoute.tsx
5039+
msgid "Synced {0} resource{1}"
5040+
msgstr "Synced {0} resource{1}"
5041+
5042+
#~ msgid "Peer sync"
5043+
#~ msgstr "Peer sync"
5044+
5045+
#~ msgid "Paste device ID to sync with"
5046+
#~ msgstr "Paste device ID to sync with"
5047+
5048+
#: src/routes/SyncRoute.tsx
5049+
msgid "Peers"
5050+
msgstr "Peers"
5051+
5052+
#: src/routes/SyncRoute.tsx
5053+
msgid "No peers connected"
5054+
msgstr "No peers connected"
5055+
5056+
#~ msgid "Paste device ID"
5057+
#~ msgstr "Paste device ID"
5058+
5059+
#: src/routes/SyncRoute.tsx
5060+
msgid "Node DID"
5061+
msgstr "Node DID"
5062+
5063+
#. placeholder {0}: irohNodeId.slice(0, 12)
5064+
#: src/routes/SyncRoute.tsx
5065+
msgid "did:ad:node:{0}..."
5066+
msgstr "did:ad:node:{0}..."
5067+
50245068
#: src/routes/SyncRoute.tsx
5025-
msgid "Peer ID"
5026-
msgstr "Peer ID"
5069+
msgid "Paste did:ad:node:..."
5070+
msgstr "Paste did:ad:node:..."

0 commit comments

Comments
 (0)