diff --git a/src/lib/queries/__tests__/use-dashboard-snapshot.test.tsx b/src/lib/queries/__tests__/use-dashboard-snapshot.test.tsx index e1b85b32b..ba22b54c1 100644 --- a/src/lib/queries/__tests__/use-dashboard-snapshot.test.tsx +++ b/src/lib/queries/__tests__/use-dashboard-snapshot.test.tsx @@ -194,6 +194,53 @@ describe("prefetchDashboardSnapshot — hydration-safe promise handoff", () => { expect(fetchMock).not.toHaveBeenCalled(); }); + it("a cell that already carries data never parks a response, so a write's refetch reads the server as it is AFTER the write", async () => { + // The read-after-write rule for the handoff. A parked response is a copy of + // the server from the moment it was requested; handing it to a later + // `queryFn` run serves pre-write data to a refetch whose entire purpose is + // to see the write. The dashboard reaches `queryFn` again only via the + // 120 s poll, a focus refetch, or the invalidation a mutation fires — never + // via a remount (`refetchOnMount: false`) — so every consumer of a parked + // response is one of those three. + let serverBody: Record = { + layout: { widgets: [] }, + tiles: {}, + medsToday: { takenToday: 0 }, + }; + const fetchMock = vi + .fn() + .mockImplementation(() => Promise.resolve(envelopeResponse(serverBody))); + vi.stubGlobal("fetch", fetchMock); + getQueryDataMock.mockReturnValue({ widgets: [{ id: "existing" }] }); + + // The user comes back to the dashboard after a few minutes away. The cell + // still holds the pre-write body, so the page paints from it at once. + prefetchDashboardSnapshot( + fakeQueryClient({ + data: { tiles: {} }, + dataUpdatedAt: Date.now() - 5 * 60_000, + }), + ); + expect(fetchMock).not.toHaveBeenCalled(); + + // They record a dose. The server now reports it taken. + serverBody = { + layout: { widgets: [] }, + tiles: {}, + medsToday: { takenToday: 1 }, + }; + + // The write's invalidation refetches the mounted cell. + useDashboardSnapshot(); + const opts = lastOptsOf(useQueryMock); + const snap = await ( + opts.queryFn as () => Promise<{ medsToday: { takenToday: number } }> + )(); + + expect(fetchMock).toHaveBeenCalledTimes(1); + expect(snap.medsToday.takenToday).toBe(1); + }); + it("falls back to a fresh fetch when the preload failed — the mounted cell owns error surfacing", async () => { const layout = { widgets: [] }; const fetchMock = vi diff --git a/src/lib/queries/use-dashboard-snapshot.ts b/src/lib/queries/use-dashboard-snapshot.ts index 8608171f0..62c99c199 100644 --- a/src/lib/queries/use-dashboard-snapshot.ts +++ b/src/lib/queries/use-dashboard-snapshot.ts @@ -99,15 +99,25 @@ export function prefetchDashboardSnapshot(queryClient: QueryClient) { } // A fresh handoff is already in flight / parked. if (preloadedSnapshot) return; - // Cache already warm (return-to-dashboard within staleTime) — the - // mounted cell serves it without ever calling `queryFn`; fetching here - // would be a wasted request parked in the slot. + // The handoff exists to cut the COLD first-load waterfall, so it is only + // ever warranted when no snapshot cell has produced data yet. Once a cell + // carries data the page paints from it immediately and there is no waterfall + // left to cut — but the parked response would still sit in the slot, because + // `refetchOnMount: false` means a remount never calls `queryFn` to consume + // it. The next `queryFn` run is then whatever comes along: the 120 s poll, a + // focus refetch, or the invalidation that follows the user's own write. All + // three exist to fetch data that is NEWER than what the cell holds, and all + // three would instead be handed a response whose request was issued BEFORE + // the write. That is how a just-recorded dose kept reading as still due + // until the following poll cleared it. + // // v1.21.3 (b) — the live cell is locale-keyed (`["dashboard","snapshot", // locale]`), but the preloader runs before the locale context is in scope. - // Probe ALL `["dashboard","snapshot"]` cells (prefix) and treat the freshest - // as the warm-slot signal: if any locale's cell is fresh, skip the preload; - // otherwise warm the request through the handoff and let the mounted cell - // commit it under whichever locale the user lands on. + // Probe ALL `["dashboard","snapshot"]` cells (prefix): if ANY locale's cell + // has data, skip the preload; otherwise warm the request through the handoff + // and let the mounted cell commit it under whichever locale the user lands + // on. A cell that exists but never resolved (an error slot) reports + // `dataUpdatedAt === 0` and correctly still warms. const states = queryClient .getQueriesData({ queryKey: queryKeys.dashboardSnapshot() }) .map(([key]) => queryClient.getQueryState(key)); @@ -115,7 +125,7 @@ export function prefetchDashboardSnapshot(queryClient: QueryClient) { (max, s) => (s ? Math.max(max, s.dataUpdatedAt) : max), 0, ); - if (freshest > 0 && Date.now() - freshest < PRELOAD_MAX_AGE_MS) { + if (freshest > 0) { return; } const promise = fetchDashboardSnapshot();