fix(desktop): suppress fresh focus-return query herd

Gate the seven query families measured refetching together on focus so only
results older than five minutes catch up. Keep their normal stale times,
focused polling, and live invalidation behavior unchanged.

Co-authored-by: Carl <c7ebe626f000404285d3686e1dc74cc07cc60a9754a150041ba132e14bd3e2ec@buzz.block.builderlab.xyz>
Signed-off-by: Wes <wesbillman@users.noreply.github.com>
This commit is contained in:
Wes
2026-08-11 11:19:36 -06:00
parent d3ec831e0c
commit b78a469eb2
8 changed files with 172 additions and 12 deletions

View File

@@ -19,6 +19,7 @@ import {
import { updateCachedChannelMemberDisplayName } from "@/features/channels/channelMemberProfileCache";
import { evictUsersBatchEntries } from "@/features/profile/hooks";
import {
refetchOnFocusWhenOld,
useAppFocused,
useFocusedRefetchInterval,
} from "@/shared/lib/useDocumentVisible";
@@ -340,7 +341,7 @@ export function useRelayAgentsQuery(options?: { enabled?: boolean }) {
// reconcile (kinds PERSONA/TEAM/MANAGED_AGENT), never for kind:10100. So we
// keep polling but at a relaxed cadence and pause it while backgrounded.
refetchInterval,
refetchOnWindowFocus: true,
refetchOnWindowFocus: refetchOnFocusWhenOld,
enabled: options?.enabled,
});
}
@@ -364,7 +365,7 @@ export function useManagedAgentsQuery(options?: { enabled?: boolean }) {
? 5_000
: false;
},
refetchOnWindowFocus: true,
refetchOnWindowFocus: refetchOnFocusWhenOld,
});
}

View File

@@ -7,7 +7,10 @@ import {
} from "@/features/agents/lib/personaCatalogRelay";
import { invalidatePersonaEditCaches } from "@/features/agents/lib/personaEditCaches";
import { relayClient } from "@/shared/api/relayClient";
import { useFocusedRefetchInterval } from "@/shared/lib/useDocumentVisible";
import {
refetchOnFocusWhenOld,
useFocusedRefetchInterval,
} from "@/shared/lib/useDocumentVisible";
import {
setPersonaShared,
updatePersonaAndPublish,
@@ -27,7 +30,7 @@ export function usePersonaCatalogQuery(communityId: string | null) {
queryFn: fetchPersonaCatalogPublications,
staleTime: 30_000,
refetchInterval,
refetchOnWindowFocus: true,
refetchOnWindowFocus: refetchOnFocusWhenOld,
});
}

View File

@@ -1,6 +1,9 @@
import { useMutation, useQuery, useQueryClient } from "@tanstack/react-query";
import { useFocusedRefetchInterval } from "@/shared/lib/useDocumentVisible";
import {
refetchOnFocusWhenOld,
useFocusedRefetchInterval,
} from "@/shared/lib/useDocumentVisible";
import {
createChannelTemplate,
@@ -25,7 +28,7 @@ export function useChannelTemplatesQuery() {
queryFn: listChannelTemplates,
staleTime: 30_000,
refetchInterval,
refetchOnWindowFocus: true,
refetchOnWindowFocus: refetchOnFocusWhenOld,
});
}

View File

@@ -10,7 +10,10 @@ import {
setCustomEmoji,
} from "@/shared/api/customEmoji";
import { relayClient } from "@/shared/api/relayClient";
import { useFocusedRefetchInterval } from "@/shared/lib/useDocumentVisible";
import {
refetchOnFocusWhenOld,
useFocusedRefetchInterval,
} from "@/shared/lib/useDocumentVisible";
import type { CustomEmoji } from "@/shared/lib/remarkCustomEmoji";
/**
@@ -38,7 +41,7 @@ export function useCustomEmojiQuery() {
// but poll every 2 minutes as a backstop for any missed live event.
staleTime: 60_000,
refetchInterval,
refetchOnWindowFocus: true,
refetchOnWindowFocus: refetchOnFocusWhenOld,
});
}

View File

@@ -7,7 +7,10 @@ import { useRelayConnection } from "@/shared/api/useRelayConnection";
import { getOsIdleSeconds } from "@/shared/api/osIdle";
import { getPresence } from "@/shared/api/tauri";
import { normalizePubkey } from "@/shared/lib/pubkey";
import { useFocusedRefetchInterval } from "@/shared/lib/useDocumentVisible";
import {
refetchOnFocusWhenOld,
useFocusedRefetchInterval,
} from "@/shared/lib/useDocumentVisible";
import {
mergePresenceUpdate,
parseLivePresenceEvent,
@@ -95,7 +98,7 @@ export function usePresenceQuery(
// (crashed clients). WS events handle the fast path. Pause on degraded
// connections — HTTP presence calls fail anyway and consume relay quota.
refetchInterval,
refetchOnWindowFocus: true,
refetchOnWindowFocus: refetchOnFocusWhenOld,
});
}

View File

@@ -8,7 +8,10 @@ import type {
UserStatusLookup,
} from "@/shared/api/types";
import { normalizePubkey } from "@/shared/lib/pubkey";
import { useFocusedRefetchInterval } from "@/shared/lib/useDocumentVisible";
import {
refetchOnFocusWhenOld,
useFocusedRefetchInterval,
} from "@/shared/lib/useDocumentVisible";
import { KIND_USER_STATUS } from "@/shared/constants/kinds";
function normalizePubkeys(pubkeys: string[]) {
@@ -78,7 +81,7 @@ export function useUserStatusQuery(pubkeys: string[]) {
},
staleTime: 60_000,
refetchInterval,
refetchOnWindowFocus: true,
refetchOnWindowFocus: refetchOnFocusWhenOld,
});
}

View File

@@ -0,0 +1,126 @@
import assert from "node:assert/strict";
import { afterEach, test } from "node:test";
import {
focusManager,
QueryClient,
QueryObserver,
} from "@tanstack/react-query";
import {
FOCUS_RETURN_STALE_TIME_MS,
refetchOnFocusWhenOld,
shouldRefetchOnFocus,
} from "./useDocumentVisible.ts";
const NOW = 1_800_000_000_000;
afterEach(() => {
focusManager.setFocused(undefined);
});
async function focusRefetchCount({
ageMs,
status = "success",
dataUpdatedAt = Date.now() - ageMs,
}) {
focusManager.setFocused(false);
const queryClient = new QueryClient({
defaultOptions: { queries: { retry: false } },
});
queryClient.mount();
const queryKey = ["focus-return-policy", ageMs, status, dataUpdatedAt];
queryClient.setQueryData(queryKey, "cached", { updatedAt: dataUpdatedAt });
if (status !== "success") {
queryClient.getQueryCache().find({ queryKey }).setState({ status });
}
let fetchCount = 0;
const observer = new QueryObserver(queryClient, {
queryKey,
queryFn: async () => {
fetchCount += 1;
return "refetched";
},
refetchOnMount: false,
refetchOnWindowFocus: refetchOnFocusWhenOld,
staleTime: 5_000,
});
const unsubscribe = observer.subscribe(() => {});
focusManager.setFocused(true);
await new Promise((resolve) => setImmediate(resolve));
unsubscribe();
queryClient.unmount();
return fetchCount;
}
test("focus return keeps recently fetched stale queries cached", async () => {
assert.equal(
await focusRefetchCount({ ageMs: FOCUS_RETURN_STALE_TIME_MS - 1_000 }),
0,
);
});
test("focus return catches up genuinely old queries", async () => {
assert.equal(
await focusRefetchCount({ ageMs: FOCUS_RETURN_STALE_TIME_MS + 1 }),
1,
);
});
test("focus return fetches a missing result", () => {
assert.equal(
shouldRefetchOnFocus({ status: "pending", dataUpdatedAt: 0 }, NOW),
"always",
);
});
test("focus return retries an error with recent retained data", async () => {
assert.equal(
shouldRefetchOnFocus({ status: "error", dataUpdatedAt: NOW - 10_000 }, NOW),
"always",
);
assert.equal(await focusRefetchCount({ ageMs: 10_000, status: "error" }), 1);
});
test("focus return fetches after the clock moves backward", async () => {
assert.equal(
shouldRefetchOnFocus(
{ status: "success", dataUpdatedAt: NOW + 1_000 },
NOW,
),
"always",
);
assert.equal(
await focusRefetchCount({
ageMs: -1_000,
dataUpdatedAt: Date.now() + 1_000,
}),
1,
);
});
test("focus return age-gates successful results at the exact boundary", () => {
assert.equal(
shouldRefetchOnFocus(
{
status: "success",
dataUpdatedAt: NOW - FOCUS_RETURN_STALE_TIME_MS + 1,
},
NOW,
),
false,
);
assert.equal(
shouldRefetchOnFocus(
{
status: "success",
dataUpdatedAt: NOW - FOCUS_RETURN_STALE_TIME_MS,
},
NOW,
),
true,
);
});

View File

@@ -1,5 +1,23 @@
import * as React from "react";
export const FOCUS_RETURN_STALE_TIME_MS = 5 * 60_000;
export function shouldRefetchOnFocus(
state: { dataUpdatedAt: number; status: string },
now = Date.now(),
): boolean | "always" {
if (state.status !== "success" || state.dataUpdatedAt <= 0) return "always";
const ageMs = now - state.dataUpdatedAt;
if (ageMs < 0) return "always";
return ageMs >= FOCUS_RETURN_STALE_TIME_MS;
}
export function refetchOnFocusWhenOld(query: {
state: { dataUpdatedAt: number; status: string };
}): boolean | "always" {
return shouldRefetchOnFocus(query.state);
}
export function isDocumentVisible(): boolean {
if (typeof document === "undefined") return true;
return document.visibilityState === "visible";