perf(explore): instant tag-cloud cache hits + fix stale-on-switch loading

The get_tag_cloud cache key was built by loading and hashing every embedding
blob for the scope *before* checking the cache, so even a cache hit re-read
hundreds of MB on large libraries and stalled Explore for several seconds.
Validate the cache from a lightweight image-ID-set signature plus the embedding
revision instead, so a hit never loads embeddings. The ID-set hash keeps the key
membership-sensitive (add/remove/move between folders) and the revision covers
an image being re-embedded in place. Cache write failures are now logged rather
than silently ignored.

On the frontend, switching folders (or re-entering Explore) no longer leaves the
previous folder's clusters/tags on screen with no loading indicator:
loadTagCloud/loadExploreTags clear stale entries on a real folder switch. The
displayed folder is tracked separately (exploreTagsShownFolderId) from the
cache-dirty marker so a same-folder invalidation (tag edits, new AI tags) does
not masquerade as a switch and wipe the visible list mid-refresh.
This commit is contained in:
2026-06-29 20:25:38 +01:00
parent 23e9850c7a
commit ab7022e118
4 changed files with 120 additions and 32 deletions
+31 -2
View File
@@ -381,7 +381,14 @@ interface GalleryState {
exploreTagEntries: ExploreTagEntry[];
exploreTagLoading: boolean;
exploreTagsFolderId: number | null | undefined;
// Cache-freshness key: the folder the loaded tags belong to. Set to undefined
// by content mutations (tag add/remove/rename/delete) to mark the cache dirty
// and force the next load to refetch.
relatedTagsByKey: Record<string, RelatedTagEntry[]>;
// The folder whose tags are actually on screen. Kept separate from the dirty
// marker above so a same-folder invalidation isn't mistaken for a folder switch
// (which would wipe the visible list and remount manager UI mid-refresh).
exploreTagsShownFolderId: number | null | undefined;
indexingProgress: Record<number, IndexProgress>;
mediaJobProgress: Record<number, FolderJobProgress>;
cacheDir: string;
@@ -877,6 +884,7 @@ export const useGalleryStore = create<GalleryState>((set, get) => ({
indexingProgress: {},
mediaJobProgress: {},
cacheDir: "",
exploreTagsShownFolderId: undefined,
captionModelStatus: null,
captionModelPreparing: false,
captionModelError: null,
@@ -1404,7 +1412,15 @@ export const useGalleryStore = create<GalleryState>((set, get) => ({
return;
}
const requestToken = ++tagCloudRequestToken;
set({ tagCloudLoading: true, tagCloudFolderId: selectedFolderId });
// On a real folder switch, drop the previous folder's clusters so the loading
// panel shows instead of lingering stale results. A same-folder refresh keeps
// them to avoid a flicker when the cache returns instantly.
const isFolderSwitch = tagCloudFolderId !== selectedFolderId;
set({
tagCloudLoading: true,
tagCloudFolderId: selectedFolderId,
...(isFolderSwitch ? { tagCloudEntries: [] } : {}),
});
try {
const entries = await invoke<TagCloudEntry[]>("get_tag_cloud", {
folderId: selectedFolderId,
@@ -1425,7 +1441,20 @@ export const useGalleryStore = create<GalleryState>((set, get) => ({
return;
}
const requestToken = ++exploreTagRequestToken;
set({ exploreTagLoading: true, exploreTagsFolderId: selectedFolderId });
// A real folder switch is decided by what's currently *shown*, not by the
// dirty marker — a same-folder invalidation nulls exploreTagsFolderId but
// leaves exploreTagsShownFolderId pointing at the displayed folder, so the
// visible list (and manager UI state) survives the refresh. On an actual
// switch, drop the previous folder's tags so the loading panel shows.
const { exploreTagsShownFolderId } = get();
const isFolderSwitch =
exploreTagsShownFolderId !== undefined && exploreTagsShownFolderId !== selectedFolderId;
set({
exploreTagLoading: true,
exploreTagsFolderId: selectedFolderId,
exploreTagsShownFolderId: selectedFolderId,
...(isFolderSwitch ? { exploreTagEntries: [], relatedTagsByKey: {} } : {}),
});
try {
const entries = await invoke<ExploreTagEntry[]>("get_explore_tags", {
params: { folder_id: selectedFolderId, limit: 180 },