revert evictFromCache, add back delay + marks invalidation

Remove evictFromCache entirely - the cache-manipulation approach was
brittle and broke both archive (photos not removed) and tags
(colors/ratings showing empty). Replace with: 200ms delay before
invalidation to let PhotoPrism's indexer process the change, plus
invalidateQueries(['marks']) so tag caches (colors, ratings, notes)
refresh alongside the photo timeline.
This commit is contained in:
2026-06-07 22:31:39 +02:00
parent 82f2a40269
commit 73c36b4817
5 changed files with 14 additions and 45 deletions

View File

@@ -1,6 +1,6 @@
import { toast } from 'svelte-sonner'; import { toast } from 'svelte-sonner';
import { batchEdit } from '$lib/services/batch'; import { batchEdit } from '$lib/services/batch';
import { invalidatePhotos, evictFromCache } from '$lib/services/bulk'; import { invalidatePhotos } from '$lib/services/bulk';
import { import {
addToHeap, addToHeap,
approvePhoto, approvePhoto,
@@ -194,10 +194,10 @@ export function gridKeyNav(node: HTMLElement, params: GridKeyNavParams = {}) {
return; return;
} }
doneBulk(doneLabel, ids); doneBulk(doneLabel, ids);
evictFromCache(ids);
focusAfter(ids); focusAfter(ids);
clearSelection(); clearSelection();
invalidatePhotos(ids); invalidatePhotos(ids);
void queryClient.invalidateQueries({ queryKey: ['marks'] });
toast.success(doneLabel, { id: tid }); toast.success(doneLabel, { id: tid });
pushUndo(doneLabel, async () => { pushUndo(doneLabel, async () => {
if (target) await batchRestore(ids); if (target) await batchRestore(ids);
@@ -234,10 +234,10 @@ export function gridKeyNav(node: HTMLElement, params: GridKeyNavParams = {}) {
return; return;
} }
doneBulk(`Deleted ${ids.length}`, ids); doneBulk(`Deleted ${ids.length}`, ids);
evictFromCache(ids);
focusAfter(ids); focusAfter(ids);
clearSelection(); clearSelection();
invalidatePhotos(ids); invalidatePhotos(ids);
void queryClient.invalidateQueries({ queryKey: ['marks'] });
toast.success(`Deleted ${ids.length}`, { id: tid }); toast.success(`Deleted ${ids.length}`, { id: tid });
} }
@@ -272,7 +272,6 @@ export function gridKeyNav(node: HTMLElement, params: GridKeyNavParams = {}) {
doneBulk(`Kept ${ids.length}`, ids); doneBulk(`Kept ${ids.length}`, ids);
toast.success(`Kept ${ids.length}`, { id: tid }); toast.success(`Kept ${ids.length}`, { id: tid });
} }
evictFromCache(ids);
focusAfter(ids); focusAfter(ids);
clearSelection(); clearSelection();
invalidatePhotos(ids); invalidatePhotos(ids);

View File

@@ -13,7 +13,6 @@
import { useQueryClient } from '@tanstack/svelte-query'; import { useQueryClient } from '@tanstack/svelte-query';
import { toast } from 'svelte-sonner'; import { toast } from 'svelte-sonner';
import { batchArchive } from '$lib/services/photoprism'; import { batchArchive } from '$lib/services/photoprism';
import { evictFromCache } from '$lib/services/bulk';
import { startBulk, doneBulk, failBulk } from '$lib/stores/bulkAction.svelte'; import { startBulk, doneBulk, failBulk } from '$lib/stores/bulkAction.svelte';
import PhotoGrid from '$lib/components/timeline/PhotoGrid.svelte'; import PhotoGrid from '$lib/components/timeline/PhotoGrid.svelte';
import { type ReviewGroup } from '$lib/services/adapters/review'; import { type ReviewGroup } from '$lib/services/adapters/review';
@@ -36,11 +35,11 @@
startBulk(`Archiving…`, uids); startBulk(`Archiving…`, uids);
try { try {
await batchArchive(uids); await batchArchive(uids);
evictFromCache(uids);
doneBulk(`Archived ${uids.length}`, uids); doneBulk(`Archived ${uids.length}`, uids);
toast.success(`Archived ${uids.length}`, { id: tid }); toast.success(`Archived ${uids.length}`, { id: tid });
void qc.invalidateQueries({ queryKey: ['review-groups'] }); void qc.invalidateQueries({ queryKey: ['review-groups'] });
void qc.invalidateQueries({ queryKey: ['photos'] }); void qc.invalidateQueries({ queryKey: ['photos'] });
void qc.invalidateQueries({ queryKey: ['marks'] });
} catch (err) { } catch (err) {
failBulk(uids); failBulk(uids);
toast.error(err instanceof Error ? err.message : 'Archive all failed', { id: tid }); toast.error(err instanceof Error ? err.message : 'Archive all failed', { id: tid });

View File

@@ -14,7 +14,6 @@
} from '$lib/services/photoprism'; } from '$lib/services/photoprism';
import { batchEdit } from '$lib/services/batch'; import { batchEdit } from '$lib/services/batch';
import { acceptDateAndKeep, cachedPhoto } from '$lib/services/photoActions'; import { acceptDateAndKeep, cachedPhoto } from '$lib/services/photoActions';
import { evictFromCache } from '$lib/services/bulk';
import { suggestDateFromPath } from '$lib/utils/suggestDateFromPath'; import { suggestDateFromPath } from '$lib/utils/suggestDateFromPath';
import { photoNameAndDir } from '$lib/types/photoprism'; import { photoNameAndDir } from '$lib/types/photoprism';
import { import {
@@ -130,6 +129,7 @@
const result = await fn(); const result = await fn();
if (bulk) { if (bulk) {
doneBulk(bulk.doneLabel, bulk.ids); doneBulk(bulk.doneLabel, bulk.ids);
await delay(200);
} }
return result; return result;
} catch (e) { } catch (e) {
@@ -138,6 +138,8 @@
} finally { } finally {
busy = false; busy = false;
void qc.invalidateQueries({ queryKey: ['photos'] }); void qc.invalidateQueries({ queryKey: ['photos'] });
void qc.invalidateQueries({ queryKey: ['marks'] });
void qc.invalidateQueries({ queryKey: ['review-groups'] });
} }
} }
@@ -152,7 +154,6 @@
setDetail(p?.FileName ?? completedId); setDetail(p?.FileName ?? completedId);
} }
}); });
evictFromCache(ids);
if (errors.length) { if (errors.length) {
toast.error(`Kept ${updated.length}; ${errors.length} failed`, { id: tid }); toast.error(`Kept ${updated.length}; ${errors.length} failed`, { id: tid });
} else { } else {
@@ -180,7 +181,6 @@
await withBusy(async () => { await withBusy(async () => {
try { try {
await batchArchive(ids); await batchArchive(ids);
evictFromCache(ids);
pushUndo(`Archived ${ids.length}`, async () => { pushUndo(`Archived ${ids.length}`, async () => {
await batchRestore(ids); await batchRestore(ids);
void qc.invalidateQueries({ queryKey: ['photos'] }); void qc.invalidateQueries({ queryKey: ['photos'] });
@@ -206,7 +206,6 @@
await withBusy(async () => { await withBusy(async () => {
try { try {
await batchDelete(ids); await batchDelete(ids);
evictFromCache(ids);
focusAfter(ids); focusAfter(ids);
clearSelection(); clearSelection();
toast.success(`Deleted ${ids.length}`, { id: tid }); toast.success(`Deleted ${ids.length}`, { id: tid });
@@ -223,7 +222,6 @@
await withBusy(async () => { await withBusy(async () => {
try { try {
await batchRestore(ids); await batchRestore(ids);
evictFromCache(ids);
pushUndo(`Restored ${ids.length}`, async () => { pushUndo(`Restored ${ids.length}`, async () => {
await batchArchive(ids); await batchArchive(ids);
void qc.invalidateQueries({ queryKey: ['photos'] }); void qc.invalidateQueries({ queryKey: ['photos'] });

View File

@@ -32,36 +32,12 @@ export function invalidatePhotos(uids: string[]): void {
} }
} }
/** export function invalidateAllPhotoCaches(): void {
* Remove uids from every cached infinite photo-list query so the grid void queryClient.invalidateQueries({ queryKey: ['photos'] });
* updates instantly instead of waiting for a refetch round-trip. Call void queryClient.invalidateQueries({ queryKey: ['marks'] });
* after the API confirms the mutation, then still invalidate for eventual void queryClient.invalidateQueries({ queryKey: ['labels'] });
* sync. Only targets infinite queries (those with a `pages` array) — void queryClient.invalidateQueries({ queryKey: ['review-groups'] });
* flat list caches like marks-pool, with-notes, keywords, etc. are left void queryClient.invalidateQueries({ queryKey: ['heaps'] });
* intact so tag/category drill pages don't lose referenced photos.
*/
export function evictFromCache(uids: string[]): void {
const uidSet = new Set(uids);
const lists = queryClient.getQueriesData<
{ pages?: PpPhoto[][] } | { pages?: unknown[] }
>({
queryKey: ['photos']
});
for (const [key, data] of lists) {
if (!data) continue;
if ('pages' in data && Array.isArray((data as { pages?: unknown[] }).pages)) {
const pages = (data as { pages: PpPhoto[][] }).pages;
let changed = false;
const filtered = pages.map((page: PpPhoto[]) => {
const f = page.filter((p) => !uidSet.has(p.UID));
if (f.length < page.length) changed = true;
return f;
});
if (changed) {
queryClient.setQueryData(key, { ...data, pages: filtered });
}
}
}
} }
/** /**

View File

@@ -13,7 +13,7 @@
import { toast } from 'svelte-sonner'; import { toast } from 'svelte-sonner';
import { batchEdit } from './batch'; import { batchEdit } from './batch';
import { invalidatePhotos, evictFromCache } from './bulk'; import { invalidatePhotos } from './bulk';
import { import {
approvePhoto, approvePhoto,
batchArchive, batchArchive,
@@ -68,7 +68,6 @@ export async function dismissPhotos(uids: string[]): Promise<void> {
if (uids.length === 0) return; if (uids.length === 0) return;
const tid = toast.loading(`Dismissing ${uids.length}`); const tid = toast.loading(`Dismissing ${uids.length}`);
const { updated, errors } = await batchEdit(uids, (id) => approvePhoto(id)); const { updated, errors } = await batchEdit(uids, (id) => approvePhoto(id));
evictFromCache(uids);
focusAfter(uids); focusAfter(uids);
clearSelection(); clearSelection();
invalidatePhotos(uids); invalidatePhotos(uids);
@@ -108,7 +107,6 @@ export async function acceptDateAndKeep(uids: string[]): Promise<void> {
await approvePhoto(id); await approvePhoto(id);
return id; return id;
}); });
evictFromCache(uids);
focusAfter(uids); focusAfter(uids);
clearSelection(); clearSelection();
invalidatePhotos(uids); invalidatePhotos(uids);
@@ -135,7 +133,6 @@ export async function archivePhotos(uids: string[]): Promise<void> {
toast.error(err instanceof Error ? err.message : 'Archive failed', { id: tid }); toast.error(err instanceof Error ? err.message : 'Archive failed', { id: tid });
return; return;
} }
evictFromCache(uids);
pushUndo(`Archived ${uids.length}`, async () => { pushUndo(`Archived ${uids.length}`, async () => {
await batchRestore(uids); await batchRestore(uids);
invalidatePhotos(uids); invalidatePhotos(uids);