mirror of
https://github.com/block/buzz.git
synced 2026-10-31 06:21:34 +00:00
perf(desktop): limit reconnect query invalidation to relay data
Finding L3-2: auto-heal and manual reconnect used broad queryClient.invalidateQueries(), refetching unrelated Tauri/disk/local queries after relay recovery. Route both reconnect paths through a shared relay-dependent query predicate. The taxonomy includes relay-backed messages/channels/profiles/social/workflows/projects state and excludes local-only agent/runtime/workspace-icon/repo queries. Co-authored-by: Tyler Longwell <tlongwell@block.xyz> Signed-off-by: Tyler Longwell <tlongwell@block.xyz>
This commit is contained in:
parent
9da294968c
commit
980b7e8ed9
81
desktop/src/shared/api/relayQueryInvalidation.test.mjs
Normal file
81
desktop/src/shared/api/relayQueryInvalidation.test.mjs
Normal file
@@ -0,0 +1,81 @@
|
||||
import assert from "node:assert/strict";
|
||||
import test from "node:test";
|
||||
|
||||
import { isRelayDependentQueryKey } from "./relayQueryInvalidation.ts";
|
||||
|
||||
test("relay invalidation includes relay-backed channel and profile queries", () => {
|
||||
for (const queryKey of [
|
||||
["channels"],
|
||||
["channels", "channel-1", "members"],
|
||||
["channel-messages", "channel-1"],
|
||||
["thread-replies", "channel-1", "root-1"],
|
||||
["forum-posts", "channel-1"],
|
||||
["home-feed"],
|
||||
["users-batch", "alice"],
|
||||
["presence", "alice"],
|
||||
["user-status", "alice"],
|
||||
["relay-agents"],
|
||||
["relayMembers"],
|
||||
["archivedIdentities"],
|
||||
["oaOwner", "alice"],
|
||||
]) {
|
||||
assert.equal(isRelayDependentQueryKey(queryKey), true, queryKey.join("/"));
|
||||
}
|
||||
});
|
||||
|
||||
test("relay invalidation includes social/workflow relay queries", () => {
|
||||
for (const queryKey of [
|
||||
["global-notes"],
|
||||
["liked-notes", "alice"],
|
||||
["pulse-reactions", "note-1"],
|
||||
["workflows", "channel-1"],
|
||||
["workflows-all", "channel-1"],
|
||||
["workflow-runs", "workflow-1"],
|
||||
["run-approvals", "workflow-1", "run-1"],
|
||||
["reminders", "alice"],
|
||||
["custom-emoji"],
|
||||
]) {
|
||||
assert.equal(isRelayDependentQueryKey(queryKey), true, queryKey.join("/"));
|
||||
}
|
||||
});
|
||||
|
||||
test("relay invalidation excludes local Tauri and disk-only query roots", () => {
|
||||
for (const queryKey of [
|
||||
["identity"],
|
||||
["managed-agents"],
|
||||
["personas"],
|
||||
["teams"],
|
||||
["acp-runtimes"],
|
||||
["backend-providers"],
|
||||
["managed-agent-log", "agent-1", 200],
|
||||
["workspace-icon", "wss://relay.example"],
|
||||
["agent-memory", "agent-1"],
|
||||
]) {
|
||||
assert.equal(isRelayDependentQueryKey(queryKey), false, queryKey.join("/"));
|
||||
}
|
||||
});
|
||||
|
||||
test("relay invalidation separates relay project queries from local repo work", () => {
|
||||
for (const queryKey of [
|
||||
["projects"],
|
||||
["project", "project-1"],
|
||||
["project", "project-1", "issues"],
|
||||
["project", "project-1", "pull-requests"],
|
||||
["projects", "issues", ["project-1"]],
|
||||
["projects", "activity-summaries", ["addr-1"]],
|
||||
]) {
|
||||
assert.equal(isRelayDependentQueryKey(queryKey), true, queryKey.join("/"));
|
||||
}
|
||||
|
||||
for (const queryKey of [
|
||||
["project", "project-1", "repo-state"],
|
||||
["project", "project-1", "repo-snapshot", "main"],
|
||||
["project", "project-1", "repo-diff", "main"],
|
||||
["project", "project-1", "local-repo-diff"],
|
||||
["project", "project-1", "commit-diff", "remote"],
|
||||
["projects", "local-repositories", "default"],
|
||||
["projects", "repo-snapshots", "default", ["project-1"]],
|
||||
]) {
|
||||
assert.equal(isRelayDependentQueryKey(queryKey), false, queryKey.join("/"));
|
||||
}
|
||||
});
|
||||
84
desktop/src/shared/api/relayQueryInvalidation.ts
Normal file
84
desktop/src/shared/api/relayQueryInvalidation.ts
Normal file
@@ -0,0 +1,84 @@
|
||||
const RELAY_QUERY_ROOTS = new Set<string>([
|
||||
"archivedIdentities",
|
||||
"channel-canvas",
|
||||
"channel-messages",
|
||||
"channels",
|
||||
"contact-list",
|
||||
"custom-emoji",
|
||||
"custom-emoji-own",
|
||||
"forum-posts",
|
||||
"forum-thread",
|
||||
"global-notes",
|
||||
"home-feed",
|
||||
"liked-notes",
|
||||
"my-notes",
|
||||
"myRelayMembership",
|
||||
"oaOwner",
|
||||
"presence",
|
||||
"profile",
|
||||
"pulse-note",
|
||||
"pulse-reactions",
|
||||
"pulse-timeline",
|
||||
"relay-agents",
|
||||
"relayMembers",
|
||||
"reminders",
|
||||
"run-approvals",
|
||||
"search-messages",
|
||||
"thread-replies",
|
||||
"user-profile",
|
||||
"user-search",
|
||||
"user-status",
|
||||
"users-batch",
|
||||
"workflow",
|
||||
"workflow-runs",
|
||||
"workflows",
|
||||
"workflows-all",
|
||||
]);
|
||||
|
||||
const RELAY_PROJECT_QUERY_PARTS = new Set<string>([
|
||||
"activity-summaries",
|
||||
"issues",
|
||||
"pull-requests",
|
||||
]);
|
||||
|
||||
const LOCAL_PROJECT_QUERY_PARTS = new Set<string>([
|
||||
"commit-diff",
|
||||
"local-repo-diff",
|
||||
"local-repo-snapshot",
|
||||
"local-repositories",
|
||||
"repo-diff",
|
||||
"repo-snapshot",
|
||||
"repo-state",
|
||||
"repo-sync-status",
|
||||
]);
|
||||
|
||||
function isRelayDependentProjectQueryKey(queryKey: readonly unknown[]) {
|
||||
if (queryKey[0] === "projects") {
|
||||
const scope = queryKey[1];
|
||||
if (scope === undefined) return true;
|
||||
if (typeof scope !== "string") return false;
|
||||
if (LOCAL_PROJECT_QUERY_PARTS.has(scope)) return false;
|
||||
return RELAY_PROJECT_QUERY_PARTS.has(scope);
|
||||
}
|
||||
|
||||
if (queryKey[0] === "project") {
|
||||
const scope = queryKey[2];
|
||||
if (scope === undefined) return true;
|
||||
if (typeof scope !== "string") return false;
|
||||
if (LOCAL_PROJECT_QUERY_PARTS.has(scope)) return false;
|
||||
return RELAY_PROJECT_QUERY_PARTS.has(scope);
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
export function isRelayDependentQueryKey(queryKey: readonly unknown[]) {
|
||||
const root = queryKey[0];
|
||||
if (typeof root !== "string") return false;
|
||||
if (RELAY_QUERY_ROOTS.has(root)) return true;
|
||||
return isRelayDependentProjectQueryKey(queryKey);
|
||||
}
|
||||
|
||||
export function isRelayDependentQuery(query: { queryKey: readonly unknown[] }) {
|
||||
return isRelayDependentQueryKey(query.queryKey);
|
||||
}
|
||||
@@ -16,6 +16,7 @@ import { invoke } from "@tauri-apps/api/core";
|
||||
import { toast } from "sonner";
|
||||
|
||||
import { relayClient } from "@/shared/api/relayClient";
|
||||
import { isRelayDependentQuery } from "@/shared/api/relayQueryInvalidation";
|
||||
import { relayReconnectController } from "@/shared/api/relayReconnectController";
|
||||
|
||||
function buildDeps(onSuccess: () => void, onBackstop: () => void) {
|
||||
@@ -59,12 +60,14 @@ export function useReconnectRelay(): {
|
||||
onSuccessRef.current = React.useCallback(() => {
|
||||
// Defer query invalidation so callers render the recovered state first.
|
||||
window.setTimeout(() => {
|
||||
void queryClient.invalidateQueries().catch((err) => {
|
||||
console.error(
|
||||
"[useReconnectRelay] failed to refresh queries after reconnect:",
|
||||
err,
|
||||
);
|
||||
});
|
||||
void queryClient
|
||||
.invalidateQueries({ predicate: isRelayDependentQuery })
|
||||
.catch((err) => {
|
||||
console.error(
|
||||
"[useReconnectRelay] failed to refresh queries after reconnect:",
|
||||
err,
|
||||
);
|
||||
});
|
||||
}, 0);
|
||||
}, [queryClient]);
|
||||
|
||||
|
||||
@@ -3,6 +3,7 @@ import * as React from "react";
|
||||
import { useQueryClient } from "@tanstack/react-query";
|
||||
|
||||
import type { ConnectionState } from "@/shared/api/relayClientShared";
|
||||
import { isRelayDependentQuery } from "@/shared/api/relayQueryInvalidation";
|
||||
import {
|
||||
isRelayConnectionDegraded,
|
||||
useRelayConnection,
|
||||
@@ -82,12 +83,12 @@ export class RelayAutoHealScheduler {
|
||||
|
||||
/**
|
||||
* Auto-heal: when the connection recovers from a degraded state, invalidate
|
||||
* all queries so errored queries (e.g. messages, which don't poll) refetch
|
||||
* automatically without requiring a manual reconnect action.
|
||||
* relay-dependent queries so errored queries (e.g. messages, which don't poll)
|
||||
* refetch automatically without requiring a manual reconnect action.
|
||||
*
|
||||
* Rate-limited to prevent a flappy connection (e.g. VPN toggling) from
|
||||
* firing an unfiltered invalidation — ~20-40 requests across active queries
|
||||
* with retry:1 — every time the relay briefly recovers.
|
||||
* firing a relay-wide invalidation across active queries with retry:1 every
|
||||
* time the relay briefly recovers.
|
||||
*
|
||||
* When a recovery is suppressed by the rate limiter (an earlier flap consumed
|
||||
* the budget), a deferred heal is scheduled for the remaining window so the
|
||||
@@ -101,7 +102,10 @@ export function useRelayAutoHeal(): void {
|
||||
|
||||
if (schedulerRef.current === null) {
|
||||
schedulerRef.current = new RelayAutoHealScheduler(
|
||||
() => void queryClient.invalidateQueries(),
|
||||
() =>
|
||||
void queryClient.invalidateQueries({
|
||||
predicate: isRelayDependentQuery,
|
||||
}),
|
||||
AUTO_HEAL_MIN_INTERVAL_MS,
|
||||
window.setTimeout.bind(window),
|
||||
window.clearTimeout.bind(window),
|
||||
|
||||
Reference in New Issue
Block a user