From ee6b49952eae07e0af238843ed5b7e727194c590 Mon Sep 17 00:00:00 2001 From: dtoro Date: Tue, 7 Apr 2026 23:24:29 +0200 Subject: [PATCH] fix: optimistic membership cache so P toggle is instant and reliable P already toggled membership in the active heap (remove if every selected photo is a member, otherwise add the missing ones), but the mutation only invalidate-then-refetched the heap-photo-ids cache on success. Pressing P twice in quick succession could read the stale cache and mis-toggle. Both heap-toggle mutations (useKeyboardShortcuts P shortcut and the RightSidebar Pick button) now do an optimistic update in onMutate: - Read the current ['heap-photo-ids', heapId] cache - Add or remove the affected ids in a Set - Write the new array back via setQueryData - Roll back from the captured `previous` on error - Re-sync via invalidateQueries in onSettled (heap counts in particular still need server truth) Result: the basket affordance flips the moment you press P, and a quick second press always reads the new state. Co-Authored-By: Claude Opus 4.6 (1M context) --- .../src/components/layout/RightSidebar.tsx | 19 +++++++++++++-- frontend/src/hooks/useKeyboardShortcuts.ts | 24 +++++++++++++++++-- 2 files changed, 39 insertions(+), 4 deletions(-) diff --git a/frontend/src/components/layout/RightSidebar.tsx b/frontend/src/components/layout/RightSidebar.tsx index 17c9bdc..ce65599 100644 --- a/frontend/src/components/layout/RightSidebar.tsx +++ b/frontend/src/components/layout/RightSidebar.tsx @@ -146,14 +146,29 @@ export function RightSidebar() { ? heapsApi.removePhotos(activeHeap.id, [activePhotoId]) : heapsApi.addPhotos(activeHeap.id, [activePhotoId]) }, - onSuccess: () => { + // Optimistic flip so the badge / button label update instantly. + onMutate: ({ remove }) => { + if (!activeHeap || !activePhotoId) return { previous: undefined } + const key = ['heap-photo-ids', activeHeap.id] as const + const previous = queryClient.getQueryData(key) + const set = new Set(previous ?? []) + if (remove) set.delete(activePhotoId) + else set.add(activePhotoId) + queryClient.setQueryData(key, Array.from(set)) + return { previous } + }, + onError: (_e, _vars, ctx) => { + if (activeHeap && ctx?.previous) { + queryClient.setQueryData(['heap-photo-ids', activeHeap.id], ctx.previous) + } + }, + onSettled: () => { queryClient.invalidateQueries({ queryKey: HEAPS_QUERY_KEY }) if (activeHeap) { queryClient.invalidateQueries({ queryKey: ['heap-photo-ids', activeHeap.id], }) } - queryClient.invalidateQueries({ queryKey: ['photos'] }) }, }) diff --git a/frontend/src/hooks/useKeyboardShortcuts.ts b/frontend/src/hooks/useKeyboardShortcuts.ts index 7ef6871..bb90882 100644 --- a/frontend/src/hooks/useKeyboardShortcuts.ts +++ b/frontend/src/hooks/useKeyboardShortcuts.ts @@ -77,6 +77,25 @@ export function useKeyboardShortcuts(props: KeyboardShortcutsProps) { remove ? heapsApi.removePhotos(heapId, photoIds) : heapsApi.addPhotos(heapId, photoIds), + // Optimistically flip the membership cache so the basket affordance + // updates instantly and a quick second P press reads the new state + // (otherwise invalidate-then-refetch leaves a brief stale window). + onMutate: ({ heapId, photoIds, remove }) => { + const key = ['heap-photo-ids', heapId] as const + const previous = queryClient.getQueryData(key) + const set = new Set(previous ?? []) + if (remove) photoIds.forEach((id) => set.delete(id)) + else photoIds.forEach((id) => set.add(id)) + queryClient.setQueryData(key, Array.from(set)) + return { previous } + }, + onError: (e: any, _vars, ctx) => { + // Roll back the optimistic update on failure. + if (ctx?.previous) { + queryClient.setQueryData(['heap-photo-ids', _vars.heapId], ctx.previous) + } + toast.error('Heap update failed', e.message || 'Unknown error') + }, onSuccess: (data, vars) => { const heap = (queryClient.getQueryData(HEAPS_QUERY_KEY) ?? []).find( (h) => h.id === vars.heapId @@ -95,11 +114,12 @@ export function useKeyboardShortcuts(props: KeyboardShortcutsProps) { ) } } + }, + onSettled: (_data, _err, vars) => { + // Re-sync with server truth (heap counts in particular need this). queryClient.invalidateQueries({ queryKey: HEAPS_QUERY_KEY }) queryClient.invalidateQueries({ queryKey: ['heap-photo-ids', vars.heapId] }) - queryClient.invalidateQueries({ queryKey: ['photos'] }) }, - onError: (e: any) => toast.error('Heap update failed', e.message || 'Unknown error'), }) const togglePickOnSelection = () => {