fix(desktop): suppress fresh focus-return refetches for channels and home-feed (#5535)

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 <wesbillman@users.noreply.github.com>
Co-authored-by: Meeseeks <2e96988f190ed1bd3c568760103aa4cadb2bc6195b832e252c984392c89039bd@buzz.block.builderlab.xyz>
This commit is contained in:
Wes
2026-08-11 09:07:39 -07:00
committed by GitHub
co-authored by Meeseeks
parent 240cdd3ea1
commit 4935724494
3 changed files with 107 additions and 4 deletions
+8 -2
View File
@@ -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,
});
@@ -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,
);
});
}
+9 -2
View File
@@ -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