perf(frontend): invalidate duplicate-scan cache concurrently and safely

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.
This commit is contained in:
2026-07-06 20:41:16 +01:00
parent 42564a93e0
commit 96e62cb7c1
3 changed files with 25 additions and 8 deletions
+2 -4
View File
@@ -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<GalleryStore, [], [], DuplicateS
.filter((img) => 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
},
})
+2 -4
View File
@@ -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<GalleryStore, [], [], GallerySlice
}))
// The DB cascade already removed these from album_images; refresh counts/covers.
void get().loadAlbums()
await invoke('invalidate_duplicate_scan_cache', { folderId: null })
for (const folderId of affectedFolderIds) {
await invoke('invalidate_duplicate_scan_cache', { folderId })
}
await invalidateDuplicateScanCaches(affectedFolderIds)
return succeededIds.length
},
})
+21
View File
@@ -1,3 +1,4 @@
import { invoke } from '@tauri-apps/api/core'
import type {
ImageRecord,
MediaFilter,
@@ -268,6 +269,26 @@ export function scopeHasTaggingPending(
return (progressByFolder[folderId]?.tagging_pending ?? 0) > 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<number>): Promise<void> {
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