Collect settled week mutations when they settle, not only on a week change
The shared cleanup of swap and push mutations also runs whenever one of its kind settles, so an entry that settles while another week is shown is no longer kept indefinitely under gcTime: Infinity. Pending mutations, and the settled ones of a week that still has one pending, are never removed; a left week's latest mutation, when it failed, stays until another week is shown so coming straight back still shows the failure. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
@@ -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 }) => (
|
||||
<ApiProvider client={api}>
|
||||
<QueryClientProvider client={queryClient}>{children}</QueryClientProvider>
|
||||
</ApiProvider>
|
||||
)
|
||||
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([])
|
||||
})
|
||||
})
|
||||
|
||||
+25
-11
@@ -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<Plan, Error, Swap, { previous: PlanDay; swapped: PlanDay }>(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<Plan, Error, void, unknown>(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,
|
||||
|
||||
Reference in New Issue
Block a user