Skip to content

Commit 10213d8

Browse files
committed
Bugfix: Fix stale dir sizes on rename
Renames produced two writer messages (`DeleteEntryById` + `UpsertEntryV2`) but `emit_dir_updated` fired immediately after enqueuing them, before the writer committed both. The UI read intermediate `dir_stats` (delete applied, insert not yet), showing wrong sizes. - Add `WriteMessage::EmitDirUpdated(Vec<String>)` — the writer emits the notification after processing all prior messages in the batch, guaranteeing the UI only sees the final committed state - Replace direct `reconciler::emit_dir_updated()` calls in `run_live_event_loop` with `writer.send(EmitDirUpdated(...))` (both the flush-tick and channel-closed paths) - No flush roundtrip, no event loop blocking — just FIFO message ordering
1 parent b302d0e commit 10213d8

3 files changed

Lines changed: 22 additions & 3 deletions

File tree

apps/desktop/src-tauri/src/indexing/CLAUDE.md

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -52,7 +52,7 @@ Live mode:
5252
|-- macOS: FSEvents -> reconciler (resolve_path -> entry IDs) -> UpsertEntryV2/DeleteEntryById/DeleteSubtreeById -> writer -> SQLite
5353
|-- Linux: inotify (via notify crate) -> same pipeline
5454
|-- Reconciler and event loops hold a read connection for integer-keyed path resolution
55-
|-- Events deduplicated by normalized path in HashMap, flushed every 1s via index-dir-updated event
55+
|-- Events deduplicated by normalized path in HashMap, flushed every 1s, writer flush before emit ensures atomic dir_stats
5656
|
5757
Enrichment (every get_file_range call):
5858
|-- enrich_entries_with_index() -> resolve parent dir → id (one tree walk)
@@ -153,6 +153,8 @@ Key test files are alongside each module (test functions within `#[cfg(test)]` b
153153

154154
**INSERT OR REPLACE on a populated DB is catastrophically slow**: The `platform_case` collation (NFD + case fold on macOS) runs for every B-tree comparison during unique index lookups. On an empty DB a full scan takes ~2.5 min; on a populated DB with 5.5M entries the same scan takes ~30 min because each `INSERT OR REPLACE` triggers ~20 collation calls to traverse the B-tree. `start_scan()` truncates `entries` and `dir_stats` via `TruncateData` + `flush_blocking()` before every scan to avoid this. Additionally, without truncation, old rows accumulate as orphaned subtrees (3-4x DB bloat per scan cycle) because `INSERT OR REPLACE` only deduplicates at the root level.
155155

156+
**Live event notifications are enqueued as writer messages**: `emit_dir_updated` is sent via `WriteMessage::EmitDirUpdated(paths)` instead of being called directly from the event loop. Since the writer processes messages in order, the notification fires only after all prior writes (deletes, upserts, deltas) from the same batch are committed. Without this, multi-message operations (e.g. rename = `DeleteEntryById` + `UpsertEntryV2`) show intermediate `dir_stats` to the UI (e.g. parent dir size drops by the renamed file's size). The replay path uses a different approach (explicit flush + batched emit) because it accumulates paths over thousands of events.
157+
156158
**Cold-start replay enters live mode immediately after flush**: `run_replay_event_loop` (in `event_loop.rs`) doesn't emit `index-dir-updated` during Phase 1 (replay). It collects affected paths, flushes the writer (ensuring all writes are committed), emits a single batched notification, and enters live mode right away (~100ms from startup). Post-replay verification (`verify_affected_dirs`) runs in a background task (`run_background_verification`) concurrently with live events. This is safe because the writer serializes all writes. Any corrections found by verification are emitted as a separate `index-dir-updated` batch.
157159

158160
**FSEvents `item_removed` must be verified against disk**: macOS FSEvents can deliver `item_removed` for paths that still exist (atomic file swaps by editors/git, coalesced events with OR'd flags, `merge_fs_events` discarding `item_created` when `item_removed` is present). `handle_removal()` stats the path before deleting: if the file exists, it delegates to `handle_creation_or_modification()` (upsert) instead. Without this, false removals progressively delete live entries from the DB — especially damaging for directories since `DeleteSubtreeById` is recursive. `handle_creation_or_modification()` already has the inverse pattern: if stat fails, it deletes.

apps/desktop/src-tauri/src/indexing/event_loop.rs

Lines changed: 10 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -188,7 +188,9 @@ pub(super) async fn run_live_event_loop(
188188
&writer, &mut pending_paths,
189189
);
190190
if !pending_paths.is_empty() {
191-
reconciler::emit_dir_updated(&app, pending_paths.drain().collect());
191+
let _ = writer.send(WriteMessage::EmitDirUpdated(
192+
pending_paths.drain().collect(),
193+
));
192194
}
193195
break;
194196
}
@@ -221,7 +223,13 @@ pub(super) async fn run_live_event_loop(
221223
&writer, &mut pending_paths,
222224
);
223225
if !pending_paths.is_empty() {
224-
reconciler::emit_dir_updated(&app, pending_paths.drain().collect());
226+
// Enqueue the notification as a writer message so it fires
227+
// after all prior writes (deletes, upserts, deltas) commit.
228+
// Without this, multi-message operations (e.g. rename =
229+
// delete + insert) show intermediate dir_stats to the UI.
230+
let _ = writer.send(WriteMessage::EmitDirUpdated(
231+
pending_paths.drain().collect(),
232+
));
225233
}
226234
}
227235
}

apps/desktop/src-tauri/src/indexing/writer.rs

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -115,6 +115,10 @@ pub enum WriteMessage {
115115
/// Periodic housekeeping: reclaim free pages from deletes/rescans.
116116
/// Sent by a background timer, not counted in WriterStats.
117117
IncrementalVacuum,
118+
/// Emit `index-dir-updated` for the given paths. Enqueued after a batch
119+
/// of writes so the UI notification fires only after all prior messages
120+
/// (deletes, upserts, deltas) are committed.
121+
EmitDirUpdated(Vec<String>),
118122
/// Shut down the writer thread.
119123
Shutdown,
120124
}
@@ -782,6 +786,11 @@ fn process_message(
782786
Err(e) => log::warn!("Writer: freelist_count query failed: {e}"),
783787
}
784788
}
789+
WriteMessage::EmitDirUpdated(paths) => {
790+
if let Some(app) = app_handle {
791+
crate::indexing::reconciler::emit_dir_updated(app, paths);
792+
}
793+
}
785794
WriteMessage::Shutdown => return true,
786795
}
787796
false

0 commit comments

Comments
 (0)