From 49357244945c2f4b8432eb8b5cebbba5b1c30a08 Mon Sep 17 00:00:00 2001 From: Wes Date: Tue, 11 Aug 2026 10:07:39 -0600 Subject: [PATCH] fix(desktop): suppress fresh focus-return refetches for channels and home-feed (#5535) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Fixes the 0.5.9 sluggishness Wes reported in app-slowness-mac (channel list slow, content slow). ## Problem #5490 (shipped in 0.5.9) flipped ~20 query sites to `refetchOnWindowFocus: true` and wired TanStack focusManager to app focus. Instrumented at that exact commit: regaining focus after >60s away fires **7 query fetches within 2ms**, including `get_channels`, which settles at **~3.6s** (production probe: median 3.2s at 1,133 channels — 8 serial round-trips, 1,133-filter last-message batch). In 0.5.8 this burst was zero by configuration. Net: a burst of fetch/parse contention exactly when the user returns to the app. Relay ruled out: v0.2.1 small reads are 2–4ms upstream; nothing in v0.2.0..v0.2.1 degrades the query path. The O(N) `get_channels` design is a pre-existing issue (June analysis) — this PR fixes the new stampede that made it user-visible. ## Fix Raise `staleTime` to 5 minutes on the two expensive focus-refetch families — `channels` and `home-feed` — so a focus return inside that window serves cache instead of refetching. `refetchOnWindowFocus: true` only refetches stale queries, so genuinely old data still refreshes on return. Unchanged: focused polling cadence (60s channels / 30s home-feed; interval refetches ignore staleTime), #5490 blur quiescence (no changes to `useDocumentVisible.ts`/`queryClient.ts`), all push-style invalidation paths (`invalidateQueries` bypasses staleTime), and channels cold-start revalidate (`initialDataUpdatedAt: 0`). ## Validation - New regression test `desktop/src/features/home/focusRefetchPolicy.test.mjs` (4/4): fresh focus return → 0 fetches; stale → 1; polling constants locked. - Pre-push gate at the reviewed tree: desktop-check, desktop-typecheck, full desktop-test **4588/4588**. - Independent adversarial review (Beth): APPROVE at tree `4e2546ec` — verified fresh-skip/stale-refetch against query-core 5.100.14 source, polling-cadence via browser-simulated probe, side-effect sweep of all invalidation paths clean. Sole CHANGE was commit trailers, fixed by amend (tree unchanged). ## Known residual Focus returns after >5min still fire the full burst including the ~3.2–3.6s `get_channels`. This cuts stampede frequency, not magnitude — the O(N) `get_channels` relay path (RESEARCH/GET_CHANNELS_SLOWNESS.md) is the follow-up that fixes magnitude. Diagnosis: Summer (focus profiling) + Morty (relay probe); implemented by Meeseeks; reviewed by Beth; integrated by Rick. Thread: app-slowness-mac e78fad29380d9a0974c9d673910450994a228781ddce133a8cedbd90504d95be. Signed-off-by: Wes Co-authored-by: Meeseeks <2e96988f190ed1bd3c568760103aa4cadb2bc6195b832e252c984392c89039bd@buzz.block.builderlab.xyz> --- desktop/src/features/channels/hooks.ts | 10 ++- .../features/home/focusRefetchPolicy.test.mjs | 90 +++++++++++++++++++ desktop/src/features/home/hooks.ts | 11 ++- 3 files changed, 107 insertions(+), 4 deletions(-) create mode 100644 desktop/src/features/home/focusRefetchPolicy.test.mjs diff --git a/desktop/src/features/channels/hooks.ts b/desktop/src/features/channels/hooks.ts index 50475fe16..d7f8cec40 100644 --- a/desktop/src/features/channels/hooks.ts +++ b/desktop/src/features/channels/hooks.ts @@ -42,6 +42,10 @@ import { } from "@/features/channels/channelSnapshot"; export const channelsQueryKey = ["channels"] as const; +/** Keeps focused polling at the established one-minute cadence. */ +export const CHANNELS_REFETCH_INTERVAL_MS = 60_000; +/** Suppresses the expensive focus refetch until the channel list is old. */ +export const CHANNELS_FOCUS_STALE_TIME_MS = 5 * 60_000; const channelDetailQueryKey = (channelId: string) => ["channels", channelId, "detail"] as const; const channelMembersQueryKey = (channelId: string) => @@ -194,7 +198,9 @@ function setChannelArchivedState( export function useChannelsQuery(options?: { enabled?: boolean }) { const { activeCommunity } = useCommunities(); const relayUrl = activeCommunity?.relayUrl ?? null; - const refetchInterval = useFocusedRefetchInterval(60_000); + const refetchInterval = useFocusedRefetchInterval( + CHANNELS_REFETCH_INTERVAL_MS, + ); return useQuery({ enabled: options?.enabled ?? true, @@ -216,7 +222,7 @@ export function useChannelsQuery(options?: { enabled?: boolean }) { } : undefined, initialDataUpdatedAt: 0, - staleTime: 60_000, + staleTime: CHANNELS_FOCUS_STALE_TIME_MS, refetchInterval, refetchOnWindowFocus: true, }); diff --git a/desktop/src/features/home/focusRefetchPolicy.test.mjs b/desktop/src/features/home/focusRefetchPolicy.test.mjs new file mode 100644 index 000000000..d4dc7769f --- /dev/null +++ b/desktop/src/features/home/focusRefetchPolicy.test.mjs @@ -0,0 +1,90 @@ +import assert from "node:assert/strict"; +import { afterEach, test } from "node:test"; + +import { + focusManager, + QueryClient, + QueryObserver, +} from "@tanstack/react-query"; + +import { + CHANNELS_FOCUS_STALE_TIME_MS, + CHANNELS_REFETCH_INTERVAL_MS, +} from "@/features/channels/hooks.ts"; +import { + HOME_FEED_FOCUS_STALE_TIME_MS, + HOME_FEED_REFETCH_INTERVAL_MS, +} from "./hooks.ts"; + +afterEach(() => { + focusManager.setFocused(undefined); +}); + +async function focusRefetchCount({ ageMs, staleTime }) { + focusManager.setFocused(false); + const queryClient = new QueryClient({ + defaultOptions: { queries: { retry: false } }, + }); + queryClient.mount(); + + const queryKey = ["focus-refetch-policy", staleTime, ageMs]; + queryClient.setQueryData(queryKey, "cached", { + updatedAt: Date.now() - ageMs, + }); + let fetchCount = 0; + const observer = new QueryObserver(queryClient, { + queryKey, + queryFn: async () => { + fetchCount += 1; + return "refetched"; + }, + // Keep setup from fetching stale cache before the simulated focus return. + refetchOnMount: false, + refetchOnWindowFocus: true, + staleTime, + }); + const unsubscribe = observer.subscribe(() => {}); + + focusManager.setFocused(true); + await new Promise((resolve) => setImmediate(resolve)); + + unsubscribe(); + queryClient.unmount(); + return fetchCount; +} + +for (const policy of [ + { + name: "channels", + staleTime: CHANNELS_FOCUS_STALE_TIME_MS, + refetchInterval: CHANNELS_REFETCH_INTERVAL_MS, + expectedRefetchInterval: 60_000, + }, + { + name: "home feed", + staleTime: HOME_FEED_FOCUS_STALE_TIME_MS, + refetchInterval: HOME_FEED_REFETCH_INTERVAL_MS, + expectedRefetchInterval: 30_000, + }, +]) { + test(`${policy.name} skips fresh focus refetch and preserves polling`, async () => { + assert.equal(policy.refetchInterval, policy.expectedRefetchInterval); + assert.equal( + await focusRefetchCount({ + ageMs: policy.staleTime - 1_000, + staleTime: policy.staleTime, + }), + 0, + ); + }); + + test(`${policy.name} refetches genuinely stale data on focus`, async () => { + assert.equal( + await focusRefetchCount({ + ageMs: policy.staleTime + 1, + staleTime: policy.staleTime, + }), + 1, + ); + }); +} diff --git a/desktop/src/features/home/hooks.ts b/desktop/src/features/home/hooks.ts index 125dbb54d..e767e0bbd 100644 --- a/desktop/src/features/home/hooks.ts +++ b/desktop/src/features/home/hooks.ts @@ -4,10 +4,17 @@ import { getHomeFeed } from "@/shared/api/tauri"; import { useRelayConnection } from "@/shared/api/useRelayConnection"; import { useFocusedRefetchInterval } from "@/shared/lib/useDocumentVisible"; +/** Keeps focused polling at the established 30-second cadence. */ +export const HOME_FEED_REFETCH_INTERVAL_MS = 30_000; +/** Suppresses the expensive focus refetch until the home feed is old. */ +export const HOME_FEED_FOCUS_STALE_TIME_MS = 5 * 60_000; + export function useHomeFeedQuery() { const connectionState = useRelayConnection(); const connected = connectionState === "connected"; - const refetchInterval = useFocusedRefetchInterval(connected ? 30_000 : false); + const refetchInterval = useFocusedRefetchInterval( + connected ? HOME_FEED_REFETCH_INTERVAL_MS : false, + ); return useQuery({ queryKey: ["home-feed"], @@ -16,7 +23,7 @@ export function useHomeFeedQuery() { limit: 50, types: "mentions,needs_action,activity,agent_activity", }), - staleTime: 15_000, + staleTime: HOME_FEED_FOCUS_STALE_TIME_MS, gcTime: 5 * 60 * 1_000, // Pause background polling on degraded/stalled/disconnected connections. // The relay can't serve the request anyway, and the spurious failures