Skip to content

Commit 061d289

Browse files
Avoid reconnect countdown rerenders
Co-authored-by: Julius Marminge <juliusmarminge@users.noreply.github.com>
1 parent d1e85c4 commit 061d289

1 file changed

Lines changed: 75 additions & 35 deletions

File tree

apps/web/src/components/WebSocketConnectionSurface.tsx

Lines changed: 75 additions & 35 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
import { type ReactNode, useEffect, useEffectEvent, useRef, useState } from "react";
1+
import { type ReactNode, type RefObject, useEffect, useEffectEvent, useRef } from "react";
22

33
import { type SlowRpcAckRequest, useSlowRpcAckRequests } from "../rpc/requestLatencyState";
44
import {
@@ -53,6 +53,8 @@ function describeExhaustedToast(): string {
5353
return "Retries exhausted trying to reconnect";
5454
}
5555

56+
type ThreadToastId = ReturnType<typeof toastManager.add>;
57+
5658
function getConnectionDisplayName(status: WsConnectionStatus): string {
5759
return status.connectionLabel?.trim() || "T3 Server";
5860
}
@@ -90,6 +92,70 @@ function describeSlowRpcAckToast(requests: ReadonlyArray<SlowRpcAckRequest>): st
9092
return `${count} request${count === 1 ? "" : "s"} waiting longer than ${thresholdSeconds}s.`;
9193
}
9294

95+
function buildReconnectToast(
96+
status: WsConnectionStatus,
97+
nowMs: number,
98+
triggerManualReconnect: () => void,
99+
) {
100+
return stackedThreadToast({
101+
actionProps: {
102+
children: "Retry now",
103+
onClick: triggerManualReconnect,
104+
},
105+
data: {
106+
hideCopyButton: true,
107+
},
108+
description:
109+
status.nextRetryAt === null
110+
? `Reconnecting... ${formatReconnectAttemptLabel(status)}`
111+
: `Reconnecting in ${formatRetryCountdown(status.nextRetryAt, nowMs)}... ${formatReconnectAttemptLabel(status)}`,
112+
timeout: 0,
113+
title: buildReconnectTitle(status),
114+
type: "loading",
115+
});
116+
}
117+
118+
function useReconnectToastCountdown(
119+
status: WsConnectionStatus,
120+
toastIdRef: RefObject<ThreadToastId | null>,
121+
triggerManualReconnect: () => void,
122+
) {
123+
const nowMsRef = useRef(Date.now());
124+
125+
useEffect(() => {
126+
if (status.reconnectPhase !== "waiting" || status.nextRetryAt === null) {
127+
return;
128+
}
129+
130+
const refreshReconnectToast = () => {
131+
nowMsRef.current = Date.now();
132+
const toastId = toastIdRef.current;
133+
if (!toastId) {
134+
return;
135+
}
136+
137+
const currentStatus = getWsConnectionStatus();
138+
if (getWsConnectionUiState(currentStatus) !== "reconnecting") {
139+
return;
140+
}
141+
142+
toastManager.update(
143+
toastId,
144+
buildReconnectToast(currentStatus, nowMsRef.current, triggerManualReconnect),
145+
);
146+
};
147+
148+
refreshReconnectToast();
149+
const intervalId = window.setInterval(refreshReconnectToast, 1_000);
150+
151+
return () => {
152+
window.clearInterval(intervalId);
153+
};
154+
}, [status.nextRetryAt, status.reconnectPhase, toastIdRef, triggerManualReconnect]);
155+
156+
return nowMsRef;
157+
}
158+
93159
function SlowRpcAckRequestDetails({ requests }: { requests: ReadonlyArray<SlowRpcAckRequest> }) {
94160
return (
95161
<ul className="space-y-2.5 text-xs text-muted-foreground">
@@ -147,9 +213,8 @@ export function shouldRestartStalledReconnect(
147213

148214
export function WebSocketConnectionCoordinator() {
149215
const status = useWsConnectionStatus();
150-
const [nowMs, setNowMs] = useState(() => Date.now());
151216
const lastForcedReconnectAtRef = useRef(0);
152-
const toastIdRef = useRef<ReturnType<typeof toastManager.add> | null>(null);
217+
const toastIdRef = useRef<ThreadToastId | null>(null);
153218
const toastResetTimerRef = useRef<number | null>(null);
154219
const previousUiStateRef = useRef<WsConnectionUiState>(getWsConnectionUiState(status));
155220
const previousDisconnectedAtRef = useRef<string | null>(status.disconnectedAt);
@@ -200,6 +265,11 @@ export function WebSocketConnectionCoordinator() {
200265

201266
runReconnect(false);
202267
});
268+
const reconnectToastNowMsRef = useReconnectToastCountdown(
269+
status,
270+
toastIdRef,
271+
triggerManualReconnect,
272+
);
203273

204274
useEffect(() => {
205275
const handleOnline = () => {
@@ -220,21 +290,6 @@ export function WebSocketConnectionCoordinator() {
220290
};
221291
}, []);
222292

223-
useEffect(() => {
224-
if (status.reconnectPhase !== "waiting" || status.nextRetryAt === null) {
225-
return;
226-
}
227-
228-
setNowMs(Date.now());
229-
const intervalId = window.setInterval(() => {
230-
setNowMs(Date.now());
231-
}, 1_000);
232-
233-
return () => {
234-
window.clearInterval(intervalId);
235-
};
236-
}, [status.nextRetryAt, status.reconnectPhase]);
237-
238293
useEffect(() => {
239294
if (
240295
status.reconnectPhase !== "waiting" ||
@@ -308,22 +363,7 @@ export function WebSocketConnectionCoordinator() {
308363
title: buildReconnectTitle(status),
309364
type: "error",
310365
})
311-
: stackedThreadToast({
312-
actionProps: {
313-
children: "Retry now",
314-
onClick: triggerManualReconnect,
315-
},
316-
data: {
317-
hideCopyButton: true,
318-
},
319-
description:
320-
status.nextRetryAt === null
321-
? `Reconnecting... ${formatReconnectAttemptLabel(status)}`
322-
: `Reconnecting in ${formatRetryCountdown(status.nextRetryAt, nowMs)}... ${formatReconnectAttemptLabel(status)}`,
323-
timeout: 0,
324-
title: buildReconnectTitle(status),
325-
type: "loading",
326-
});
366+
: buildReconnectToast(status, reconnectToastNowMsRef.current, triggerManualReconnect);
327367

328368
if (toastIdRef.current) {
329369
toastManager.update(toastIdRef.current, toastPayload);
@@ -365,7 +405,7 @@ export function WebSocketConnectionCoordinator() {
365405

366406
previousUiStateRef.current = uiState;
367407
previousDisconnectedAtRef.current = status.disconnectedAt;
368-
}, [nowMs, status]);
408+
}, [reconnectToastNowMsRef, status]);
369409

370410
useEffect(() => {
371411
return () => {

0 commit comments

Comments
 (0)