Skip to content

feat: implement seamless local network sync via mDNS and CRDTs#45

Open
Keshav-writes-code wants to merge 18 commits intofeat/local_syncfrom
feat-local-network-sync-6380593602282876537
Open

feat: implement seamless local network sync via mDNS and CRDTs#45
Keshav-writes-code wants to merge 18 commits intofeat/local_syncfrom
feat-local-network-sync-6380593602282876537

Conversation

@Keshav-writes-code
Copy link
Copy Markdown
Owner

Implements the Apple-like seamless local network sync feature for Markdown notes.

Features

  1. Device Discovery & Pairing: Uses mDNS (mdns-sd crate) to broadcast and discover instances of the application on the local network. A simple 6-digit PIN mechanism authenticates and establishes trust between devices.
  2. Conflict Resolution: Integrates the automerge crate to manage files using Conflict-free Replicated Data Types (CRDTs). The app projects CRDT history to the visible .md files and maintains a hidden .sync/ directory for metadata.
  3. Background Sync (Android): Implements an Android ForegroundService to keep the sync process alive in the background.
  4. Local Network API: Runs a lightweight axum HTTP server within the Rust backend to exchange pairing statuses and (eventually) CRDT payloads.
  5. Frontend Settings: Adds a "Sync Devices" modal to the sidebar that allows users to enable/disable the feature, view their PIN, and connect to discovered peers.

PR created automatically by Jules for task 6380593602282876537 started by @Keshav-writes-code

@google-labs-jules
Copy link
Copy Markdown
Contributor

👋 Jules, reporting for duty! I'm here to lend a hand with this pull request.

When you start a review, I'll add a 👀 emoji to each comment to let you know I've read it. I'll focus on feedback directed at me and will do my best to stay out of conversations between you and other bots or reviewers to keep the noise down.

I'll push a commit with your requested changes shortly after. Please note there might be a delay between these steps, but rest assured I'm on the job!

For more direct control, you can switch me to Reactive Mode. When this mode is on, I will only act on comments where you specifically mention me with @jules. You can find this option in the Pull Request section of your global Jules UI settings. You can always switch back!

New to Jules? Learn more at jules.google/docs.


For security, I will only act on instructions from the user who triggered this task.

@Keshav-writes-code Keshav-writes-code force-pushed the feat-local-network-sync-6380593602282876537 branch from be4286f to 5632e5a Compare March 6, 2026 13:10
@Keshav-writes-code Keshav-writes-code force-pushed the feat-local-network-sync-6380593602282876537 branch from 5632e5a to 91caa96 Compare March 6, 2026 13:20
@Keshav-writes-code Keshav-writes-code force-pushed the feat-local-network-sync-6380593602282876537 branch from 91caa96 to fca0a60 Compare March 6, 2026 13:34
@Keshav-writes-code Keshav-writes-code force-pushed the feat-local-network-sync-6380593602282876537 branch from 4ed5f15 to fca0a60 Compare March 6, 2026 14:33
Keshav-writes-code and others added 11 commits March 8, 2026 23:18
This commit lays the groundwork for seamless local network synchronization
across devices. It introduces:
- Backend: Rust implementation using `mdns-sd` for peer discovery, `axum` for
  a local HTTP server to handle pairing/status requests, and `automerge`
  for CRDT-based file merging (Last-Write-Wins handling via automerge docs).
- Frontend: A new Svelte component `SyncSettings.svelte` to toggle
  broadcasting, generate pairing PINs, discover peers, and establish
  secure links. The UI is integrated into the BottomSidebar.
- Android: Added background `SyncForegroundService` to Android manifest
  and Kotlin source to allow background network discovery when the app is
  minimized.

Co-authored-by: Keshav-writes-code <95571677+Keshav-writes-code@users.noreply.github.com>
This commit lays the groundwork for seamless local network synchronization
across devices. It introduces:
- Backend: Rust implementation using `mdns-sd` for peer discovery, `axum` for
  a local HTTP server to handle pairing/status requests, and `automerge`
  for CRDT-based file merging (Last-Write-Wins handling via automerge docs).
- Frontend: A new Svelte component `SyncSettings.svelte` to toggle
  broadcasting, generate pairing PINs, discover peers, and establish
  secure links. The UI is integrated into the BottomSidebar.
- Android: Added background `SyncForegroundService` to Android manifest
  and Kotlin source to allow background network discovery when the app is
  minimized.

Fixed minor unused import warnings discovered during code review.

Co-authored-by: Keshav-writes-code <95571677+Keshav-writes-code@users.noreply.github.com>
Previously, `mDNS` packets were erroneously broadcasting `0.0.0.0` as the
service IP, causing other devices to discover the peer but fail to
establish a network route.

- Resolved the local network IP properly by testing a mock UDP connection
  to an external IP before initializing the `ServiceInfo`.
- Fixed the visual output of discovered device names in the frontend by
  stripping the mDNS `.cherit._tcp.local` suffix in the discovery loop.

Co-authored-by: Keshav-writes-code <95571677+Keshav-writes-code@users.noreply.github.com>
Resolves an issue where submitting a pairing PIN occasionally failed with
"Invalid PIN or pairing not active" even when the PIN was correct.

This usually occurs if the initiating device successfully sends the
`POST /pair` HTTP request faster than the local receiver processes the
mDNS discovery packet, causing the peer to be missing from the
receiver's peer list.

- The Axum handler now blindly accepts the matching PIN even if the
  peer is not yet officially recorded in the `peers` map.
- Automatically initializes a stub `PeerInfo` entry if one did not exist,
  allowing the next broadcast from that peer to update its IP, Port,
  and Name natively.

Also successfully rebased and integrated the remote changes as
requested.

Co-authored-by: Keshav-writes-code <95571677+Keshav-writes-code@users.noreply.github.com>
Implements the actual file synchronization trigger mechanism.

- Extends `window_listeners` to invoke a new `sync_file` command
  whenever a changed file is saved to disk (e.g. when the window blurs).
- Adds the `/sync` API endpoint in the `axum` server to receive incoming
  changes from peers.
- Added a `sync_file` Tauri command to loop through all paired peers and
  POST file updates to them.

Note: CRDT payload generation is currently stubbed out (pending full
integration with Automerge in a future commit).

Co-authored-by: Keshav-writes-code <95571677+Keshav-writes-code@users.noreply.github.com>
Completes the MVP data sync exchange pipeline between peers.

Previously, the `POST /sync` request from the frontend saved a dummy
stub payload, and the receiving end logged it without saving.

- Modified `sync_file` command to actually read the file contents from
  the file system disk and push that byte slice over HTTP.
- Modified `/sync` route handler on the receiving peer to save incoming
  modifications into `~/.cherit-sync-inbox/<filename>` directory (via `dirs`
  crate).

Note: Saving directly to an inbox provides immediate visibility that the
sync succeeds across the network without accidentally overwriting the root
workspace until full CRDT state merging is connected.

Co-authored-by: Keshav-writes-code <95571677+Keshav-writes-code@users.noreply.github.com>
Previously, network syncing was only firing when the app lost focus (the
`window_blur` event) due to it being hooked only in `window_listeners`.

This commit adds a direct `sync_file` invocation into the main
`text_editor` component's `write_to_file` callback, which is fired far
more frequently as the user types, ensuring the peer receives updates in
near real-time.

Co-authored-by: Keshav-writes-code <95571677+Keshav-writes-code@users.noreply.github.com>
Replaces the dummy file transmission payload with actual CRDT operations.

- Automerge Text CRDT Integration: Switched from using a naive `Last-Write-Wins`
  scalar string property to an Automerge `ObjType::Text` property
  manipulated via `splice_text`. This ensures character-level conflict
  resolution logic correctly preserves history without blowing up memory.
- Live Sync Updating: When an incoming payload merges successfully,
  the backend emits a `sync-file-updated` Tauri event. The
  `text_editor` Svelte component intercepts this event, reads the new
  file from disk, and dispatches the change immediately into the
  CodeMirror editor instance.
- Graceful Shutdown: Added a `broadcast` channel to gracefully signal
  and kill the background `axum` tokio task when stopping the sync service.
- Persistent Peers: Serialized the internal `peers` map to a
  `peers.json` inside the platform-native app data config folder.
- Path Serialization: Fixed a bug where Windows systems would send
  escaped backward-slash paths (`\\`) that macOS/Linux peers could not parse.
- Android Background Hook: Implemented Android mobile plugin invocation
  to spawn the background service notification to keep network
  sockets alive.

Co-authored-by: Keshav-writes-code <95571677+Keshav-writes-code@users.noreply.github.com>
When editing a completely new or previously un-synced file, the file
might exist on disk but lack a corresponding Automerge (.am) tracking
file locally. If a sync event was triggered for it, the content was not
properly parsed into an `ObjType::Text` before generating the sync payload.

- Updated `load_or_create_doc` to inject the existing plaintext content
  into the newly minted CRDT Text object using `splice_text` if the
  file is found on disk.
- Updated the backend `/sync` route to initialize the parent directories
  and create the plaintext file automatically if it receives a payload
  for a file that doesn't yet exist in the receiver's workspace.

Co-authored-by: Keshav-writes-code <95571677+Keshav-writes-code@users.noreply.github.com>
@Keshav-writes-code Keshav-writes-code force-pushed the feat-local-network-sync-6380593602282876537 branch from 8488795 to 98044b3 Compare March 8, 2026 18:09
google-labs-jules bot and others added 7 commits March 8, 2026 23:39
Addresses the issue where newly created files, or existing files that
were opened for the first time while sync was running, did not
propagate their initial state to the peer correctly.

- The `update_doc_from_file` and `load_or_create_doc` routines now
  detect and load the initial plaintext directly into the Automerge
  `ObjType::Text` before emitting the first sync payload.
- Fixed the receiver side to automatically construct parent directories
  and a dummy file stub when receiving a payload for a completely
  unknown path before attempting to apply the CRDT data.

Co-authored-by: Keshav-writes-code <95571677+Keshav-writes-code@users.noreply.github.com>
This commit addresses several critical issues with the syncing process
and improves the user experience.

- Fixes issue where syncing a completely new or pre-existing but
  un-tracked file failed to send its contents. `crdt_manager` now
  reliably provisions an `ObjType::Text` and uses `splice_text` to seed
  its value if a plaintext file exists before generating payloads.
- Fixes issue where offline changes were not synced. Instead of relying
  solely on the active Svelte editor to push updates, the backend now
  watches the workspace using `notify-debouncer-full` and
  asynchronously syncs external changes seamlessly.
- Device discovery feels significantly faster. Cached peers are proactively
  probed via HTTP instead of waiting up to 3 seconds for the next mDNS
  multicast cycle.
- Fixed device naming. Hostnames are properly derived using `gethostname`
  instead of `Device-PIN`.
- Svelte UI now allows renaming and deleting known peers to provide
  control over persistent trust data.

Co-authored-by: Keshav-writes-code <95571677+Keshav-writes-code@users.noreply.github.com>
Fixes a CI build failure on Android targets: `error[E0599]: no method named run_mobile_plugin found for struct AppHandle`.

- Removed the direct `run_mobile_plugin` call on `AppHandle` in `commands.rs`.
- This method was historically part of internal Tauri mobile traits but
  is not exposed cleanly on the main AppHandle without specific plugin
  bindings in Tauri v2.
- Replaced with a log statement for the MVP. A full native intent invocation
  would require pulling in the `jni` crate and querying the JNI env.

Co-authored-by: Keshav-writes-code <95571677+Keshav-writes-code@users.noreply.github.com>
Fixes a CI build failure on Android targets: `error[E0599]: no method named run_mobile_plugin found for struct AppHandle`.

- Removed the direct `run_mobile_plugin` call on `AppHandle` in `commands.rs`.
- This method was historically part of internal Tauri mobile traits but
  is not exposed cleanly on the main AppHandle without specific plugin
  bindings in Tauri v2.
- Replaced with a log statement for the MVP. A full native intent invocation
  would require pulling in the `jni` crate and querying the JNI env.

Co-authored-by: Keshav-writes-code <95571677+Keshav-writes-code@users.noreply.github.com>
Android 10+ (API 29+) enforces Scoped Storage via the Storage Access Framework (SAF),
meaning that standard `std::fs` operations cannot write to user-selected virtual URIs,
causing a `Read-only file system (os error 30)` panic when the backend tried to initialize
the hidden `.sync` directory inside the workspace.

- Relocated the Automerge `.am` metadata files into the Tauri application's native,
  read-write enabled config directory (`app_config_dir()`).
- Added MD5 hashing of the workspace path to ensure different workspace trees
  get their own isolated sub-folder of sync states to prevent cross-workspace collisions.

Co-authored-by: Keshav-writes-code <95571677+Keshav-writes-code@users.noreply.github.com>
Addresses an issue in `crdt.rs` where the `automerge` crate was
panicking or improperly updating the text object when trying to replace
the existing text with a new string.

- Split the text replacement into two separate `splice_text` operations:
  one to completely delete the existing length, and a subsequent one
  to insert the new content.
- This ensures that Automerge robustly records the text replacements
  when an untracked file initializes or an external change causes a sync
  event without throwing out of bounds errors.

Co-authored-by: Keshav-writes-code <95571677+Keshav-writes-code@users.noreply.github.com>
The previous branch pushes relocated Svelte global states, causing
the local `SyncSettings.svelte` compilation to fail during Vite build
or checks.

- Updated the import path in `SyncSettings.svelte` from
  `@/lib/global_states/index.svelte` to `@/lib/states/global/index.svelte`
  to match the user's latest architectural refactoring.

Co-authored-by: Keshav-writes-code <95571677+Keshav-writes-code@users.noreply.github.com>
@Keshav-writes-code Keshav-writes-code force-pushed the feat/local_sync branch 10 times, most recently from dabd928 to 92d68d2 Compare March 24, 2026 09:54
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant