mirror of
https://github.com/block/buzz.git
synced 2026-08-18 06:50:31 +02:00
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:
@@ -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,
|
||||
);
|
||||
});
|
||||
}
|
||||
@@ -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
|
||||
|
||||
Reference in New Issue
Block a user