authorgravatar for me@t3.ggTheo Browne <me@t3.gg> 2026-08-07 02:18:34-04:00
committergravatar for noreply@github.comGitHub <noreply@github.com> 2026-08-06 23:18:34-07:00
log2288d416aa3bcb5f2eeb4004228872593296971e
tree6895c05065c9deb643b8c8494fa76bd22fa781fa
parentddfe45c66eccd93c0adf61db92a16cbbbcde23e1
signature Signed by PGP key B5690EEEBB952194

fix(web): stop the "requests are slow" warning from firing on every provider update (#5570)

Co-authored-by: Claude Opus 5 (1M context) <noreply@anthropic.com>

4 files changed, 62 insertions(+), 9 deletions(-)

apps/web/src/components/SlowRpcRequestToastCoordinator.tsx+4-1
......@@ -5,7 +5,10 @@ import { toastManager } from "./ui/toast";
55
66function describeSlowRequests(requests: ReadonlyArray<SlowRpcAckRequest>): string {
77 const count = requests.length;
8 const thresholdSeconds = Math.round((requests[0]?.thresholdMs ?? 0) / 1000);
8 // Thresholds vary per method, so report the smallest one the batch has passed.
9 const thresholdSeconds = Math.round(
10 Math.min(...requests.map((request) => request.thresholdMs)) / 1000,
11 );
912
1013 return `${count} request${count === 1 ? "" : "s"} waiting longer than ${thresholdSeconds}s.`;
1114}
apps/web/src/connection/platform.ts+1-1
......@@ -590,7 +590,7 @@ const rpcRequestObserverLayer = Layer.succeed(
590590 Effect.sync(() => {
591591 nextObservedRpcRequestId += 1;
592592 const requestId = `${environmentId}:${nextObservedRpcRequestId}`;
593 trackRpcRequestSent(requestId, `${method} · ${environmentId}`);
593 trackRpcRequestSent(requestId, method, `${method} · ${environmentId}`);
594594 return Effect.sync(() => {
595595 acknowledgeRpcRequest(requestId);
596596 });
apps/web/src/rpc/requestLatencyState.test.ts+27
......@@ -6,6 +6,7 @@ import {
66 getSlowRpcAckRequests,
77 resetRequestLatencyStateForTests,
88 trackRpcRequestSent,
9 LONG_RUNNING_RPC_ACK_THRESHOLD_MS,
910 SLOW_RPC_ACK_THRESHOLD_MS,
1011 MAX_TRACKED_RPC_ACK_REQUESTS,
1112} from "./requestLatencyState";
......@@ -58,6 +59,32 @@ describe("requestLatencyState", () => {
5859 expect(getSlowRpcAckRequests()).toEqual([]);
5960 });
6061
62 it("keeps ignoring untracked methods when a display tag is supplied", () => {
63 trackRpcRequestSent(
64 "1",
65 WS_METHODS.previewAutomationConnect,
66 `${WS_METHODS.previewAutomationConnect} · env-1`,
67 );
68 vi.advanceTimersByTime(SLOW_RPC_ACK_THRESHOLD_MS * 2);
69
70 expect(getSlowRpcAckRequests()).toEqual([]);
71 });
72
73 it("gives provider updates a longer threshold before warning", () => {
74 trackRpcRequestSent("1", WS_METHODS.serverUpdateProvider, "server.updateProvider · env-1");
75 vi.advanceTimersByTime(LONG_RUNNING_RPC_ACK_THRESHOLD_MS - 1);
76 expect(getSlowRpcAckRequests()).toEqual([]);
77
78 vi.advanceTimersByTime(1);
79 expect(getSlowRpcAckRequests()).toMatchObject([
80 {
81 requestId: "1",
82 tag: "server.updateProvider · env-1",
83 thresholdMs: LONG_RUNNING_RPC_ACK_THRESHOLD_MS,
84 },
85 ]);
86 });
87
6188 it("evicts the oldest pending requests once the tracker reaches capacity", () => {
6289 for (let index = 0; index < MAX_TRACKED_RPC_ACK_REQUESTS + 1; index += 1) {
6390 trackRpcRequestSent(String(index), "server.getConfig");
apps/web/src/rpc/requestLatencyState.ts+30-7
......@@ -5,6 +5,12 @@ import { Atom } from "effect/unstable/reactivity";
55import { appAtomRegistry } from "./atomRegistry";
66
77export const SLOW_RPC_ACK_THRESHOLD_MS = 15_000;
8/**
9 * Some requests are slow by design — they shell out to a package manager on the
10 * server and only respond once the install finishes. Warning about those after
11 * 15s is noise, so they get a much longer leash.
12 */
13export const LONG_RUNNING_RPC_ACK_THRESHOLD_MS = 120_000;
814export const MAX_TRACKED_RPC_ACK_REQUESTS = 256;
915let slowRpcAckThresholdMs = SLOW_RPC_ACK_THRESHOLD_MS;
1016
......@@ -22,7 +28,12 @@ interface PendingRpcAckRequest {
2228}
2329
2430const pendingRpcAckRequests = new Map<string, PendingRpcAckRequest>();
25const untrackedRpcAckTags = new Set<string>([WS_METHODS.previewAutomationConnect]);
31const untrackedRpcAckMethods = new Set<string>([WS_METHODS.previewAutomationConnect]);
32const longRunningRpcAckMethods = new Set<string>([
33 WS_METHODS.serverUpdateProvider,
34 WS_METHODS.serverRefreshProviders,
35 WS_METHODS.serverUpdateServer,
36]);
2637
2738const slowRpcAckRequestsAtom = Atom.make<ReadonlyArray<SlowRpcAckRequest>>([]).pipe(
2839 Atom.keepAlive,
......@@ -37,16 +48,27 @@ function getSlowRpcAckRequestsValue(): ReadonlyArray<SlowRpcAckRequest> {
3748 return appAtomRegistry.get(slowRpcAckRequestsAtom);
3849}
3950
40function shouldTrackRpcAck(tag: string): boolean {
41 return !tag.includes("subscribe") && !untrackedRpcAckTags.has(tag);
51function shouldTrackRpcAck(method: string): boolean {
52 return !method.includes("subscribe") && !untrackedRpcAckMethods.has(method);
53}
54
55function rpcAckThresholdMs(method: string): number {
56 return longRunningRpcAckMethods.has(method)
57 ? Math.max(slowRpcAckThresholdMs, LONG_RUNNING_RPC_ACK_THRESHOLD_MS)
58 : slowRpcAckThresholdMs;
4259}
4360
4461export function getSlowRpcAckRequests(): ReadonlyArray<SlowRpcAckRequest> {
4562 return getSlowRpcAckRequestsValue();
4663}
4764
48export function trackRpcRequestSent(requestId: string, tag: string): void {
49 if (!shouldTrackRpcAck(tag)) {
65/**
66 * Starts the slow-request timer for one in-flight unary RPC. `method` is the
67 * bare WS method (used to decide whether and how long to wait); `tag` is the
68 * human-readable label shown in the toast, which defaults to the method.
69 */
70export function trackRpcRequestSent(requestId: string, method: string, tag = method): void {
71 if (!shouldTrackRpcAck(method)) {
5072 return;
5173 }
5274
......@@ -54,17 +76,18 @@ export function trackRpcRequestSent(requestId: string, tag: string): void {
5476 evictOldestPendingRpcRequestIfNeeded();
5577
5678 const startedAtMs = Date.now();
79 const thresholdMs = rpcAckThresholdMs(method);
5780 const request: SlowRpcAckRequest = {
5881 requestId,
5982 startedAt: new Date(startedAtMs).toISOString(),
6083 startedAtMs,
6184 tag,
62 thresholdMs: slowRpcAckThresholdMs,
85 thresholdMs,
6386 };
6487 const timeoutId = setTimeout(() => {
6588 pendingRpcAckRequests.delete(requestId);
6689 appendSlowRpcAckRequest(request);
67 }, slowRpcAckThresholdMs);
90 }, thresholdMs);
6891
6992 pendingRpcAckRequests.set(requestId, {
7093 request,