From 7ead26d7fbfcbb5191312f2d3b039054ab4175c0 Mon Sep 17 00:00:00 2001 From: LyAhn Date: Sun, 7 Jun 2026 23:10:34 +0100 Subject: [PATCH] fix: pre-empt review findings before next push - hnsw_index: fetch folder embedding IDs before acquiring the read lock in find_similar_image_matches; holding the lock across a SQLite query was delaying concurrent ensure_index write-lock requests - store/DuplicateFinder: add duplicateScanError state; scanDuplicates now catches errors and stores them rather than silently leaving the user with empty results and no feedback --- src-tauri/src/hnsw_index.rs | 19 ++++++++++++++++--- src/components/DuplicateFinder.tsx | 4 ++++ src/store.ts | 6 +++++- 3 files changed, 25 insertions(+), 4 deletions(-) diff --git a/src-tauri/src/hnsw_index.rs b/src-tauri/src/hnsw_index.rs index 2cdc110..0836a03 100644 --- a/src-tauri/src/hnsw_index.rs +++ b/src-tauri/src/hnsw_index.rs @@ -103,16 +103,29 @@ pub fn find_similar_image_matches( None => return Ok(Vec::new()), }; + // Fetch folder image IDs *before* acquiring the read lock so we don't hold + // the lock across a potentially slow SQLite query, which would delay any + // concurrent ensure_index call waiting for a write lock. + let folder_image_ids: Option> = if let Some(folder_id) = folder_id { + let ids = vector::get_all_image_embeddings_with_ids(conn, Some(folder_id))? + .into_iter() + .map(|(id, _)| id) + .collect(); + Some(ids) + } else { + None + }; + let guard = cache().read().expect("hnsw cache poisoned"); let Some(cached) = guard.as_ref() else { return Ok(Vec::new()); }; let knbn = (offset + limit).max(limit).saturating_add(32); - let neighbours: Vec = if let Some(folder_id) = folder_id { - let mut allowed_ids = vector::get_all_image_embeddings_with_ids(conn, Some(folder_id))? + let neighbours: Vec = if let Some(image_ids) = folder_image_ids { + let mut allowed_ids = image_ids .into_iter() - .filter_map(|(allowed_image_id, _)| { + .filter_map(|allowed_image_id| { cached.external_by_image_id.get(&allowed_image_id).copied() }) .collect::>(); diff --git a/src/components/DuplicateFinder.tsx b/src/components/DuplicateFinder.tsx index d01f05d..cae277f 100644 --- a/src/components/DuplicateFinder.tsx +++ b/src/components/DuplicateFinder.tsx @@ -116,6 +116,7 @@ export function DuplicateFinder() { const duplicateGroups = useGalleryStore((state) => state.duplicateGroups); const duplicateScanning = useGalleryStore((state) => state.duplicateScanning); const duplicateScanProgress = useGalleryStore((state) => state.duplicateScanProgress); + const duplicateScanError = useGalleryStore((state) => state.duplicateScanError); const duplicateSelectedIds = useGalleryStore((state) => state.duplicateSelectedIds); const duplicateLastScanned = useGalleryStore((state) => state.duplicateLastScanned); const selectedFolderId = useGalleryStore((state) => state.selectedFolderId); @@ -228,6 +229,9 @@ export function DuplicateFinder() { ) : null} + {duplicateScanError ? ( +

{duplicateScanError}

+ ) : null} {deleteResult ? (

{deleteResult}

) : null} diff --git a/src/store.ts b/src/store.ts index 82be8d4..60f406a 100644 --- a/src/store.ts +++ b/src/store.ts @@ -278,6 +278,7 @@ interface GalleryState { duplicateGroups: DuplicateGroup[]; duplicateScanning: boolean; duplicateScanProgress: { scanned: number; total: number } | null; + duplicateScanError: string | null; duplicateSelectedIds: Set; duplicateLastScanned: number | null; // Unix timestamp (seconds) duplicateScanFolderId: number | null | undefined; // undefined = never scanned @@ -613,6 +614,7 @@ export const useGalleryStore = create((set, get) => ({ duplicateGroups: [], duplicateScanning: false, duplicateScanProgress: null, + duplicateScanError: null, duplicateSelectedIds: new Set(), duplicateLastScanned: null, duplicateScanFolderId: undefined, @@ -1466,7 +1468,7 @@ export const useGalleryStore = create((set, get) => ({ scanDuplicates: async (folderId = null) => { const { listen } = await import("@tauri-apps/api/event"); - set({ duplicateScanning: true, duplicateGroups: [], duplicateScanProgress: null, duplicateSelectedIds: new Set() }); + set({ duplicateScanning: true, duplicateGroups: [], duplicateScanProgress: null, duplicateScanError: null, duplicateSelectedIds: new Set() }); const unlisten = await listen<[number, number]>("duplicate_scan_progress", (event) => { const [scanned, total] = event.payload; set({ duplicateScanProgress: { scanned, total } }); @@ -1478,6 +1480,8 @@ export const useGalleryStore = create((set, get) => ({ "Duplicate scan complete", groups.length === 1 ? "Found 1 duplicate group." : `Found ${groups.length.toLocaleString()} duplicate groups.`, ); + } catch (e) { + set({ duplicateScanError: String(e) }); } finally { unlisten(); set({ duplicateScanning: false });