Skip to content

Commit a9acebb

Browse files
feat: fix initial state bugs, caching, and add UI features
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>
1 parent 80b9d73 commit a9acebb

8 files changed

Lines changed: 192 additions & 26 deletions

File tree

apps/app/src-tauri/Cargo.lock

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

apps/app/src-tauri/Cargo.toml

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -18,6 +18,7 @@ crate-type = ["staticlib", "cdylib", "rlib"]
1818
tauri-build = { version = "2.5.5", features = [] }
1919

2020
[dependencies]
21+
gethostname = "0.4.3"
2122
tauri-plugin-android-fs = { version = "26", features = [
2223
"legacy_storage_permission",
2324
] }
@@ -44,6 +45,8 @@ tokio = { version = "1.50.0", features = ["full"] }
4445
reqwest = { version = "0.13.2", features = ["json"] }
4546
rand = "0.9.2"
4647
dirs = "6.0.0"
48+
notify = "8.2.0"
49+
notify-debouncer-full = "0.6.0"
4750

4851
[profile.dev]
4952
incremental = true # Compile your binary in smaller steps.

apps/app/src-tauri/src/lib.rs

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -456,7 +456,9 @@ pub fn run() {
456456
sync::commands::generate_pairing_pin,
457457
sync::commands::get_discovered_peers,
458458
sync::commands::pair_with_peer,
459-
sync::commands::sync_file
459+
sync::commands::sync_file,
460+
sync::commands::remove_peer,
461+
sync::commands::rename_peer
460462
])
461463
.setup(|app| {
462464
app.manage(sync::commands::AppSyncState {

apps/app/src-tauri/src/sync/commands.rs

Lines changed: 36 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,8 @@ use crate::sync::pairing::{PairRequest, PairResponse};
33
use std::sync::Arc;
44
use tokio::sync::RwLock;
55
use tauri::{Manager, State};
6+
// use notify_debouncer_full::{new_debouncer, notify::*};
7+
// use std::time::Duration;
68

79
pub struct AppSyncState {
810
pub inner: RwLock<Option<Arc<SyncState>>>,
@@ -15,7 +17,9 @@ pub async fn start_sync_service(app_handle: tauri::AppHandle, state: State<'_, A
1517
if sync_state_lock.is_none() {
1618
// Generate a random ID for the session to prevent collisions in MVP
1719
let my_id = crate::sync::pairing::generate_pin().await;
18-
let my_name = format!("Device-{}", my_id);
20+
let hostname = gethostname::gethostname().to_string_lossy().into_owned();
21+
let my_name = if hostname.is_empty() { format!("Device-{}", my_id) } else { hostname };
22+
1923
let port = 8080; // Should find an available port dynamically
2024

2125
let config_dir = app_handle.path().app_config_dir().unwrap_or_else(|_| std::path::PathBuf::from("."));
@@ -47,6 +51,7 @@ pub async fn start_sync_service(app_handle: tauri::AppHandle, state: State<'_, A
4751
eprintln!("Failed to start Android background service: {:?}", e);
4852
}
4953
}
54+
5055
}
5156

5257
Ok(())
@@ -151,6 +156,36 @@ pub async fn sync_file(state: State<'_, AppSyncState>, file_path: String) -> Res
151156
Ok(())
152157
}
153158

159+
#[tauri::command]
160+
pub async fn remove_peer(state: State<'_, AppSyncState>, peer_id: String) -> Result<(), String> {
161+
let sync_state_lock = state.inner.read().await;
162+
if let Some(sync_state) = sync_state_lock.as_ref() {
163+
let mut peers = sync_state.peers.write().await;
164+
peers.remove(&peer_id);
165+
drop(peers);
166+
sync_state.save_peers().await;
167+
Ok(())
168+
} else {
169+
Err("Sync service is not running".into())
170+
}
171+
}
172+
173+
#[tauri::command]
174+
pub async fn rename_peer(state: State<'_, AppSyncState>, peer_id: String, new_name: String) -> Result<(), String> {
175+
let sync_state_lock = state.inner.read().await;
176+
if let Some(sync_state) = sync_state_lock.as_ref() {
177+
let mut peers = sync_state.peers.write().await;
178+
if let Some(peer) = peers.get_mut(&peer_id) {
179+
peer.name = new_name;
180+
}
181+
drop(peers);
182+
sync_state.save_peers().await;
183+
Ok(())
184+
} else {
185+
Err("Sync service is not running".into())
186+
}
187+
}
188+
154189
#[tauri::command]
155190
pub async fn pair_with_peer(state: State<'_, AppSyncState>, peer_id: String, pin: String) -> Result<PairResponse, String> {
156191
let sync_state_lock = state.inner.read().await;

apps/app/src-tauri/src/sync/discovery.rs

Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -26,6 +26,7 @@ pub struct SyncState {
2626
pub crdt_manager: RwLock<CrdtManager>,
2727
pub server_shutdown_tx: Option<tokio::sync::broadcast::Sender<()>>,
2828
pub config_dir: std::path::PathBuf,
29+
// pub fs_watcher: RwLock<Option<notify_debouncer_full::Debouncer<notify::RecommendedWatcher, notify_debouncer_full::FileIdMap>>>,
2930
}
3031

3132
impl SyncState {
@@ -114,6 +115,20 @@ impl SyncState {
114115
}
115116

116117
pub async fn start_discovery(state: Arc<SyncState>) -> Result<(), String> {
118+
// Also proactively probe known peers to make connection fast instead of waiting for mDNS
119+
let peers_clone = state.peers.read().await.clone();
120+
tokio::spawn(async move {
121+
let client = reqwest::Client::builder().timeout(std::time::Duration::from_secs(2)).build().unwrap();
122+
for (_, peer) in peers_clone {
123+
if peer.is_paired {
124+
let url = format!("http://{}:{}/status", peer.ip, peer.port);
125+
if client.get(&url).send().await.is_ok() {
126+
// Found them!
127+
}
128+
}
129+
}
130+
});
131+
117132
let receiver = state.mdns.browse(SERVICE_TYPE).map_err(|e| e.to_string())?;
118133

119134
tokio::spawn(async move {

apps/app/src/lib/components/sync/SyncSettings.svelte

Lines changed: 44 additions & 19 deletions
Original file line numberDiff line numberDiff line change
@@ -41,14 +41,17 @@
4141
}
4242
}
4343
44+
let poll_timer: any = null;
45+
4446
async function refresh_peers() {
4547
if (!is_sync_enabled) return;
4648
try {
4749
discovered_peers = await invoke('get_discovered_peers');
4850
} catch (e) {
4951
console.error('Failed to get peers:', e);
5052
}
51-
setTimeout(refresh_peers, 5000); // Poll every 5s
53+
if (poll_timer) clearTimeout(poll_timer);
54+
poll_timer = setTimeout(refresh_peers, 1000); // Poll every 1s for snappy UI
5255
}
5356
5457
async function pair_with_peer(peer_id: string) {
@@ -105,24 +108,46 @@
105108
{:else}
106109
<ul class="flex flex-col gap-2">
107110
{#each discovered_peers as peer}
108-
<li class="flex items-center justify-between bg-base-200 p-3 rounded-lg">
109-
<span>{peer.name}</span>
110-
{#if peer.is_paired}
111-
<span class="badge badge-success">Paired</span>
112-
{:else}
113-
<div class="flex gap-2">
114-
<input
115-
type="text"
116-
placeholder="PIN"
117-
class="input input-bordered input-sm w-24"
118-
bind:value={input_pin}
119-
maxlength="6"
120-
/>
121-
<button class="btn btn-sm btn-primary" onclick={() => pair_with_peer(peer.id)}>
122-
Connect
123-
</button>
124-
</div>
125-
{/if}
111+
<li class="flex flex-col gap-2 bg-base-200 p-3 rounded-lg">
112+
<div class="flex items-center justify-between w-full">
113+
<span class="font-medium">{peer.name}</span>
114+
{#if peer.is_paired}
115+
<div class="flex items-center gap-2">
116+
<span class="badge badge-success">Paired</span>
117+
<button class="btn btn-xs btn-ghost text-error" onclick={async () => {
118+
await invoke('remove_peer', { peerId: peer.id });
119+
refresh_peers();
120+
}}>Delete</button>
121+
</div>
122+
{:else}
123+
<div class="flex gap-2">
124+
<input
125+
type="text"
126+
placeholder="PIN"
127+
class="input input-bordered input-sm w-24"
128+
bind:value={input_pin}
129+
maxlength="6"
130+
/>
131+
<button class="btn btn-sm btn-primary" onclick={() => pair_with_peer(peer.id)}>
132+
Connect
133+
</button>
134+
</div>
135+
{/if}
136+
</div>
137+
<div class="flex items-center gap-2 mt-1">
138+
<input
139+
type="text"
140+
placeholder="Rename device..."
141+
class="input input-bordered input-xs flex-1 max-w-40 opacity-50 hover:opacity-100 focus:opacity-100 transition-opacity"
142+
onchange={async (e) => {
143+
const newName = e.currentTarget.value;
144+
if (newName) {
145+
await invoke('rename_peer', { peerId: peer.id, newName });
146+
refresh_peers();
147+
}
148+
}}
149+
/>
150+
</div>
126151
</li>
127152
{/each}
128153
</ul>

apps/app/src/lib/file_system/file_tree/operations.ts

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -162,6 +162,12 @@ export async function add_new_note(
162162
sort_nodes(subtree);
163163
const node = subtree.find((n) => n.path === new_file_path);
164164
if (!node) throw new Error('Failed to find the newly created note node.');
165+
166+
// Attempt to trigger initial sync for the newly created file so peers get it immediately.
167+
invoke('sync_file', { filePath: node.path }).catch(e => {
168+
console.warn("Failed to trigger sync for new file: ", e);
169+
});
170+
165171
return node;
166172
} catch (e) {
167173
console.error(e);

0 commit comments

Comments
 (0)