From 96e62cb7c1e8967db4fdd31a812734050322049c Mon Sep 17 00:00:00 2001 From: LyAhn Date: Mon, 6 Jul 2026 20:41:16 +0100 Subject: [PATCH] perf(frontend): invalidate duplicate-scan cache concurrently and safely MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Cache invalidation across affected folders after a bulk delete ran sequentially through awaited invoke calls, duplicated between gallerySlice and duplicateSlice. Extract a shared invalidateDuplicateScanCaches helper that batches the calls with Promise.allSettled, logging failures instead of throwing — a best-effort cache refresh should never turn an already-successful delete into a rejected promise for the caller. --- src/store/duplicateSlice.ts | 6 ++---- src/store/gallerySlice.ts | 6 ++---- src/store/helpers.ts | 21 +++++++++++++++++++++ 3 files changed, 25 insertions(+), 8 deletions(-) diff --git a/src/store/duplicateSlice.ts b/src/store/duplicateSlice.ts index f2bce2f..3fbcdbf 100644 --- a/src/store/duplicateSlice.ts +++ b/src/store/duplicateSlice.ts @@ -1,6 +1,7 @@ import { invoke } from '@tauri-apps/api/core' import type { StateCreator } from 'zustand' import { notifyTaskComplete } from '../notifications' +import { invalidateDuplicateScanCaches } from './helpers' import type { GalleryStore } from './index' import type { DuplicateGroup, DuplicateScanProgress, DuplicateScanResult } from './types' @@ -146,10 +147,7 @@ export const createDuplicateSlice: StateCreator succeededSet.has(img.id)) .map((img) => img.folder_id) ) - await invoke('invalidate_duplicate_scan_cache', { folderId: null }) // global - for (const folderId of affectedFolderIds) { - await invoke('invalidate_duplicate_scan_cache', { folderId }) - } + await invalidateDuplicateScanCaches(affectedFolderIds) return succeededIds.length }, }) diff --git a/src/store/gallerySlice.ts b/src/store/gallerySlice.ts index 6e0273d..84e7034 100644 --- a/src/store/gallerySlice.ts +++ b/src/store/gallerySlice.ts @@ -3,6 +3,7 @@ import type { StateCreator } from 'zustand' import { PAGE_SIZE, TIMELINE_PAGE_SIZE, + invalidateDuplicateScanCaches, isCurrentGalleryRequest, isDerivedCollectionTitle, mergeImages, @@ -608,10 +609,7 @@ export const createGallerySlice: StateCreator 0 } +// Invalidates the persisted duplicate-scan cache for every scope affected by a +// deletion: the global "all" cache (always, since a folder-scoped deletion +// still makes the global result stale) and each folder that contained a +// deleted image. This is a best-effort background refresh — a failure here +// must not turn an already-successful delete into a rejected promise for the +// caller, so failures are logged rather than thrown. +export async function invalidateDuplicateScanCaches(affectedFolderIds: Set): Promise { + const results = await Promise.allSettled([ + invoke('invalidate_duplicate_scan_cache', { folderId: null }), + ...[...affectedFolderIds].map((folderId) => + invoke('invalidate_duplicate_scan_cache', { folderId }) + ), + ]) + for (const result of results) { + if (result.status === 'rejected') { + console.error('Failed to invalidate duplicate-scan cache:', result.reason) + } + } +} + export function taggingProgressAffectsScope( progressFolderId: number, scopeFolderId: number | null