Skip to content

Commit fbacdbd

Browse files
committed
Indexing: generalize the single-volume indexer into a per-volume registry (M1, no behavior change)
Replaces the one hardwired `INDEXING: Mutex<IndexPhase>` global with `INDEX_REGISTRY: Mutex<HashMap<VolumeId, IndexInstance>>`, where each `IndexInstance` bundles a volume's `{phase, read_pool, pending_sizes}`. This is the structural foundation for indexing SMB shares and MTP devices (later milestones); M1 lands the refactor in isolation with the local disk (`root`) still the only registered volume, so externally observable behavior is byte-identical. - **Per-volume lifecycle, keyed independently.** Every invariant the single-volume design held now holds per volume id: single-writer-per-DB, lock-first reservation (`(absent) -> Initializing` claimed atomically per volume before building the heavy `IndexManager`), drop-the-guard-before-`mgr.shutdown()`'s 5 s drain, and reads via `ReadPool` never under the lifecycle lock. Two volumes can't corrupt each other; two starts for the same volume still can't race (the `UNIQUE(parent_id, name_folded)` safety net is unchanged). - **Disabled is the absence of a key** — there's no `IndexPhase::Disabled` variant; stop/clear remove the instance after the drain. - **Reads route by volume id.** `get_read_pool_for(vid)` / `get_pending_sizes_for(vid)` route root to the `READ_POOL`/`PENDING_SIZES` globals (the same `Arc` the root instance holds, so they can't drift — kept as globals because search reads them hot and the tests install them directly) and non-root to the registry instance. Enrichment, verification, and IPC dir-stats thread the listing's `volume_id` where they have it. - **The `should_exclude` early-return in `enrichment.rs` is reframed to "skip if no index is registered for this volume"** (`get_read_pool_for` returns `None`). In M1 only `root` has a pool, so every non-root listing still early-returns exactly as before — same set of listings skipped, new reason. For `root` the local-path `should_exclude` check is also retained so a root-volume listing navigated under `/Volumes`, `/mnt`, or `/proc` still skips (no "Parent path not found" log-spam on network mounts). - **IPC surface unchanged.** `#[tauri::command]` signatures and `bindings.ts` are byte-identical: path-based commands resolve the volume internally (M1: always `root`, with a `TODO(M2/M4)` for `/Volumes/<share>` and `mtp-*` virtual-path mapping); the scan/status/clear commands pass `ROOT_VOLUME_ID`. Tests: added `state.rs::tests` for the registry (skip-vs-route gate tracks registration; two volume ids reserve/release independently and route to distinct pools). The existing `integration_tests.rs` enrichment/`ReadPool`/dir-stats/FDA regression tests pass with their assertions unchanged; the `IndexPhase` lifecycle harness tests were ported mechanically to the registry (same assertion intent — "phase Disabled" becomes "instance absent"). Full `pnpm check rust` green (clippy clean, `bindings-fresh` confirms the IPC surface, 2863 + 38 tests on macOS and the Linux Docker lane). Left for later milestones (TODOs in-code): the memory watchdog still stops only `root` (M2 makes it a global budget), and `volume_id_for_local_path` maps everything to `root` until SMB/MTP backends register (M2/M4).
1 parent 7a08831 commit fbacdbd

18 files changed

Lines changed: 882 additions & 380 deletions

File tree

apps/desktop/src-tauri/src/commands/file_system/write_ops.rs

Lines changed: 7 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -32,7 +32,7 @@ pub async fn create_directory(
3232
// Synthetic diff only works for volumes backed by the local filesystem.
3333
// Protocol-only volumes (MTP) handle UI updates through their own event systems.
3434
if should_emit_synthetic_diff(volume_id.as_deref()) {
35-
emit_synthetic_entry_diff(&new_path, &PathBuf::from(&expanded_path));
35+
emit_synthetic_entry_diff(volume_id.as_deref(), &new_path, &PathBuf::from(&expanded_path));
3636
}
3737
Ok(new_path.to_string_lossy().to_string())
3838
}
@@ -43,7 +43,7 @@ pub async fn create_file(volume_id: Option<String>, parent_path: String, name: S
4343
let (new_path, expanded_path) = create_file_core(volume_id.clone(), &parent_path, &name).await?;
4444

4545
if should_emit_synthetic_diff(volume_id.as_deref()) {
46-
emit_synthetic_entry_diff(&new_path, &PathBuf::from(&expanded_path));
46+
emit_synthetic_entry_diff(volume_id.as_deref(), &new_path, &PathBuf::from(&expanded_path));
4747
}
4848
Ok(new_path.to_string_lossy().to_string())
4949
}
@@ -364,7 +364,7 @@ fn should_emit_synthetic_diff(volume_id: Option<&str>) -> bool {
364364
///
365365
/// Best-effort: if any step fails (stat, cache lookup, etc.) we log a warning
366366
/// and return. The watcher will pick up the change later.
367-
fn emit_synthetic_entry_diff(entry_path: &Path, parent_path: &Path) {
367+
fn emit_synthetic_entry_diff(volume_id: Option<&str>, entry_path: &Path, parent_path: &Path) {
368368
use crate::file_system::listing::diff_emitter::enqueue_diff;
369369
use crate::file_system::listing::reading::get_single_entry;
370370
use crate::file_system::listing::{find_listings_for_path, insert_entry_sorted};
@@ -379,8 +379,10 @@ fn emit_synthetic_entry_diff(entry_path: &Path, parent_path: &Path) {
379379
}
380380
};
381381

382-
// 2. Enrich with index data
383-
crate::indexing::enrich_entries_with_index(std::slice::from_mut(&mut entry));
382+
// 2. Enrich with index data. `None` means the local filesystem (`root`); this
383+
// path only runs for local-FS volumes (`should_emit_synthetic_diff`).
384+
let volume_id = volume_id.unwrap_or(crate::indexing::ROOT_VOLUME_ID);
385+
crate::indexing::enrich_entries_with_index_on_volume(volume_id, std::slice::from_mut(&mut entry));
384386

385387
// 3. Find affected listings
386388
let listings = find_listings_for_path(parent_path);

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

Lines changed: 15 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -4,14 +4,19 @@
44
55
use tauri::AppHandle;
66

7-
use crate::indexing::{self, IndexDebugStatusResponse, IndexStatusResponse, store::DirStats};
7+
use crate::indexing::{self, IndexDebugStatusResponse, IndexStatusResponse, ROOT_VOLUME_ID, store::DirStats};
8+
9+
// IPC stays path-based and single-volume in M1: the index-status, scan, and
10+
// clear commands all act on the local-disk `root` index. The backend resolves
11+
// the volume internally (here, the constant `root`), so the frontend and
12+
// `bindings.ts` are unchanged. M2+ will widen these to carry a volume.
813

914
#[tauri::command]
1015
#[specta::specta]
1116
pub async fn start_drive_index(app: AppHandle) -> Result<(), String> {
12-
if indexing::is_active() {
17+
if indexing::is_active(ROOT_VOLUME_ID) {
1318
// Already running: force a fresh full scan (for example, from the debug "Start scan" button)
14-
indexing::force_scan()
19+
indexing::force_scan(ROOT_VOLUME_ID)
1520
} else {
1621
indexing::start_indexing(&app)
1722
}
@@ -20,13 +25,13 @@ pub async fn start_drive_index(app: AppHandle) -> Result<(), String> {
2025
#[tauri::command]
2126
#[specta::specta]
2227
pub async fn stop_drive_index() -> Result<(), String> {
23-
indexing::stop_scan()
28+
indexing::stop_scan(ROOT_VOLUME_ID)
2429
}
2530

2631
#[tauri::command]
2732
#[specta::specta]
2833
pub async fn get_index_status() -> Result<IndexStatusResponse, String> {
29-
indexing::get_status()
34+
indexing::get_status(ROOT_VOLUME_ID)
3035
}
3136

3237
#[tauri::command]
@@ -44,26 +49,26 @@ pub async fn get_dir_stats_batch(paths: Vec<String>) -> Result<Vec<Option<DirSta
4449
#[tauri::command]
4550
#[specta::specta]
4651
pub async fn clear_drive_index() -> Result<(), String> {
47-
indexing::clear_index()
52+
indexing::clear_index(ROOT_VOLUME_ID)
4853
}
4954

5055
/// Extended debug status for the debug window (dev only).
5156
#[tauri::command]
5257
#[specta::specta]
5358
pub async fn get_index_debug_status() -> Result<IndexDebugStatusResponse, String> {
54-
indexing::get_debug_status()
59+
indexing::get_debug_status(ROOT_VOLUME_ID)
5560
}
5661

5762
/// Toggle drive indexing on/off based on the user's setting.
5863
#[tauri::command]
5964
#[specta::specta]
6065
pub async fn set_indexing_enabled(app: AppHandle, enabled: bool) -> Result<(), String> {
6166
if enabled {
62-
if !indexing::is_active() {
67+
if !indexing::is_active(ROOT_VOLUME_ID) {
6368
indexing::start_indexing(&app)?;
6469
}
6570
} else {
66-
indexing::stop_indexing()?;
71+
indexing::stop_indexing(ROOT_VOLUME_ID)?;
6772
}
6873
Ok(())
6974
}
@@ -105,7 +110,7 @@ pub async fn start_indexing_after_fda_decision(app: AppHandle) -> Result<(), Str
105110
#[cfg(any(target_os = "macos", target_os = "linux"))]
106111
crate::mtp::start_mtp_watcher(&app);
107112

108-
if indexing::is_active() {
113+
if indexing::is_active(ROOT_VOLUME_ID) {
109114
return Ok(());
110115
}
111116
indexing::start_indexing(&app)

apps/desktop/src-tauri/src/file_system/listing/caching.rs

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -428,7 +428,7 @@ pub fn notify_directory_changed(volume_id: &str, parent_path: &Path, change: Dir
428428
match change {
429429
DirectoryChange::Added(entry) => {
430430
let mut entry = entry;
431-
crate::indexing::enrich_entries_with_index(std::slice::from_mut(&mut entry));
431+
crate::indexing::enrich_entries_with_index_on_volume(volume_id, std::slice::from_mut(&mut entry));
432432
for (listing_id, ..) in &listings {
433433
notify_added(listing_id, entry.clone());
434434
}
@@ -441,14 +441,14 @@ pub fn notify_directory_changed(volume_id: &str, parent_path: &Path, change: Dir
441441
}
442442
DirectoryChange::Modified(entry) => {
443443
let mut entry = entry;
444-
crate::indexing::enrich_entries_with_index(std::slice::from_mut(&mut entry));
444+
crate::indexing::enrich_entries_with_index_on_volume(volume_id, std::slice::from_mut(&mut entry));
445445
for (listing_id, ..) in &listings {
446446
notify_modified(listing_id, entry.clone());
447447
}
448448
}
449449
DirectoryChange::Renamed { old_name, new_entry } => {
450450
let mut new_entry = new_entry;
451-
crate::indexing::enrich_entries_with_index(std::slice::from_mut(&mut new_entry));
451+
crate::indexing::enrich_entries_with_index_on_volume(volume_id, std::slice::from_mut(&mut new_entry));
452452
let old_path = parent_path.join(&old_name);
453453
for (listing_id, ..) in &listings {
454454
notify_removed(listing_id, &old_path);
@@ -600,7 +600,7 @@ async fn notify_full_refresh(
600600
}
601601
};
602602

603-
crate::indexing::enrich_entries_with_index(&mut new_entries);
603+
crate::indexing::enrich_entries_with_index_on_volume(&volume_id, &mut new_entries);
604604

605605
for (listing_id, sort_by, sort_order, dir_sort_mode) in &listings {
606606
// Re-sort to match this listing's sort params

apps/desktop/src-tauri/src/file_system/listing/mod.rs

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -29,8 +29,8 @@ pub use operations::{get_files_at_indices, get_paths_at_indices};
2929

3030
// Internal re-exports for file_system module internals (pub(crate) for crate-internal use)
3131
pub(crate) use caching::{
32-
ModifyResult, find_listings_for_path, get_listing_path, get_listing_volume_id_and_path, has_entry,
33-
increment_sequence, insert_entry_sorted, remove_entry_by_path, start_orphan_listing_reaper, update_entry_sorted,
32+
ModifyResult, find_listings_for_path, get_listing_volume_id_and_path, has_entry, increment_sequence,
33+
insert_entry_sorted, remove_entry_by_path, start_orphan_listing_reaper, update_entry_sorted,
3434
};
3535
// Notification API for volume mutations
3636
#[cfg(any(target_os = "macos", target_os = "linux"))]

apps/desktop/src-tauri/src/file_system/listing/operations.rs

Lines changed: 7 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -77,8 +77,8 @@ pub async fn list_directory_start_with_volume(
7777
// Enrich directory entries with index data (recursive_size etc.) before sorting,
7878
// so that sort-by-size works correctly for directories.
7979
let mut all_entries = all_entries;
80-
crate::indexing::enrich_entries_with_index(&mut all_entries);
81-
crate::indexing::trigger_verification(&path.to_string_lossy());
80+
crate::indexing::enrich_entries_with_index_on_volume(volume_id, &mut all_entries);
81+
crate::indexing::trigger_verification(volume_id, &path.to_string_lossy());
8282

8383
// Sort the entries
8484
sort_entries(&mut all_entries, sort_by, sort_order, dir_sort_mode);
@@ -369,7 +369,8 @@ pub fn resort_listing(
369369
};
370370

371371
// Refresh index data before re-sorting (cache entries may not have fresh sizes)
372-
crate::indexing::enrich_entries_with_index(&mut listing.entries);
372+
let volume_id = listing.volume_id.clone();
373+
crate::indexing::enrich_entries_with_index_on_volume(&volume_id, &mut listing.entries);
373374

374375
// Re-sort the entries
375376
sort_entries(&mut listing.entries, sort_by, sort_order, dir_sort_mode);
@@ -421,7 +422,7 @@ pub(crate) fn update_listing_entries(listing_id: &str, entries: Vec<FileEntry>)
421422
{
422423
listing.touch();
423424
let mut entries = entries;
424-
crate::indexing::enrich_entries_with_index(&mut entries);
425+
crate::indexing::enrich_entries_with_index_on_volume(&listing.volume_id, &mut entries);
425426
sort_entries(
426427
&mut entries,
427428
listing.sort_by,
@@ -585,7 +586,8 @@ pub fn get_listing_stats(
585586
pub fn refresh_listing_index_sizes(listing_id: &str) -> Result<(), String> {
586587
let mut cache = LISTING_CACHE.write().map_err(|_| "Failed to acquire cache lock")?;
587588
if let Some(listing) = cache.get_mut(listing_id) {
588-
crate::indexing::enrich_entries_with_index(&mut listing.entries);
589+
let volume_id = listing.volume_id.clone();
590+
crate::indexing::enrich_entries_with_index_on_volume(&volume_id, &mut listing.entries);
589591
}
590592
Ok(())
591593
}

apps/desktop/src-tauri/src/file_system/listing/streaming.rs

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -473,9 +473,9 @@ pub(crate) async fn read_directory_with_progress(
473473
// Enrich directory entries with index data (recursive_size etc.) before sorting,
474474
// so that sort-by-size works correctly for directories.
475475
let enrich_start = std::time::Instant::now();
476-
crate::indexing::enrich_entries_with_index(&mut entries);
476+
crate::indexing::enrich_entries_with_index_on_volume(volume_id, &mut entries);
477477
let enrich_ms = enrich_start.elapsed().as_millis();
478-
crate::indexing::trigger_verification(&path.to_string_lossy());
478+
crate::indexing::trigger_verification(volume_id, &path.to_string_lossy());
479479

480480
// Sort entries
481481
benchmark::log_event("sort START");

apps/desktop/src-tauri/src/file_system/watcher.rs

Lines changed: 7 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -19,8 +19,8 @@ use tauri::AppHandle;
1919
use tauri_specta::Event as _;
2020

2121
use crate::file_system::listing::{
22-
FileEntry, ModifyResult, get_listing_entries, get_listing_path, get_single_entry, has_entry, insert_entry_sorted,
23-
list_directory_core, remove_entry_by_path, update_entry_sorted, update_listing_entries,
22+
FileEntry, ModifyResult, get_listing_entries, get_listing_volume_id_and_path, get_single_entry, has_entry,
23+
insert_entry_sorted, list_directory_core, remove_entry_by_path, update_entry_sorted, update_listing_entries,
2424
};
2525

2626
/// Default debounce duration in milliseconds (used if not configured)
@@ -204,8 +204,8 @@ fn handle_directory_change_incremental(listing_id: &str, events: Vec<DebouncedEv
204204
return;
205205
}
206206

207-
// Get watched directory path from the cache (without cloning all entries)
208-
let Some(dir_path) = get_listing_path(listing_id) else {
207+
// Get watched directory path + volume from the cache (without cloning all entries)
208+
let Some((volume_id, dir_path)) = get_listing_volume_id_and_path(listing_id) else {
209209
return;
210210
};
211211

@@ -256,10 +256,10 @@ fn handle_directory_change_incremental(listing_id: &str, events: Vec<DebouncedEv
256256

257257
// Enrich new/modified entries with index data
258258
for entry in &mut adds {
259-
crate::indexing::enrich_entries_with_index(std::slice::from_mut(entry));
259+
crate::indexing::enrich_entries_with_index_on_volume(&volume_id, std::slice::from_mut(entry));
260260
}
261261
for entry in &mut modifies {
262-
crate::indexing::enrich_entries_with_index(std::slice::from_mut(entry));
262+
crate::indexing::enrich_entries_with_index_on_volume(&volume_id, std::slice::from_mut(entry));
263263
}
264264

265265
// Apply changes: removes first (indices refer to OLD listing), then adds, then modifies.
@@ -443,7 +443,7 @@ pub async fn handle_directory_change(listing_id: &str) {
443443
if let Ok(cache) = LISTING_CACHE.read()
444444
&& let Some(listing) = cache.get(listing_id)
445445
{
446-
crate::indexing::enrich_entries_with_index(&mut new_entries);
446+
crate::indexing::enrich_entries_with_index_on_volume(&listing.volume_id, &mut new_entries);
447447
sort_entries(
448448
&mut new_entries,
449449
listing.sort_by,

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

Lines changed: 32 additions & 25 deletions
Original file line numberDiff line numberDiff line change
@@ -5,7 +5,8 @@ size aggregates so listings can show directory sizes.
55

66
## Module map
77

8-
- **Lifecycle / state**: `state.rs` (`IndexPhase` machine, `INDEXING` mutex, public API), `manager.rs` (coordinator).
8+
- **Lifecycle / state**: `state.rs` (`IndexPhase` machine, the per-volume `INDEX_REGISTRY`, public API), `manager.rs`
9+
(per-volume coordinator).
910
- **Write path**: `writer/` (single writer thread + write connection; `mod.rs` = protocol + loop, `entries.rs` /
1011
`delta.rs` / `aggregation.rs` / `maintenance.rs` = handlers), `scanner.rs` (jwalk walk), `aggregator.rs` (dir-stats),
1112
`reconciler.rs` + `event_loop.rs` (replay + live FSEvents).
@@ -16,39 +17,45 @@ size aggregates so listings can show directory sizes.
1617

1718
## Must-knows (invariants and guardrails)
1819

19-
- **Single-writer thread.** All writes go through one writer thread via a bounded `sync_channel` (backpressure on full).
20-
Reads use a `ReadPool` of thread-local WAL connections, NOT the `INDEXING` mutex (which guards only lifecycle
21-
transitions); don't move read paths (enrichment, verification, IPC dir-stats) back under it. `stop_indexing` and
22-
`clear_index` must drop the guard BEFORE `mgr.shutdown()`'s 5 s drain.
20+
- **Per-volume registry.** `INDEX_REGISTRY: Mutex<HashMap<VolumeId, IndexInstance{phase, read_pool, pending_sizes}>>` is
21+
the authority for which volumes are indexed; an absent key = disabled (no `Disabled` phase). Every invariant below
22+
holds PER volume id; in M1 only `root` is registered, so behavior is byte-identical to single-volume. Reads route by
23+
volume id (`get_read_pool_for`); enrichment SKIPS when it returns `None` ("no index registered", replacing the old
24+
`should_exclude` early-return, so non-root listings skip with zero DB work). Root's pool/tracker stay in the
25+
`READ_POOL`/`PENDING_SIZES` globals (same `Arc` the root instance holds). DETAILS § registry.
26+
- **Single-writer thread, per DB; reads off the lock.** Writes go through one writer thread (bounded `sync_channel`,
27+
backpressure on full). Reads use the per-volume `ReadPool` (thread-local WAL conns), NOT the registry mutex (lifecycle
28+
only); don't move read paths back under it. `stop_indexing` / `clear_index` take/remove the instance under the lock,
29+
then drop the guard BEFORE `mgr.shutdown()`'s 5 s drain.
2330
- **`platform_case` collation must be registered on every connection.** It isn't persisted, so the `sqlite3` CLI fails
2431
on any query touching `name`; use `index-query`.
25-
- **Don't drop `UNIQUE (parent_id, name_folded)`**: it's the data-safety net against two writers racing on one DB
26-
(seen once as a 1.83 TB ghost size). Don't drop `name_folded` either; without it the composite-index rebuild takes
27-
~25 min on macOS.
32+
- **Don't drop `UNIQUE (parent_id, name_folded)`** (data-safety net against two writers racing on one DB; seen once as a
33+
1.83 TB ghost size) **nor `name_folded`** (without it the composite-index rebuild takes ~25 min on macOS).
2834
- **Scanner uses `INSERT OR IGNORE`, not `INSERT OR REPLACE`.** REPLACE reassigns IDs (orphaning children) and is
29-
catastrophically slow on a populated DB. `start_scan` truncates `entries` + `dir_stats` before every scan, and the
35+
catastrophically slow on a populated DB. `start_scan` truncates `entries` + `dir_stats` before every scan; the
3036
accumulator counts only rows that landed (per-row OR-IGNORE flags), never bytes a row lost.
31-
- **Mid-scan partial aggregation must BORROW the accumulator maps read-only, never consume/mutate them**, and its
32-
empty-maps no-op must stay SQL-free (a late partial pass legitimately lands after the final aggregation cleared the
33-
maps). Either violation silently ships wrong sizes. Keep `try_send` non-blocking, and fire partial passes only from
34-
the full-scan progress loop (its death scopes them to the scan window).
37+
- **Mid-scan partial aggregation must BORROW the accumulator maps read-only** (never consume/mutate), and its empty-maps
38+
no-op must stay SQL-free (a late pass legitimately lands after the final aggregation cleared the maps) — either
39+
violation silently ships wrong sizes. Keep `try_send` non-blocking; fire passes only from the full-scan progress loop.
3540
- **The index is a disposable cache.** Schema-version mismatch drops + rebuilds; corruption heals by rescan; no online
3641
migrations or user-facing DB errors. Bounded buffers make dropping events then rescanning safe.
37-
- **An interrupted scan must heal to a fresh rescan**, via two cooperating writes: `start_scan` clears
38-
`scan_completed_at` before truncating, and the completion handler writes meta only when `!was_cancelled`. Gate only
39-
the meta writes on it, never the reconcile/live transition.
40-
- **`start_indexing` is lock-first**: claim `Disabled -> Initializing` atomically BEFORE building the heavy
41-
`IndexManager`, else two near-simultaneous starts spawn writer threads racing on one DB.
42+
- **An interrupted scan heals to a fresh rescan** via two cooperating writes: `start_scan` clears `scan_completed_at`
43+
before truncating; the completion handler writes meta only when `!was_cancelled`. Gate only the meta writes, never the
44+
reconcile/live transition.
45+
- **`start_indexing` is lock-first, per volume**: claim `(absent) -> Initializing` for that volume id atomically BEFORE
46+
building the heavy `IndexManager`, else two starts for the SAME volume race writer threads on one DB. Different volumes
47+
start freely.
4248
- **Reconciler/event loops hold a READ connection** (`open_read_connection`), never a write one: write-mode pragmas can
43-
`SQLITE_BUSY` and silently kill live indexing for the session.
44-
- **Defer indexer auto-start until FDA is decided** (`should_auto_start_indexing`): a first-launch scan from `/` opens
45-
TCC-protected dirs and stacks native popups over the FDA modal.
49+
`SQLITE_BUSY` and silently kill live indexing.
50+
- **Defer `root` auto-start until FDA is decided** (`should_auto_start_indexing`): a first-launch scan from `/` stacks
51+
TCC popups over the FDA modal. FDA gates ONLY `root` — future SMB/MTP starts must not route through it (not
52+
TCC-protected).
4653
- **macOS: the writer thread (and any thread calling ObjC/Cocoa) must wrap work in `objc2::rc::autoreleasepool`** or
47-
autoreleased `NSData` / `NSInvocation` objects leak (multi-GB over hours).
48-
- **Spawn indexing tasks with `tauri::async_runtime::spawn`, not `tokio::spawn`**: indexing can start from the sync
49-
`setup()` hook, where `tokio::spawn` panics (no runtime).
54+
autoreleased `NSData` / `NSInvocation` leak (multi-GB over hours).
55+
- **Spawn indexing tasks with `tauri::async_runtime::spawn`, not `tokio::spawn`** (indexing can start from the sync
56+
`setup()` hook, where `tokio::spawn` panics).
5057
- **`IndexWriter` owns the shared `Arc<AtomicI64>` ID counter.** Don't allocate IDs from `MAX(id)` on a read connection:
51-
uncommitted channel inserts make it stale and double-assign IDs.
58+
uncommitted channel inserts make it stale and double-assign.
5259
- **FSEvents `item_removed` must be stat-verified before deleting** (atomic swaps and coalesced events deliver false
5360
removals); upsert instead when the path exists.
5461

0 commit comments

Comments
 (0)