From 1df16a614221c7e0f9005554275f72c7be52eee7 Mon Sep 17 00:00:00 2001 From: dtoro Date: Sun, 7 Jun 2026 21:53:16 +0200 Subject: [PATCH] perf: evict archived/deleted photos from cache immediately Remove archived/restored/deleted/approved UIDs from all cached photo-list pages right after the API confirms, so the grid updates on the same tick instead of waiting for a network round-trip. Also removes the 400ms doneBulk animation delay (now unnecessary since tiles vanish instantly). --- web/src/lib/actions/gridKeyNav.ts | 5 +++- .../components/review/CauseGroupCard.svelte | 2 ++ .../components/timeline/BulkActionBar.svelte | 6 ++++- web/src/lib/services/bulk.ts | 26 +++++++++++++++++++ web/src/lib/services/photoActions.ts | 5 +++- 5 files changed, 41 insertions(+), 3 deletions(-) diff --git a/web/src/lib/actions/gridKeyNav.ts b/web/src/lib/actions/gridKeyNav.ts index 47bd6a2..fc62d2f 100644 --- a/web/src/lib/actions/gridKeyNav.ts +++ b/web/src/lib/actions/gridKeyNav.ts @@ -1,6 +1,6 @@ import { toast } from 'svelte-sonner'; import { batchEdit } from '$lib/services/batch'; -import { invalidatePhotos } from '$lib/services/bulk'; +import { invalidatePhotos, evictFromCache } from '$lib/services/bulk'; import { addToHeap, approvePhoto, @@ -194,6 +194,7 @@ export function gridKeyNav(node: HTMLElement, params: GridKeyNavParams = {}) { return; } doneBulk(doneLabel, ids); + evictFromCache(ids); focusAfter(ids); clearSelection(); invalidatePhotos(ids); @@ -233,6 +234,7 @@ export function gridKeyNav(node: HTMLElement, params: GridKeyNavParams = {}) { return; } doneBulk(`Deleted ${ids.length}`, ids); + evictFromCache(ids); focusAfter(ids); clearSelection(); invalidatePhotos(ids); @@ -270,6 +272,7 @@ export function gridKeyNav(node: HTMLElement, params: GridKeyNavParams = {}) { doneBulk(`Kept ${ids.length}`, ids); toast.success(`Kept ${ids.length}`, { id: tid }); } + evictFromCache(ids); focusAfter(ids); clearSelection(); invalidatePhotos(ids); diff --git a/web/src/lib/components/review/CauseGroupCard.svelte b/web/src/lib/components/review/CauseGroupCard.svelte index 7d2f72d..a64d53b 100644 --- a/web/src/lib/components/review/CauseGroupCard.svelte +++ b/web/src/lib/components/review/CauseGroupCard.svelte @@ -13,6 +13,7 @@ import { useQueryClient } from '@tanstack/svelte-query'; import { toast } from 'svelte-sonner'; import { batchArchive } from '$lib/services/photoprism'; + import { evictFromCache } from '$lib/services/bulk'; import { startBulk, doneBulk, failBulk } from '$lib/stores/bulkAction.svelte'; import PhotoGrid from '$lib/components/timeline/PhotoGrid.svelte'; import { type ReviewGroup } from '$lib/services/adapters/review'; @@ -35,6 +36,7 @@ startBulk(`Archiving…`, uids); try { await batchArchive(uids); + evictFromCache(uids); doneBulk(`Archived ${uids.length}`, uids); toast.success(`Archived ${uids.length}`, { id: tid }); void qc.invalidateQueries({ queryKey: ['review-groups'] }); diff --git a/web/src/lib/components/timeline/BulkActionBar.svelte b/web/src/lib/components/timeline/BulkActionBar.svelte index 8b34486..b5335b8 100644 --- a/web/src/lib/components/timeline/BulkActionBar.svelte +++ b/web/src/lib/components/timeline/BulkActionBar.svelte @@ -14,6 +14,7 @@ } from '$lib/services/photoprism'; import { batchEdit } from '$lib/services/batch'; import { acceptDateAndKeep, cachedPhoto } from '$lib/services/photoActions'; + import { evictFromCache } from '$lib/services/bulk'; import { suggestDateFromPath } from '$lib/utils/suggestDateFromPath'; import { photoNameAndDir } from '$lib/types/photoprism'; import { @@ -129,7 +130,6 @@ const result = await fn(); if (bulk) { doneBulk(bulk.doneLabel, bulk.ids); - await delay(400); } return result; } catch (e) { @@ -152,6 +152,7 @@ setDetail(p?.FileName ?? completedId); } }); + evictFromCache(ids); if (errors.length) { toast.error(`Kept ${updated.length}; ${errors.length} failed`, { id: tid }); } else { @@ -179,6 +180,7 @@ await withBusy(async () => { try { await batchArchive(ids); + evictFromCache(ids); pushUndo(`Archived ${ids.length}`, async () => { await batchRestore(ids); void qc.invalidateQueries({ queryKey: ['photos'] }); @@ -204,6 +206,7 @@ await withBusy(async () => { try { await batchDelete(ids); + evictFromCache(ids); focusAfter(ids); clearSelection(); toast.success(`Deleted ${ids.length}`, { id: tid }); @@ -220,6 +223,7 @@ await withBusy(async () => { try { await batchRestore(ids); + evictFromCache(ids); pushUndo(`Restored ${ids.length}`, async () => { await batchArchive(ids); void qc.invalidateQueries({ queryKey: ['photos'] }); diff --git a/web/src/lib/services/bulk.ts b/web/src/lib/services/bulk.ts index 127b83c..9b7612f 100644 --- a/web/src/lib/services/bulk.ts +++ b/web/src/lib/services/bulk.ts @@ -32,6 +32,32 @@ export function invalidatePhotos(uids: string[]): void { } } +/** + * Remove uids from every cached photo-list page so the grid updates + * instantly instead of waiting for a refetch round-trip. Call after the + * API confirms the mutation, then still invalidate for eventual sync. + */ +export function evictFromCache(uids: string[]): void { + const uidSet = new Set(uids); + const lists = queryClient.getQueriesData({ + queryKey: ['photos'] + }); + for (const [key, data] of lists) { + if (!data) continue; + if (Array.isArray(data)) { + const filtered = data.filter((p) => !uidSet.has(p.UID)); + if (filtered.length < data.length) { + queryClient.setQueryData(key, filtered); + } + } else if (data && 'pages' in data && Array.isArray(data.pages)) { + const pages = data.pages.map((page: PpPhoto[]) => + page.filter((p) => !uidSet.has(p.UID)) + ); + queryClient.setQueryData(key, { ...data, pages }); + } + } +} + /** * Apply a patch to every uid. The patch can be a static body or a per-photo * function (used by keyword merges which need to read each photo's current diff --git a/web/src/lib/services/photoActions.ts b/web/src/lib/services/photoActions.ts index 13b02ac..af54c29 100644 --- a/web/src/lib/services/photoActions.ts +++ b/web/src/lib/services/photoActions.ts @@ -13,7 +13,7 @@ import { toast } from 'svelte-sonner'; import { batchEdit } from './batch'; -import { invalidatePhotos } from './bulk'; +import { invalidatePhotos, evictFromCache } from './bulk'; import { approvePhoto, batchArchive, @@ -68,6 +68,7 @@ export async function dismissPhotos(uids: string[]): Promise { if (uids.length === 0) return; const tid = toast.loading(`Dismissing ${uids.length}…`); const { updated, errors } = await batchEdit(uids, (id) => approvePhoto(id)); + evictFromCache(uids); focusAfter(uids); clearSelection(); invalidatePhotos(uids); @@ -107,6 +108,7 @@ export async function acceptDateAndKeep(uids: string[]): Promise { await approvePhoto(id); return id; }); + evictFromCache(uids); focusAfter(uids); clearSelection(); invalidatePhotos(uids); @@ -133,6 +135,7 @@ export async function archivePhotos(uids: string[]): Promise { toast.error(err instanceof Error ? err.message : 'Archive failed', { id: tid }); return; } + evictFromCache(uids); pushUndo(`Archived ${uids.length}`, async () => { await batchRestore(uids); invalidatePhotos(uids);