From 9bd8c98f674e40f4b3f54e201702e97b3844daed Mon Sep 17 00:00:00 2001 From: Wonsuk Choi Date: Thu, 13 Aug 2026 21:44:21 +0900 Subject: [PATCH 01/21] fix(landing/QueryLanding): restore the live cache demo with a real 'useQuery' hero --- src/components/landing/LibraryLanding.tsx | 5 +- src/components/landing/QueryLanding.tsx | 304 +++++++++++++++++++++- 2 files changed, 307 insertions(+), 2 deletions(-) diff --git a/src/components/landing/LibraryLanding.tsx b/src/components/landing/LibraryLanding.tsx index 08f6c7570..911946092 100644 --- a/src/components/landing/LibraryLanding.tsx +++ b/src/components/landing/LibraryLanding.tsx @@ -66,6 +66,9 @@ export type LibraryLandingConfig = { items: readonly LibraryLandingWorkbenchItem[] label: string } + // Replaces the static `hero` mockup with a live panel. Query uses this to + // demo its own cache; every other landing rides the mockup. + heroRender?: React.ReactNode libraryId: LibraryLandingId lifecycle: { body: string @@ -189,7 +192,7 @@ export function LibraryLanding({ config }: { config: LibraryLandingConfig }) { } + hero={config.heroRender ?? } libraryId={config.libraryId} prompt={config.prompt} promptLabel={config.promptLabel} diff --git a/src/components/landing/QueryLanding.tsx b/src/components/landing/QueryLanding.tsx index ebf747b52..56938a11d 100644 --- a/src/components/landing/QueryLanding.tsx +++ b/src/components/landing/QueryLanding.tsx @@ -1,12 +1,67 @@ +import * as React from 'react' +import { useMutation, useQuery, useQueryClient } from '@tanstack/react-query' import { + ArrowsClockwiseIcon, EyeClosedIcon, KeyIcon, LightningIcon, + PlusIcon, SkullIcon, } from '@phosphor-icons/react' +import { usePrefersReducedMotion } from '~/utils/usePrefersReducedMotion' import { LibraryLanding, type LibraryLandingConfig } from './LibraryLanding' +type QueryHeroIssue = { + id: string + observers: number + priority: number + title: string +} + +type QueryHeroSnapshot = { + fetchedAt: number + revision: number + rows: Array +} + +type QueryHeroMutationContext = { + previous?: QueryHeroSnapshot +} + +const queryHeroKey = ['landing-query-hero'] as const + +const queryHeroInitialRows: Array = [ + { id: 'router-cache', observers: 3, priority: 98, title: 'Router dashboard' }, + { id: 'project-detail', observers: 2, priority: 91, title: 'Project detail' }, + { + id: 'offline-queue', + observers: 1, + priority: 84, + title: 'Offline mutation queue', + }, +] + +const queryHeroInitialSnapshot: QueryHeroSnapshot = { + // `0` keeps `Date.now()` out of the first render so SSR and hydration agree. + fetchedAt: 0, + revision: 0, + rows: queryHeroInitialRows, +} + +const queryHeroMutationTitles = [ + 'Optimistic table edit', + 'Search filter sync', + 'Background retry lane', + 'Prefetched route data', +] + +function waitForQueryHero(ms: number) { + return new Promise((resolve) => { + setTimeout(resolve, ms) + }) +} + const queryLanding = { libraryId: 'query', headline: 'The server-state standard for modern frontend apps.', @@ -115,5 +170,252 @@ const queryLanding = { } satisfies LibraryLandingConfig export default function QueryLanding() { - return + return ( + }} + /> + ) +} + +/** + * The hero panel runs a real QueryClient rather than mocking one: the cache + * badge, revision counter, and row list are all derived from query state, and + * "Add issue" is an optimistic mutation that rolls back on error. + */ +function QueryCachePanel() { + const prefersReducedMotion = usePrefersReducedMotion() + const queryClient = useQueryClient() + const serverRowsRef = React.useRef(queryHeroInitialRows) + const serverRevisionRef = React.useRef(0) + const mutationSequenceRef = React.useRef(0) + // Starts paused so the server render matches the first client render; the + // effect below turns it on unless the visitor asked for reduced motion. + const [isLive, setIsLive] = React.useState(false) + + const projectsQuery = useQuery({ + queryKey: queryHeroKey, + queryFn: async (): Promise => { + await waitForQueryHero(620) + + return { + fetchedAt: Date.now(), + revision: serverRevisionRef.current, + rows: serverRowsRef.current, + } + }, + initialData: queryHeroInitialSnapshot, + initialDataUpdatedAt: 0, + refetchInterval: isLive ? 4200 : false, + staleTime: 3200, + }) + + const addIssueMutation = useMutation< + QueryHeroIssue, + Error, + QueryHeroIssue, + QueryHeroMutationContext + >({ + mutationFn: async (issue) => { + await waitForQueryHero(720) + serverRevisionRef.current += 1 + serverRowsRef.current = [ + issue, + ...serverRowsRef.current.filter((row) => row.id !== issue.id), + ].slice(0, 5) + + return issue + }, + onMutate: async (issue) => { + await queryClient.cancelQueries({ queryKey: queryHeroKey }) + const previous = queryClient.getQueryData(queryHeroKey) + + queryClient.setQueryData(queryHeroKey, (current) => ({ + fetchedAt: current?.fetchedAt ?? 0, + revision: current?.revision ?? serverRevisionRef.current, + rows: [ + issue, + ...(current?.rows ?? queryHeroInitialRows).filter( + (row) => row.id !== issue.id, + ), + ].slice(0, 5), + })) + + return { previous } + }, + onError: (_error, _issue, context) => { + if (context?.previous) { + queryClient.setQueryData( + queryHeroKey, + context.previous, + ) + } + }, + onSettled: () => queryClient.invalidateQueries({ queryKey: queryHeroKey }), + }) + + const cacheState = projectsQuery.isFetching + ? 'fetching' + : projectsQuery.isStale + ? 'stale' + : 'fresh' + const fetchedLabel = + projectsQuery.data.fetchedAt > 0 + ? `${Math.max(0, Math.round((Date.now() - projectsQuery.data.fetchedAt) / 1000))}s ago` + : 'primed' + + React.useEffect(() => { + if (prefersReducedMotion === false) { + setIsLive(true) + } + }, [prefersReducedMotion]) + + const addIssue = () => { + const nextSequence = mutationSequenceRef.current + 1 + const nextTitle = + queryHeroMutationTitles[ + (nextSequence - 1) % queryHeroMutationTitles.length + ] + + mutationSequenceRef.current = nextSequence + addIssueMutation.mutate({ + id: `optimistic-${nextSequence}`, + observers: (nextSequence % 3) + 1, + priority: 72 + ((nextSequence * 7) % 24), + title: nextTitle ?? 'Optimistic write', + }) + } + + return ( +
+
+ + + {queryLanding.hero.label} + +
+ +
+
+
+ + {cacheState} + + + rev {projectsQuery.data.revision} / {fetchedLabel} + +
+ + {projectsQuery.data.rows.map((row) => ( +
+ + + + ['issues', '{row.id}'] + + + {row.title} + + + + P{row.priority} + + + + + + + + {row.observers} obs + + +
+ ))} +
+ +
+
+ +
+ + +
+
+ +
+

{queryLanding.hero.detailTitle}

+

+ ['issues', '{projectsQuery.data.rows[0]?.id ?? 'router-cache'}'] +

+

+ {queryLanding.hero.detailBody} +

+
+ +
+ {[ + { label: 'status', value: projectsQuery.status }, + { + label: 'isFetching', + value: String(projectsQuery.isFetching), + }, + { label: 'staleTime', value: '3,200' }, + { label: 'mutation', value: addIssueMutation.status }, + ].map((fact) => ( +
+
{fact.label}
+
+ {fact.value} +
+
+ ))} +
+
+
+
+ ) } From a63fcf8f14d518374feb7d382a1c98b99779a4a4 Mon Sep 17 00:00:00 2001 From: Wonsuk Choi Date: Thu, 13 Aug 2026 21:47:42 +0900 Subject: [PATCH 02/21] refactor(landing/QueryLanding): drop comments the code already states --- src/components/landing/LibraryLanding.tsx | 2 -- src/components/landing/QueryLanding.tsx | 5 ----- 2 files changed, 7 deletions(-) diff --git a/src/components/landing/LibraryLanding.tsx b/src/components/landing/LibraryLanding.tsx index 911946092..0c7362022 100644 --- a/src/components/landing/LibraryLanding.tsx +++ b/src/components/landing/LibraryLanding.tsx @@ -66,8 +66,6 @@ export type LibraryLandingConfig = { items: readonly LibraryLandingWorkbenchItem[] label: string } - // Replaces the static `hero` mockup with a live panel. Query uses this to - // demo its own cache; every other landing rides the mockup. heroRender?: React.ReactNode libraryId: LibraryLandingId lifecycle: { diff --git a/src/components/landing/QueryLanding.tsx b/src/components/landing/QueryLanding.tsx index 56938a11d..061b455b4 100644 --- a/src/components/landing/QueryLanding.tsx +++ b/src/components/landing/QueryLanding.tsx @@ -177,11 +177,6 @@ export default function QueryLanding() { ) } -/** - * The hero panel runs a real QueryClient rather than mocking one: the cache - * badge, revision counter, and row list are all derived from query state, and - * "Add issue" is an optimistic mutation that rolls back on error. - */ function QueryCachePanel() { const prefersReducedMotion = usePrefersReducedMotion() const queryClient = useQueryClient() From dc97d4c78c58d3c5c3edf66767fc1ac48462f55e Mon Sep 17 00:00:00 2001 From: Wonsuk Choi Date: Thu, 13 Aug 2026 21:52:59 +0900 Subject: [PATCH 03/21] fix(landing/QueryLanding): tick the age readout and honor reduced motion changes --- src/components/landing/QueryLanding.tsx | 21 ++++++++++++++++----- 1 file changed, 16 insertions(+), 5 deletions(-) diff --git a/src/components/landing/QueryLanding.tsx b/src/components/landing/QueryLanding.tsx index 061b455b4..81b341beb 100644 --- a/src/components/landing/QueryLanding.tsx +++ b/src/components/landing/QueryLanding.tsx @@ -186,6 +186,8 @@ function QueryCachePanel() { // Starts paused so the server render matches the first client render; the // effect below turns it on unless the visitor asked for reduced motion. const [isLive, setIsLive] = React.useState(false) + // `0` until the first tick, for the same first-render reason as `fetchedAt`. + const [now, setNow] = React.useState(0) const projectsQuery = useQuery({ queryKey: queryHeroKey, @@ -255,15 +257,20 @@ function QueryCachePanel() { : 'fresh' const fetchedLabel = projectsQuery.data.fetchedAt > 0 - ? `${Math.max(0, Math.round((Date.now() - projectsQuery.data.fetchedAt) / 1000))}s ago` + ? `${Math.max(0, Math.round((Math.max(now, projectsQuery.data.fetchedAt) - projectsQuery.data.fetchedAt) / 1000))}s ago` : 'primed' React.useEffect(() => { - if (prefersReducedMotion === false) { - setIsLive(true) - } + if (prefersReducedMotion === null) return + + setIsLive(prefersReducedMotion === false) }, [prefersReducedMotion]) + React.useEffect(() => { + const id = setInterval(() => setNow(Date.now()), 1000) + return () => clearInterval(id) + }, []) + const addIssue = () => { const nextSequence = mutationSequenceRef.current + 1 const nextTitle = @@ -365,7 +372,11 @@ function QueryCachePanel() { aria-hidden="true" size={13} weight="bold" - className={projectsQuery.isFetching ? 'animate-spin' : ''} + className={ + projectsQuery.isFetching + ? 'animate-spin motion-reduce:animate-none' + : '' + } /> Refetch From 3a1d31e0484253178c020104e4cebe7c47896250 Mon Sep 17 00:00:00 2001 From: Wonsuk Choi Date: Thu, 13 Aug 2026 21:58:00 +0900 Subject: [PATCH 04/21] feat(landing/QueryLanding): select a cached row to inspect its state --- src/components/landing/QueryLanding.tsx | 24 ++++++++++++++++++++---- 1 file changed, 20 insertions(+), 4 deletions(-) diff --git a/src/components/landing/QueryLanding.tsx b/src/components/landing/QueryLanding.tsx index 81b341beb..830e40527 100644 --- a/src/components/landing/QueryLanding.tsx +++ b/src/components/landing/QueryLanding.tsx @@ -188,6 +188,9 @@ function QueryCachePanel() { const [isLive, setIsLive] = React.useState(false) // `0` until the first tick, for the same first-render reason as `fetchedAt`. const [now, setNow] = React.useState(0) + const [selectedId, setSelectedId] = React.useState( + queryHeroInitialRows[0]?.id, + ) const projectsQuery = useQuery({ queryKey: queryHeroKey, @@ -255,6 +258,11 @@ function QueryCachePanel() { : projectsQuery.isStale ? 'stale' : 'fresh' + // The selected row can fall out of the cache once five newer issues arrive, + // so fall back to the top row rather than holding a dangling id. + const selectedRow = + projectsQuery.data.rows.find((row) => row.id === selectedId) ?? + projectsQuery.data.rows[0] const fetchedLabel = projectsQuery.data.fetchedAt > 0 ? `${Math.max(0, Math.round((Math.max(now, projectsQuery.data.fetchedAt) - projectsQuery.data.fetchedAt) / 1000))}s ago` @@ -320,9 +328,12 @@ function QueryCachePanel() { {projectsQuery.data.rows.map((row) => ( -
setSelectedId(row.id)} > @@ -348,7 +359,7 @@ function QueryCachePanel() { {row.observers} obs -
+ ))} @@ -395,7 +406,7 @@ function QueryCachePanel() {

{queryLanding.hero.detailTitle}

- ['issues', '{projectsQuery.data.rows[0]?.id ?? 'router-cache'}'] + ['issues', '{selectedRow?.id ?? 'router-cache'}']

{queryLanding.hero.detailBody} @@ -409,6 +420,11 @@ function QueryCachePanel() { label: 'isFetching', value: String(projectsQuery.isFetching), }, + { + label: 'observers', + value: String(selectedRow?.observers ?? 0), + }, + { label: 'priority', value: `P${selectedRow?.priority ?? 0}` }, { label: 'staleTime', value: '3,200' }, { label: 'mutation', value: addIssueMutation.status }, ].map((fact) => ( From b4c4d1ae8cd7968d41b179959e4f181dcf459f19 Mon Sep 17 00:00:00 2001 From: Wonsuk Choi Date: Thu, 13 Aug 2026 22:00:20 +0900 Subject: [PATCH 05/21] fix(landing/QueryLanding): spell out the observer count instead of 'obs' --- src/components/landing/QueryLanding.tsx | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/src/components/landing/QueryLanding.tsx b/src/components/landing/QueryLanding.tsx index 830e40527..fe6b371a2 100644 --- a/src/components/landing/QueryLanding.tsx +++ b/src/components/landing/QueryLanding.tsx @@ -356,7 +356,8 @@ function QueryCachePanel() { /> - {row.observers} obs + {row.observers}{' '} + {row.observers === 1 ? 'observer' : 'observers'} From 9a25298ab7581c7ae4b5b942a3e36dde5b08ff36 Mon Sep 17 00:00:00 2001 From: Wonsuk Choi Date: Fri, 14 Aug 2026 09:32:51 +0900 Subject: [PATCH 06/21] feat(landing/QueryLanding): drive the row gauge with cache freshness --- src/components/landing/QueryLanding.tsx | 29 ++++++++++++++++++++++--- 1 file changed, 26 insertions(+), 3 deletions(-) diff --git a/src/components/landing/QueryLanding.tsx b/src/components/landing/QueryLanding.tsx index fe6b371a2..d03b6bf46 100644 --- a/src/components/landing/QueryLanding.tsx +++ b/src/components/landing/QueryLanding.tsx @@ -31,6 +31,8 @@ type QueryHeroMutationContext = { const queryHeroKey = ['landing-query-hero'] as const +const queryHeroStaleTime = 3200 + const queryHeroInitialRows: Array = [ { id: 'router-cache', observers: 3, priority: 98, title: 'Router dashboard' }, { id: 'project-detail', observers: 2, priority: 91, title: 'Project detail' }, @@ -206,7 +208,7 @@ function QueryCachePanel() { initialData: queryHeroInitialSnapshot, initialDataUpdatedAt: 0, refetchInterval: isLive ? 4200 : false, - staleTime: 3200, + staleTime: queryHeroStaleTime, }) const addIssueMutation = useMutation< @@ -267,6 +269,24 @@ function QueryCachePanel() { projectsQuery.data.fetchedAt > 0 ? `${Math.max(0, Math.round((Math.max(now, projectsQuery.data.fetchedAt) - projectsQuery.data.fetchedAt) / 1000))}s ago` : 'primed' + // Freshness drains from 100% to 0% across `staleTime`, so the gauge shows how + // much of the cache entry's fresh window is left before Query marks it stale. + const freshness = + projectsQuery.data.fetchedAt > 0 + ? Math.max( + 0, + Math.min( + 100, + Math.round( + (1 - + (Math.max(now, projectsQuery.data.fetchedAt) - + projectsQuery.data.fetchedAt) / + queryHeroStaleTime) * + 100, + ), + ), + ) + : 100 React.useEffect(() => { if (prefersReducedMotion === null) return @@ -352,7 +372,7 @@ function QueryCachePanel() { @@ -426,7 +446,10 @@ function QueryCachePanel() { value: String(selectedRow?.observers ?? 0), }, { label: 'priority', value: `P${selectedRow?.priority ?? 0}` }, - { label: 'staleTime', value: '3,200' }, + { + label: 'staleTime', + value: queryHeroStaleTime.toLocaleString('en-US'), + }, { label: 'mutation', value: addIssueMutation.status }, ].map((fact) => (

From d16d1a11c3279bce3d10f7e5165ea962393967c4 Mon Sep 17 00:00:00 2001 From: Wonsuk Choi Date: Fri, 14 Aug 2026 09:39:50 +0900 Subject: [PATCH 07/21] feat(landing/QueryLanding): give each row its own cache entry and 'staleTime' --- src/components/landing/QueryLanding.tsx | 271 +++++++++++------------- 1 file changed, 127 insertions(+), 144 deletions(-) diff --git a/src/components/landing/QueryLanding.tsx b/src/components/landing/QueryLanding.tsx index d03b6bf46..070096620 100644 --- a/src/components/landing/QueryLanding.tsx +++ b/src/components/landing/QueryLanding.tsx @@ -1,5 +1,5 @@ import * as React from 'react' -import { useMutation, useQuery, useQueryClient } from '@tanstack/react-query' +import { useMutation, useQueries, useQueryClient } from '@tanstack/react-query' import { ArrowsClockwiseIcon, EyeClosedIcon, @@ -13,50 +13,50 @@ import { usePrefersReducedMotion } from '~/utils/usePrefersReducedMotion' import { LibraryLanding, type LibraryLandingConfig } from './LibraryLanding' type QueryHeroIssue = { - id: string observers: number priority: number - title: string -} - -type QueryHeroSnapshot = { - fetchedAt: number revision: number - rows: Array + title: string } type QueryHeroMutationContext = { - previous?: QueryHeroSnapshot + previous?: QueryHeroIssue } -const queryHeroKey = ['landing-query-hero'] as const - -const queryHeroStaleTime = 3200 - -const queryHeroInitialRows: Array = [ - { id: 'router-cache', observers: 3, priority: 98, title: 'Router dashboard' }, - { id: 'project-detail', observers: 2, priority: 91, title: 'Project detail' }, +// Each row is its own cache entry with its own `staleTime`, so the three +// gauges drain at different rates and go stale independently. +const queryHeroRows = [ + { + id: 'router-cache', + staleTime: 2000, + refetchInterval: 3000, + seed: { + observers: 3, + priority: 98, + revision: 0, + title: 'Router dashboard', + }, + }, + { + id: 'project-detail', + staleTime: 6000, + refetchInterval: 7000, + seed: { observers: 2, priority: 91, revision: 0, title: 'Project detail' }, + }, { id: 'offline-queue', - observers: 1, - priority: 84, - title: 'Offline mutation queue', + staleTime: 14000, + refetchInterval: 15000, + seed: { + observers: 1, + priority: 84, + revision: 0, + title: 'Offline mutation queue', + }, }, -] +] as const -const queryHeroInitialSnapshot: QueryHeroSnapshot = { - // `0` keeps `Date.now()` out of the first render so SSR and hydration agree. - fetchedAt: 0, - revision: 0, - rows: queryHeroInitialRows, -} - -const queryHeroMutationTitles = [ - 'Optimistic table edit', - 'Search filter sync', - 'Background retry lane', - 'Prefetched route data', -] +const queryHeroKey = (id: string) => ['issues', id] as const function waitForQueryHero(ms: number) { return new Promise((resolve) => { @@ -182,111 +182,112 @@ export default function QueryLanding() { function QueryCachePanel() { const prefersReducedMotion = usePrefersReducedMotion() const queryClient = useQueryClient() - const serverRowsRef = React.useRef(queryHeroInitialRows) - const serverRevisionRef = React.useRef(0) - const mutationSequenceRef = React.useRef(0) + // One server-side record per key, so the three queries return distinct data. + const serverRowsRef = React.useRef( + Object.fromEntries( + queryHeroRows.map((row) => [row.id, { ...row.seed }]), + ) as Record, + ) // Starts paused so the server render matches the first client render; the // effect below turns it on unless the visitor asked for reduced motion. const [isLive, setIsLive] = React.useState(false) - // `0` until the first tick, for the same first-render reason as `fetchedAt`. + // `0` until the first tick; re-renders drive the draining freshness gauges. const [now, setNow] = React.useState(0) - const [selectedId, setSelectedId] = React.useState( - queryHeroInitialRows[0]?.id, + const [selectedId, setSelectedId] = React.useState( + queryHeroRows[0].id, ) - const projectsQuery = useQuery({ - queryKey: queryHeroKey, - queryFn: async (): Promise => { - await waitForQueryHero(620) - - return { - fetchedAt: Date.now(), - revision: serverRevisionRef.current, - rows: serverRowsRef.current, - } - }, - initialData: queryHeroInitialSnapshot, - initialDataUpdatedAt: 0, - refetchInterval: isLive ? 4200 : false, - staleTime: queryHeroStaleTime, + const rowQueries = useQueries({ + queries: queryHeroRows.map((row) => ({ + queryKey: queryHeroKey(row.id), + queryFn: async (): Promise => { + await waitForQueryHero(620) + return serverRowsRef.current[row.id]! + }, + initialData: row.seed as QueryHeroIssue, + initialDataUpdatedAt: 0, + refetchInterval: isLive ? row.refetchInterval : false, + staleTime: row.staleTime, + })), }) - const addIssueMutation = useMutation< + const bumpMutation = useMutation< QueryHeroIssue, Error, - QueryHeroIssue, + string, QueryHeroMutationContext >({ - mutationFn: async (issue) => { + mutationFn: async (id) => { await waitForQueryHero(720) - serverRevisionRef.current += 1 - serverRowsRef.current = [ - issue, - ...serverRowsRef.current.filter((row) => row.id !== issue.id), - ].slice(0, 5) + const current = serverRowsRef.current[id]! + const next = { + ...current, + priority: Math.min(99, current.priority + 1), + revision: current.revision + 1, + } + serverRowsRef.current[id] = next - return issue + return next }, - onMutate: async (issue) => { - await queryClient.cancelQueries({ queryKey: queryHeroKey }) - const previous = queryClient.getQueryData(queryHeroKey) + onMutate: async (id) => { + await queryClient.cancelQueries({ queryKey: queryHeroKey(id) }) + const previous = queryClient.getQueryData( + queryHeroKey(id), + ) - queryClient.setQueryData(queryHeroKey, (current) => ({ - fetchedAt: current?.fetchedAt ?? 0, - revision: current?.revision ?? serverRevisionRef.current, - rows: [ - issue, - ...(current?.rows ?? queryHeroInitialRows).filter( - (row) => row.id !== issue.id, - ), - ].slice(0, 5), - })) + queryClient.setQueryData(queryHeroKey(id), (current) => + current + ? { + ...current, + priority: Math.min(99, current.priority + 1), + revision: current.revision + 1, + } + : current, + ) return { previous } }, - onError: (_error, _issue, context) => { + onError: (_error, id, context) => { if (context?.previous) { - queryClient.setQueryData( - queryHeroKey, + queryClient.setQueryData( + queryHeroKey(id), context.previous, ) } }, - onSettled: () => queryClient.invalidateQueries({ queryKey: queryHeroKey }), + // Only the mutated key is invalidated; the other rows keep their own + // freshness windows. + onSettled: (_data, _error, id) => + queryClient.invalidateQueries({ queryKey: queryHeroKey(id) }), }) - const cacheState = projectsQuery.isFetching + const rows = queryHeroRows.map((row, index) => { + const query = rowQueries[index]! + const elapsed = query.dataUpdatedAt > 0 ? now - query.dataUpdatedAt : 0 + return { + ...row, + query, + issue: query.data, + // Drains from 100% to 0% across this row's own `staleTime`. + freshness: + query.dataUpdatedAt > 0 + ? Math.max( + 0, + Math.min(100, Math.round((1 - elapsed / row.staleTime) * 100)), + ) + : 100, + } + }) + const selected = rows.find((row) => row.id === selectedId) ?? rows[0]! + const cacheState = selected.query.isFetching ? 'fetching' - : projectsQuery.isStale + : selected.query.isStale ? 'stale' : 'fresh' - // The selected row can fall out of the cache once five newer issues arrive, - // so fall back to the top row rather than holding a dangling id. - const selectedRow = - projectsQuery.data.rows.find((row) => row.id === selectedId) ?? - projectsQuery.data.rows[0] const fetchedLabel = - projectsQuery.data.fetchedAt > 0 - ? `${Math.max(0, Math.round((Math.max(now, projectsQuery.data.fetchedAt) - projectsQuery.data.fetchedAt) / 1000))}s ago` + selected.query.dataUpdatedAt > 0 + ? `${Math.max(0, Math.round((Math.max(now, selected.query.dataUpdatedAt) - selected.query.dataUpdatedAt) / 1000))}s ago` : 'primed' - // Freshness drains from 100% to 0% across `staleTime`, so the gauge shows how - // much of the cache entry's fresh window is left before Query marks it stale. - const freshness = - projectsQuery.data.fetchedAt > 0 - ? Math.max( - 0, - Math.min( - 100, - Math.round( - (1 - - (Math.max(now, projectsQuery.data.fetchedAt) - - projectsQuery.data.fetchedAt) / - queryHeroStaleTime) * - 100, - ), - ), - ) - : 100 React.useEffect(() => { if (prefersReducedMotion === null) return @@ -299,22 +300,6 @@ function QueryCachePanel() { return () => clearInterval(id) }, []) - const addIssue = () => { - const nextSequence = mutationSequenceRef.current + 1 - const nextTitle = - queryHeroMutationTitles[ - (nextSequence - 1) % queryHeroMutationTitles.length - ] - - mutationSequenceRef.current = nextSequence - addIssueMutation.mutate({ - id: `optimistic-${nextSequence}`, - observers: (nextSequence % 3) + 1, - priority: 72 + ((nextSequence * 7) % 24), - title: nextTitle ?? 'Optimistic write', - }) - } - return (
@@ -343,15 +328,15 @@ function QueryCachePanel() { {cacheState} - rev {projectsQuery.data.revision} / {fetchedLabel} + rev {selected.issue.revision} / {fetchedLabel}
- {projectsQuery.data.rows.map((row) => ( + {rows.map((row) => ( @@ -398,14 +382,14 @@ function QueryCachePanel() {
@@ -427,7 +411,7 @@ function QueryCachePanel() {

{queryLanding.hero.detailTitle}

- ['issues', '{selectedRow?.id ?? 'router-cache'}'] + ['issues', '{selected.id}']

{queryLanding.hero.detailBody} @@ -436,21 +420,20 @@ function QueryCachePanel() {

{[ - { label: 'status', value: projectsQuery.status }, + { label: 'status', value: selected.query.status }, { - label: 'isFetching', - value: String(projectsQuery.isFetching), + label: 'isStale', + value: String(selected.query.isStale), }, { label: 'observers', - value: String(selectedRow?.observers ?? 0), + value: String(selected.issue.observers), }, - { label: 'priority', value: `P${selectedRow?.priority ?? 0}` }, { label: 'staleTime', - value: queryHeroStaleTime.toLocaleString('en-US'), + value: selected.staleTime.toLocaleString('en-US'), }, - { label: 'mutation', value: addIssueMutation.status }, + { label: 'mutation', value: bumpMutation.status }, ].map((fact) => (
{fact.label}
From 8585d3d5d1c537667e5e36c255785b1be3b8ad3c Mon Sep 17 00:00:00 2001 From: Wonsuk Choi Date: Fri, 14 Aug 2026 09:44:34 +0900 Subject: [PATCH 08/21] feat(landing/QueryLanding): badge each row's cache state and summarise the header --- src/components/landing/QueryLanding.tsx | 43 ++++++++++++++++--------- 1 file changed, 28 insertions(+), 15 deletions(-) diff --git a/src/components/landing/QueryLanding.tsx b/src/components/landing/QueryLanding.tsx index 070096620..cafaee267 100644 --- a/src/components/landing/QueryLanding.tsx +++ b/src/components/landing/QueryLanding.tsx @@ -58,6 +58,12 @@ const queryHeroRows = [ const queryHeroKey = (id: string) => ['issues', id] as const +const queryHeroStateClass = { + fetching: 'bg-amber-400 text-amber-950', + fresh: 'bg-emerald-500 text-emerald-950', + stale: 'bg-[var(--landing-accent)] text-[var(--landing-accent-ink)]', +} as const + function waitForQueryHero(ms: number) { return new Promise((resolve) => { setTimeout(resolve, ms) @@ -268,6 +274,11 @@ function QueryCachePanel() { ...row, query, issue: query.data, + state: query.isFetching + ? ('fetching' as const) + : query.isStale + ? ('stale' as const) + : ('fresh' as const), // Drains from 100% to 0% across this row's own `staleTime`. freshness: query.dataUpdatedAt > 0 @@ -279,11 +290,11 @@ function QueryCachePanel() { } }) const selected = rows.find((row) => row.id === selectedId) ?? rows[0]! - const cacheState = selected.query.isFetching - ? 'fetching' - : selected.query.isStale - ? 'stale' - : 'fresh' + // The header summarises all three entries; each row carries its own badge. + const freshCount = rows.filter((row) => row.state === 'fresh').length + const fetchingCount = rows.filter((row) => row.state === 'fetching').length + const cacheState = + fetchingCount > 0 ? 'fetching' : freshCount > 0 ? 'fresh' : 'stale' const fetchedLabel = selected.query.dataUpdatedAt > 0 ? `${Math.max(0, Math.round((Math.max(now, selected.query.dataUpdatedAt) - selected.query.dataUpdatedAt) / 1000))}s ago` @@ -317,18 +328,12 @@ function QueryCachePanel() {
{cacheState} - rev {selected.issue.revision} / {fetchedLabel} + {freshCount}/{rows.length} fresh
@@ -349,8 +354,15 @@ function QueryCachePanel() { {row.issue.title} - - P{row.issue.priority} + + + {row.state} + + + P{row.issue.priority} + @@ -433,6 +445,7 @@ function QueryCachePanel() { label: 'staleTime', value: selected.staleTime.toLocaleString('en-US'), }, + { label: 'updated', value: fetchedLabel }, { label: 'mutation', value: bumpMutation.status }, ].map((fact) => (
From 15f04de271e3bb834342f4c7435b063cf614a8d4 Mon Sep 17 00:00:00 2001 From: Wonsuk Choi Date: Fri, 14 Aug 2026 09:47:28 +0900 Subject: [PATCH 09/21] refactor(landing/QueryLanding): use 'useIsFetching' instead of tallying rows --- src/components/landing/QueryLanding.tsx | 12 ++++++++++-- 1 file changed, 10 insertions(+), 2 deletions(-) diff --git a/src/components/landing/QueryLanding.tsx b/src/components/landing/QueryLanding.tsx index cafaee267..489ed9041 100644 --- a/src/components/landing/QueryLanding.tsx +++ b/src/components/landing/QueryLanding.tsx @@ -1,5 +1,10 @@ import * as React from 'react' -import { useMutation, useQueries, useQueryClient } from '@tanstack/react-query' +import { + useIsFetching, + useMutation, + useQueries, + useQueryClient, +} from '@tanstack/react-query' import { ArrowsClockwiseIcon, EyeClosedIcon, @@ -217,6 +222,10 @@ function QueryCachePanel() { })), }) + // Query tracks in-flight fetches across the whole client, so the header does + // not have to tally the rows itself. + const fetchingCount = useIsFetching({ queryKey: ['issues'] }) + const bumpMutation = useMutation< QueryHeroIssue, Error, @@ -292,7 +301,6 @@ function QueryCachePanel() { const selected = rows.find((row) => row.id === selectedId) ?? rows[0]! // The header summarises all three entries; each row carries its own badge. const freshCount = rows.filter((row) => row.state === 'fresh').length - const fetchingCount = rows.filter((row) => row.state === 'fetching').length const cacheState = fetchingCount > 0 ? 'fetching' : freshCount > 0 ? 'fresh' : 'stale' const fetchedLabel = From 7391b32fe60befd733d206476655beb8c485098f Mon Sep 17 00:00:00 2001 From: Wonsuk Choi Date: Fri, 14 Aug 2026 10:14:52 +0900 Subject: [PATCH 10/21] feat(landing/QueryLanding): reject every third write so the rollback path runs --- src/components/landing/QueryLanding.tsx | 21 +++++++++++++++++++-- 1 file changed, 19 insertions(+), 2 deletions(-) diff --git a/src/components/landing/QueryLanding.tsx b/src/components/landing/QueryLanding.tsx index 489ed9041..583b4b50b 100644 --- a/src/components/landing/QueryLanding.tsx +++ b/src/components/landing/QueryLanding.tsx @@ -14,6 +14,7 @@ import { SkullIcon, } from '@phosphor-icons/react' +import { useToast } from '~/components/ToastProvider' import { usePrefersReducedMotion } from '~/utils/usePrefersReducedMotion' import { LibraryLanding, type LibraryLandingConfig } from './LibraryLanding' @@ -193,12 +194,14 @@ export default function QueryLanding() { function QueryCachePanel() { const prefersReducedMotion = usePrefersReducedMotion() const queryClient = useQueryClient() + const { notify } = useToast() // One server-side record per key, so the three queries return distinct data. const serverRowsRef = React.useRef( Object.fromEntries( queryHeroRows.map((row) => [row.id, { ...row.seed }]), ) as Record, ) + const bumpAttemptRef = React.useRef(0) // Starts paused so the server render matches the first client render; the // effect below turns it on unless the visitor asked for reduced motion. const [isLive, setIsLive] = React.useState(false) @@ -234,10 +237,18 @@ function QueryCachePanel() { >({ mutationFn: async (id) => { await waitForQueryHero(720) + // Every third write fails on purpose, so the optimistic update visibly + // rolls back instead of the rollback path being unreachable code. + bumpAttemptRef.current += 1 + if (bumpAttemptRef.current % 3 === 0) { + throw new Error('Write rejected by the server') + } + const current = serverRowsRef.current[id]! const next = { ...current, - priority: Math.min(99, current.priority + 1), + // Wraps instead of clamping so repeated bumps always visibly move. + priority: current.priority >= 99 ? 80 : current.priority + 1, revision: current.revision + 1, } serverRowsRef.current[id] = next @@ -254,7 +265,8 @@ function QueryCachePanel() { current ? { ...current, - priority: Math.min(99, current.priority + 1), + // Wraps instead of clamping so repeated bumps always visibly move. + priority: current.priority >= 99 ? 80 : current.priority + 1, revision: current.revision + 1, } : current, @@ -269,6 +281,11 @@ function QueryCachePanel() { context.previous, ) } + // A fixed id keeps repeated failures from stacking up toasts. + notify( + `Write rejected — rolled ['issues', '${id}'] back to P${context?.previous?.priority ?? ''}`, + { id: 'query-landing-rollback' }, + ) }, // Only the mutated key is invalidated; the other rows keep their own // freshness windows. From 03af0f71b74625db449480bde60a32ac78458135 Mon Sep 17 00:00:00 2001 From: Wonsuk Choi Date: Fri, 14 Aug 2026 10:17:31 +0900 Subject: [PATCH 11/21] refactor(landing/QueryLanding): seed every row at 'P0' so bumps read as a count --- src/components/landing/QueryLanding.tsx | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/src/components/landing/QueryLanding.tsx b/src/components/landing/QueryLanding.tsx index 583b4b50b..298255976 100644 --- a/src/components/landing/QueryLanding.tsx +++ b/src/components/landing/QueryLanding.tsx @@ -38,7 +38,7 @@ const queryHeroRows = [ refetchInterval: 3000, seed: { observers: 3, - priority: 98, + priority: 0, revision: 0, title: 'Router dashboard', }, @@ -47,7 +47,7 @@ const queryHeroRows = [ id: 'project-detail', staleTime: 6000, refetchInterval: 7000, - seed: { observers: 2, priority: 91, revision: 0, title: 'Project detail' }, + seed: { observers: 2, priority: 0, revision: 0, title: 'Project detail' }, }, { id: 'offline-queue', @@ -55,7 +55,7 @@ const queryHeroRows = [ refetchInterval: 15000, seed: { observers: 1, - priority: 84, + priority: 0, revision: 0, title: 'Offline mutation queue', }, @@ -248,7 +248,7 @@ function QueryCachePanel() { const next = { ...current, // Wraps instead of clamping so repeated bumps always visibly move. - priority: current.priority >= 99 ? 80 : current.priority + 1, + priority: current.priority >= 99 ? 0 : current.priority + 1, revision: current.revision + 1, } serverRowsRef.current[id] = next From f0ca6a99c7ecb0bb848ab0758d574d17bb8126ff Mon Sep 17 00:00:00 2001 From: Wonsuk Choi Date: Fri, 14 Aug 2026 10:27:48 +0900 Subject: [PATCH 12/21] fix(landing/QueryLanding): read the real observer count from the cache --- src/components/landing/QueryLanding.tsx | 26 +++++++++++-------------- 1 file changed, 11 insertions(+), 15 deletions(-) diff --git a/src/components/landing/QueryLanding.tsx b/src/components/landing/QueryLanding.tsx index 298255976..90b59ed7a 100644 --- a/src/components/landing/QueryLanding.tsx +++ b/src/components/landing/QueryLanding.tsx @@ -19,7 +19,6 @@ import { usePrefersReducedMotion } from '~/utils/usePrefersReducedMotion' import { LibraryLanding, type LibraryLandingConfig } from './LibraryLanding' type QueryHeroIssue = { - observers: number priority: number revision: number title: string @@ -36,29 +35,19 @@ const queryHeroRows = [ id: 'router-cache', staleTime: 2000, refetchInterval: 3000, - seed: { - observers: 3, - priority: 0, - revision: 0, - title: 'Router dashboard', - }, + seed: { priority: 0, revision: 0, title: 'Router dashboard' }, }, { id: 'project-detail', staleTime: 6000, refetchInterval: 7000, - seed: { observers: 2, priority: 0, revision: 0, title: 'Project detail' }, + seed: { priority: 0, revision: 0, title: 'Project detail' }, }, { id: 'offline-queue', staleTime: 14000, refetchInterval: 15000, - seed: { - observers: 1, - priority: 0, - revision: 0, - title: 'Offline mutation queue', - }, + seed: { priority: 0, revision: 0, title: 'Offline mutation queue' }, }, ] as const @@ -305,6 +294,13 @@ function QueryCachePanel() { : query.isStale ? ('stale' as const) : ('fresh' as const), + // Read from the cache rather than seeded, so it reflects the components + // actually subscribed to this key. + observers: + queryClient + .getQueryCache() + .find({ queryKey: queryHeroKey(row.id) }) + ?.getObserversCount() ?? 0, // Drains from 100% to 0% across this row's own `staleTime`. freshness: query.dataUpdatedAt > 0 @@ -464,7 +460,7 @@ function QueryCachePanel() { }, { label: 'observers', - value: String(selected.issue.observers), + value: String(selected.observers), }, { label: 'staleTime', From 8f297baee26303f97beec5da152996c4f7cbee59 Mon Sep 17 00:00:00 2001 From: Wonsuk Choi Date: Fri, 14 Aug 2026 10:31:27 +0900 Subject: [PATCH 13/21] feat(landing/QueryLanding): subscribe the detail pane to the selected key --- src/components/landing/QueryLanding.tsx | 133 +++++++++++++++--------- 1 file changed, 86 insertions(+), 47 deletions(-) diff --git a/src/components/landing/QueryLanding.tsx b/src/components/landing/QueryLanding.tsx index 90b59ed7a..967eb1b41 100644 --- a/src/components/landing/QueryLanding.tsx +++ b/src/components/landing/QueryLanding.tsx @@ -3,6 +3,7 @@ import { useIsFetching, useMutation, useQueries, + useQuery, useQueryClient, } from '@tanstack/react-query' import { @@ -65,6 +66,85 @@ function waitForQueryHero(ms: number) { }) } +/** + * Subscribes to the selected row's key rather than receiving its data as a + * prop. That is what a second component reading the same cache entry looks + * like in a real app, and it is why `observers` reads 2 for the selected row + * and 1 for the others. + */ +function QueryHeroDetail({ + id, + mutationStatus, + now, + staleTime, +}: { + id: string + mutationStatus: string + now: number + staleTime: number +}) { + const queryClient = useQueryClient() + const detailQuery = useQuery({ + queryKey: queryHeroKey(id), + // No `queryFn` — the panel owns fetching; this observer only reads. + enabled: false, + staleTime, + }) + // Read after paint: during render this observer has not registered yet, so + // the count would lag by one on the first render after `id` changes. + const [observers, setObservers] = React.useState(0) + + React.useEffect(() => { + const read = () => + setObservers( + queryClient + .getQueryCache() + .find({ queryKey: queryHeroKey(id) }) + ?.getObserversCount() ?? 0, + ) + + read() + return queryClient.getQueryCache().subscribe(read) + }, [id, queryClient]) + + const updatedLabel = + detailQuery.dataUpdatedAt > 0 + ? `${Math.max(0, Math.round((Math.max(now, detailQuery.dataUpdatedAt) - detailQuery.dataUpdatedAt) / 1000))}s ago` + : 'primed' + + return ( + <> +
+

{queryLanding.hero.detailTitle}

+

+ ['issues', '{id}'] +

+

+ {queryLanding.hero.detailBody} +

+
+ +
+ {[ + { label: 'status', value: detailQuery.status }, + { label: 'isStale', value: String(detailQuery.isStale) }, + { label: 'observers', value: String(observers) }, + { label: 'staleTime', value: staleTime.toLocaleString('en-US') }, + { label: 'updated', value: updatedLabel }, + { label: 'mutation', value: mutationStatus }, + ].map((fact) => ( +
+
{fact.label}
+
+ {fact.value} +
+
+ ))} +
+ + ) +} + const queryLanding = { libraryId: 'query', headline: 'The server-state standard for modern frontend apps.', @@ -294,13 +374,6 @@ function QueryCachePanel() { : query.isStale ? ('stale' as const) : ('fresh' as const), - // Read from the cache rather than seeded, so it reflects the components - // actually subscribed to this key. - observers: - queryClient - .getQueryCache() - .find({ queryKey: queryHeroKey(row.id) }) - ?.getObserversCount() ?? 0, // Drains from 100% to 0% across this row's own `staleTime`. freshness: query.dataUpdatedAt > 0 @@ -316,10 +389,6 @@ function QueryCachePanel() { const freshCount = rows.filter((row) => row.state === 'fresh').length const cacheState = fetchingCount > 0 ? 'fetching' : freshCount > 0 ? 'fresh' : 'stale' - const fetchedLabel = - selected.query.dataUpdatedAt > 0 - ? `${Math.max(0, Math.round((Math.max(now, selected.query.dataUpdatedAt) - selected.query.dataUpdatedAt) / 1000))}s ago` - : 'primed' React.useEffect(() => { if (prefersReducedMotion === null) return @@ -441,42 +510,12 @@ function QueryCachePanel() {
-
-

{queryLanding.hero.detailTitle}

-

- ['issues', '{selected.id}'] -

-

- {queryLanding.hero.detailBody} -

-
- -
- {[ - { label: 'status', value: selected.query.status }, - { - label: 'isStale', - value: String(selected.query.isStale), - }, - { - label: 'observers', - value: String(selected.observers), - }, - { - label: 'staleTime', - value: selected.staleTime.toLocaleString('en-US'), - }, - { label: 'updated', value: fetchedLabel }, - { label: 'mutation', value: bumpMutation.status }, - ].map((fact) => ( -
-
{fact.label}
-
- {fact.value} -
-
- ))} -
+
From a989cfb762a62e879f80f40225e90fdc6c118eff Mon Sep 17 00:00:00 2001 From: Wonsuk Choi Date: Fri, 14 Aug 2026 10:32:40 +0900 Subject: [PATCH 14/21] Revert "feat(landing/QueryLanding): subscribe the detail pane to the selected key" This reverts commit 8f297baee26303f97beec5da152996c4f7cbee59. --- src/components/landing/QueryLanding.tsx | 133 +++++++++--------------- 1 file changed, 47 insertions(+), 86 deletions(-) diff --git a/src/components/landing/QueryLanding.tsx b/src/components/landing/QueryLanding.tsx index 967eb1b41..90b59ed7a 100644 --- a/src/components/landing/QueryLanding.tsx +++ b/src/components/landing/QueryLanding.tsx @@ -3,7 +3,6 @@ import { useIsFetching, useMutation, useQueries, - useQuery, useQueryClient, } from '@tanstack/react-query' import { @@ -66,85 +65,6 @@ function waitForQueryHero(ms: number) { }) } -/** - * Subscribes to the selected row's key rather than receiving its data as a - * prop. That is what a second component reading the same cache entry looks - * like in a real app, and it is why `observers` reads 2 for the selected row - * and 1 for the others. - */ -function QueryHeroDetail({ - id, - mutationStatus, - now, - staleTime, -}: { - id: string - mutationStatus: string - now: number - staleTime: number -}) { - const queryClient = useQueryClient() - const detailQuery = useQuery({ - queryKey: queryHeroKey(id), - // No `queryFn` — the panel owns fetching; this observer only reads. - enabled: false, - staleTime, - }) - // Read after paint: during render this observer has not registered yet, so - // the count would lag by one on the first render after `id` changes. - const [observers, setObservers] = React.useState(0) - - React.useEffect(() => { - const read = () => - setObservers( - queryClient - .getQueryCache() - .find({ queryKey: queryHeroKey(id) }) - ?.getObserversCount() ?? 0, - ) - - read() - return queryClient.getQueryCache().subscribe(read) - }, [id, queryClient]) - - const updatedLabel = - detailQuery.dataUpdatedAt > 0 - ? `${Math.max(0, Math.round((Math.max(now, detailQuery.dataUpdatedAt) - detailQuery.dataUpdatedAt) / 1000))}s ago` - : 'primed' - - return ( - <> -
-

{queryLanding.hero.detailTitle}

-

- ['issues', '{id}'] -

-

- {queryLanding.hero.detailBody} -

-
- -
- {[ - { label: 'status', value: detailQuery.status }, - { label: 'isStale', value: String(detailQuery.isStale) }, - { label: 'observers', value: String(observers) }, - { label: 'staleTime', value: staleTime.toLocaleString('en-US') }, - { label: 'updated', value: updatedLabel }, - { label: 'mutation', value: mutationStatus }, - ].map((fact) => ( -
-
{fact.label}
-
- {fact.value} -
-
- ))} -
- - ) -} - const queryLanding = { libraryId: 'query', headline: 'The server-state standard for modern frontend apps.', @@ -374,6 +294,13 @@ function QueryCachePanel() { : query.isStale ? ('stale' as const) : ('fresh' as const), + // Read from the cache rather than seeded, so it reflects the components + // actually subscribed to this key. + observers: + queryClient + .getQueryCache() + .find({ queryKey: queryHeroKey(row.id) }) + ?.getObserversCount() ?? 0, // Drains from 100% to 0% across this row's own `staleTime`. freshness: query.dataUpdatedAt > 0 @@ -389,6 +316,10 @@ function QueryCachePanel() { const freshCount = rows.filter((row) => row.state === 'fresh').length const cacheState = fetchingCount > 0 ? 'fetching' : freshCount > 0 ? 'fresh' : 'stale' + const fetchedLabel = + selected.query.dataUpdatedAt > 0 + ? `${Math.max(0, Math.round((Math.max(now, selected.query.dataUpdatedAt) - selected.query.dataUpdatedAt) / 1000))}s ago` + : 'primed' React.useEffect(() => { if (prefersReducedMotion === null) return @@ -510,12 +441,42 @@ function QueryCachePanel() { - +
+

{queryLanding.hero.detailTitle}

+

+ ['issues', '{selected.id}'] +

+

+ {queryLanding.hero.detailBody} +

+
+ +
+ {[ + { label: 'status', value: selected.query.status }, + { + label: 'isStale', + value: String(selected.query.isStale), + }, + { + label: 'observers', + value: String(selected.observers), + }, + { + label: 'staleTime', + value: selected.staleTime.toLocaleString('en-US'), + }, + { label: 'updated', value: fetchedLabel }, + { label: 'mutation', value: bumpMutation.status }, + ].map((fact) => ( +
+
{fact.label}
+
+ {fact.value} +
+
+ ))} +
From d5d48ec160bbcd3135cadff1778627b22e704027 Mon Sep 17 00:00:00 2001 From: Wonsuk Choi Date: Fri, 14 Aug 2026 10:41:07 +0900 Subject: [PATCH 15/21] fix(landing/QueryLanding): name the rolled-back values in the toast --- src/components/landing/QueryLanding.tsx | 29 +++++++++++++++---------- 1 file changed, 17 insertions(+), 12 deletions(-) diff --git a/src/components/landing/QueryLanding.tsx b/src/components/landing/QueryLanding.tsx index 90b59ed7a..9612c6da6 100644 --- a/src/components/landing/QueryLanding.tsx +++ b/src/components/landing/QueryLanding.tsx @@ -25,6 +25,7 @@ type QueryHeroIssue = { } type QueryHeroMutationContext = { + optimistic?: QueryHeroIssue previous?: QueryHeroIssue } @@ -250,18 +251,20 @@ function QueryCachePanel() { queryHeroKey(id), ) - queryClient.setQueryData(queryHeroKey(id), (current) => - current - ? { - ...current, - // Wraps instead of clamping so repeated bumps always visibly move. - priority: current.priority >= 99 ? 80 : current.priority + 1, - revision: current.revision + 1, - } - : current, - ) + const optimistic = previous + ? { + ...previous, + // Wraps instead of clamping so repeated bumps always visibly move. + priority: previous.priority >= 99 ? 0 : previous.priority + 1, + revision: previous.revision + 1, + } + : undefined + + if (optimistic) { + queryClient.setQueryData(queryHeroKey(id), optimistic) + } - return { previous } + return { optimistic, previous } }, onError: (_error, id, context) => { if (context?.previous) { @@ -272,7 +275,9 @@ function QueryCachePanel() { } // A fixed id keeps repeated failures from stacking up toasts. notify( - `Write rejected — rolled ['issues', '${id}'] back to P${context?.previous?.priority ?? ''}`, + context?.previous && context.optimistic + ? `Demo: every third write fails. Rolled ['issues', '${id}'] back from P${context.optimistic.priority} to P${context.previous.priority}.` + : `Demo: every third write fails. Rolled ['issues', '${id}'] back.`, { id: 'query-landing-rollback' }, ) }, From e13159bc5692874c1d8274331b9e765c78ebc2c2 Mon Sep 17 00:00:00 2001 From: Wonsuk Choi Date: Fri, 14 Aug 2026 10:46:42 +0900 Subject: [PATCH 16/21] refactor(landing/QueryLanding): drop type assertions in favor of declared types --- src/components/landing/QueryLanding.tsx | 35 ++++++++++++++++--------- 1 file changed, 22 insertions(+), 13 deletions(-) diff --git a/src/components/landing/QueryLanding.tsx b/src/components/landing/QueryLanding.tsx index 9612c6da6..49f7b37f4 100644 --- a/src/components/landing/QueryLanding.tsx +++ b/src/components/landing/QueryLanding.tsx @@ -50,15 +50,30 @@ const queryHeroRows = [ refetchInterval: 15000, seed: { priority: 0, revision: 0, title: 'Offline mutation queue' }, }, -] as const +] satisfies ReadonlyArray<{ + id: string + refetchInterval: number + seed: QueryHeroIssue + staleTime: number +}> const queryHeroKey = (id: string) => ['issues', id] as const -const queryHeroStateClass = { +type QueryHeroState = 'fetching' | 'fresh' | 'stale' + +const queryHeroStateClass: Record = { fetching: 'bg-amber-400 text-amber-950', fresh: 'bg-emerald-500 text-emerald-950', stale: 'bg-[var(--landing-accent)] text-[var(--landing-accent-ink)]', -} as const +} + +function queryHeroState(query: { + isFetching: boolean + isStale: boolean +}): QueryHeroState { + if (query.isFetching) return 'fetching' + return query.isStale ? 'stale' : 'fresh' +} function waitForQueryHero(ms: number) { return new Promise((resolve) => { @@ -186,10 +201,8 @@ function QueryCachePanel() { const queryClient = useQueryClient() const { notify } = useToast() // One server-side record per key, so the three queries return distinct data. - const serverRowsRef = React.useRef( - Object.fromEntries( - queryHeroRows.map((row) => [row.id, { ...row.seed }]), - ) as Record, + const serverRowsRef = React.useRef>( + Object.fromEntries(queryHeroRows.map((row) => [row.id, { ...row.seed }])), ) const bumpAttemptRef = React.useRef(0) // Starts paused so the server render matches the first client render; the @@ -208,7 +221,7 @@ function QueryCachePanel() { await waitForQueryHero(620) return serverRowsRef.current[row.id]! }, - initialData: row.seed as QueryHeroIssue, + initialData: row.seed, initialDataUpdatedAt: 0, refetchInterval: isLive ? row.refetchInterval : false, staleTime: row.staleTime, @@ -294,11 +307,7 @@ function QueryCachePanel() { ...row, query, issue: query.data, - state: query.isFetching - ? ('fetching' as const) - : query.isStale - ? ('stale' as const) - : ('fresh' as const), + state: queryHeroState(query), // Read from the cache rather than seeded, so it reflects the components // actually subscribed to this key. observers: From 52ae1c88f61d283385c4dc932eeb8e376e82d2d6 Mon Sep 17 00:00:00 2001 From: Wonsuk Choi Date: Fri, 14 Aug 2026 16:54:30 +0900 Subject: [PATCH 17/21] fix(landing/QueryLanding): add 'fetchStatus' row to the detail pane --- src/components/landing/QueryLanding.tsx | 1 + 1 file changed, 1 insertion(+) diff --git a/src/components/landing/QueryLanding.tsx b/src/components/landing/QueryLanding.tsx index 49f7b37f4..2b2d6c35c 100644 --- a/src/components/landing/QueryLanding.tsx +++ b/src/components/landing/QueryLanding.tsx @@ -468,6 +468,7 @@ function QueryCachePanel() {
{[ { label: 'status', value: selected.query.status }, + { label: 'fetchStatus', value: selected.query.fetchStatus }, { label: 'isStale', value: String(selected.query.isStale), From bd0ba07e32e447362cc18f625f9682f510bbe0c1 Mon Sep 17 00:00:00 2001 From: Wonsuk Choi Date: Fri, 14 Aug 2026 16:59:14 +0900 Subject: [PATCH 18/21] fix(landing/QueryLanding): align the row list and detail pane to the same baseline --- src/components/landing/QueryLanding.tsx | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/src/components/landing/QueryLanding.tsx b/src/components/landing/QueryLanding.tsx index 2b2d6c35c..3eceb7542 100644 --- a/src/components/landing/QueryLanding.tsx +++ b/src/components/landing/QueryLanding.tsx @@ -360,8 +360,8 @@ function QueryCachePanel() {
-
-
+
+
@@ -377,7 +377,7 @@ function QueryCachePanel() { key={row.id} type="button" aria-pressed={row.id === selected.id} - className="block w-full rounded-lg border border-transparent bg-background-subtle p-4 text-left transition-colors hover:border-text-primary/10 focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-[var(--landing-accent-bright)] aria-pressed:border-[color:rgb(var(--landing-glow)/0.42)] aria-pressed:bg-[color:rgb(var(--landing-glow)/0.1)]" + className="flex w-full flex-1 flex-col justify-between rounded-lg border border-transparent bg-background-subtle p-4 text-left transition-colors hover:border-text-primary/10 focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-[var(--landing-accent-bright)] aria-pressed:border-[color:rgb(var(--landing-glow)/0.42)] aria-pressed:bg-[color:rgb(var(--landing-glow)/0.1)]" onClick={() => setSelectedId(row.id)} > @@ -465,7 +465,7 @@ function QueryCachePanel() {

-
+
{[ { label: 'status', value: selected.query.status }, { label: 'fetchStatus', value: selected.query.fetchStatus }, From 975f82ddaeffe4c94bd69cbcd721fddc32747034 Mon Sep 17 00:00:00 2001 From: Wonsuk Choi Date: Fri, 14 Aug 2026 17:04:55 +0900 Subject: [PATCH 19/21] refactor(landing/QueryLanding): drop layout classes that no longer affect the panel --- src/components/landing/QueryLanding.tsx | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/components/landing/QueryLanding.tsx b/src/components/landing/QueryLanding.tsx index 3eceb7542..48be9e93a 100644 --- a/src/components/landing/QueryLanding.tsx +++ b/src/components/landing/QueryLanding.tsx @@ -359,7 +359,7 @@ function QueryCachePanel() {
-
+
setSelectedId(row.id)} > From 74d0e68c68ea824d8c643be9e2a27b3af5e9e9d0 Mon Sep 17 00:00:00 2001 From: Wonsuk Choi Date: Fri, 14 Aug 2026 17:31:02 +0900 Subject: [PATCH 20/21] fix(landing/QueryLanding): let the panel columns shrink below their content width --- src/components/landing/QueryLanding.tsx | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/components/landing/QueryLanding.tsx b/src/components/landing/QueryLanding.tsx index 48be9e93a..863216221 100644 --- a/src/components/landing/QueryLanding.tsx +++ b/src/components/landing/QueryLanding.tsx @@ -360,7 +360,7 @@ function QueryCachePanel() {
-
+
-
+