diff --git a/web/src/plan/api.test.tsx b/web/src/plan/api.test.tsx
index 138fe65..94eb6fb 100644
--- a/web/src/plan/api.test.tsx
+++ b/web/src/plan/api.test.tsx
@@ -269,4 +269,71 @@ describe('week mutations', () => {
hook.rerender({ week: LATER })
expect(weekMutations(queryClient, NEXT)).toEqual([])
})
+
+ it('leaves behind what settles in a week no longer shown, unless that week still has one pending or it is the failure that speaks for it', async () => {
+ const friday = deferred()
+ const monday = deferred()
+ const nextPush = deferred()
+ const laterPush = deferred()
+ stubServer({
+ [`GET ${PLAN}`]: () => jsonResponse(200, planBody()),
+ [`PUT ${FRIDAY}`]: () => friday.reply,
+ [`PUT ${MONDAY}`]: () => monday.reply,
+ [`GET /api/v1/plans/${NEXT}`]: () => errorResponse(404, 'not_found'),
+ [`POST /api/v1/plans/${NEXT}/push-calendar`]: () => nextPush.reply,
+ [`GET /api/v1/plans/${LATER}`]: () => errorResponse(404, 'not_found'),
+ [`POST /api/v1/plans/${LATER}/push-calendar`]: () => laterPush.reply,
+ })
+ const api = createApiClient(fakeLocation('/plan'), createNetworkStatus(window), createVerifiedSession(localStorage))
+ await api.loadMe()
+ const queryClient = createQueryClient()
+ const wrapper = ({ children }: { children: ReactNode }) => (
+
+ {children}
+
+ )
+ const hook = renderHook(
+ ({ week }) => ({ plan: usePlan(week), swap: useSwapDay(week), push: usePushToCalendar(week) }),
+ { wrapper, initialProps: { week: WEEK } },
+ )
+ await waitFor(() => expect(hook.result.current.plan.isSuccess).toBe(true))
+
+ // Two swaps of the week and a push of the next are left pending, and a
+ // third week is shown.
+ act(() => hook.result.current.swap.mutate(ORZO))
+ act(() => hook.result.current.swap.mutate(FISH))
+ await waitFor(() => expect(weekMutations(queryClient, WEEK)).toEqual(['swap pending', 'swap pending']))
+ hook.rerender({ week: NEXT })
+ await waitFor(() => expect(hook.result.current.plan.isSuccess).toBe(true))
+ act(() => hook.result.current.push.mutate())
+ hook.rerender({ week: LATER })
+ await waitFor(() => expect(hook.result.current.plan.isSuccess).toBe(true))
+ expect(weekMutations(queryClient, NEXT)).toEqual(['push pending'])
+
+ // The later swap settles first: the earlier, still pending, must see it,
+ // so both stay.
+ await act(async () => monday.send(jsonResponse(200, planBody({ 0: { id: 'r1', name: 'Fish tacos' } }))))
+ await waitFor(() => expect(weekMutations(queryClient, WEEK)).toEqual(['swap pending', 'swap success']))
+
+ // The push succeeds while the third week is shown: it is left behind,
+ // and the week's pending swap is not.
+ await act(async () => nextPush.send(jsonResponse(200, { ...planBody(), week_start: NEXT })))
+ await waitFor(() => expect(weekMutations(queryClient, NEXT)).toEqual([]))
+ expect(weekMutations(queryClient, WEEK)).toEqual(['swap pending', 'swap success'])
+
+ // Once the earlier swap fails, the week has none pending: both are left
+ // behind, the failure not being the week's latest.
+ await act(async () => friday.send(errorResponse(409, 'dinner_not_plannable')))
+ await waitFor(() => expect(weekMutations(queryClient, WEEK)).toEqual([]))
+
+ // A push of the third week fails once another is shown: it stays, to be
+ // shown on coming back, until yet another week is.
+ act(() => hook.result.current.push.mutate())
+ hook.rerender({ week: NEXT })
+ await waitFor(() => expect(hook.result.current.plan.isSuccess).toBe(true))
+ await act(async () => laterPush.send(errorResponse(500, 'internal')))
+ await waitFor(() => expect(weekMutations(queryClient, LATER)).toEqual(['push error']))
+ hook.rerender({ week: WEEK })
+ expect(weekMutations(queryClient, LATER)).toEqual([])
+ })
})
diff --git a/web/src/plan/api.ts b/web/src/plan/api.ts
index 4a058e0..027bed1 100644
--- a/web/src/plan/api.ts
+++ b/web/src/plan/api.ts
@@ -185,20 +185,34 @@ type WeekMutation = 'swap' | 'push'
// useWeekMutationsKept bounds the mutations of kind the cache keeps. When a
// week is shown, the settled ones of every other week are removed, unless
// that week still has one pending: a pending one, and those an earlier
-// pending one must see, stay across leaving its week and coming back. The
-// cache holds at most the week shown's and those of weeks left pending.
+// pending one must see, stay across leaving its week and coming back. When
+// one settles, the same is done, except that a week's latest, when it
+// failed, stays until another week is shown, so that coming straight back to
+// it shows the failure. The cache holds at most the week shown's, those of
+// weeks left pending, and one failure of each week whose last pending one
+// settled since the week shown was.
function useWeekMutationsKept(weekStart: string, kind: WeekMutation) {
const queryClient = useQueryClient()
useEffect(() => {
const mutations = queryClient.getMutationCache()
- const left = mutations.findAll({
- predicate: ({ options: { mutationKey: key } }) =>
- key !== undefined && key.length === 3 && key[0] === 'plans' && key[1] !== weekStart && key[2] === kind,
- })
- const pending = new Set(left.filter((m) => m.state.status === 'pending').map((m) => m.options.mutationKey![1]))
- for (const mutation of left) {
- if (!pending.has(mutation.options.mutationKey![1])) mutations.remove(mutation)
+ const isKind = (key: readonly unknown[] | undefined) => key !== undefined && key.length === 3 && key[0] === 'plans' && key[2] === kind
+ function sweep(settled: boolean) {
+ const left = mutations.findAll({
+ predicate: ({ options: { mutationKey: key } }) => isKind(key) && key![1] !== weekStart,
+ })
+ const pending = new Set(left.filter((m) => m.state.status === 'pending').map((m) => m.options.mutationKey![1]))
+ const latest = new Map(left.map((m) => [m.options.mutationKey![1], m]))
+ for (const mutation of left) {
+ const week = mutation.options.mutationKey![1]
+ if (pending.has(week)) continue
+ if (settled && latest.get(week) === mutation && mutation.state.status === 'error') continue
+ mutations.remove(mutation)
+ }
}
+ sweep(false)
+ return mutations.subscribe((event) => {
+ if (event.type === 'updated' && isKind(event.mutation.options.mutationKey) && event.mutation.state.status !== 'pending') sweep(true)
+ })
}, [queryClient, weekStart, kind])
}
@@ -252,7 +266,7 @@ export function useSwapDay(weekStart: string): SwapDay {
const isLatest = () => mutations.findAll({ mutationKey }).every((other) => other.mutationId <= mutation.mutationId)
const mutation = mutations.build(queryClient, {
mutationKey,
- // Kept until dismissed or its week is left settled
+ // Kept until dismissed or settled in a week not shown
// (useWeekMutationsKept): a later swap must stay in the cache while an
// earlier one is pending.
gcTime: Infinity,
@@ -350,7 +364,7 @@ export function usePushToCalendar(weekStart: string): PushToCalendar {
function mutate() {
const mutation = mutations.build(queryClient, {
mutationKey,
- // Kept until reset or its week is left settled (useWeekMutationsKept):
+ // Kept until reset or settled in a week not shown (useWeekMutationsKept):
// a push pending in a week left must still be found when it is shown
// again.
gcTime: Infinity,