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,