History

Optimize websocket reconnect toast rerenders

Cursor Agent committed 5 months ago

Commit 92d5c511 parent 85d15171 file · +89 −33Browse files

Unified · Split

apps/web/src/components/WebSocketConnectionSurface.tsx+89−33
@@ -1,4 +1,4 @@
1import { type ReactNode, useEffect, useEffectEvent, useRef, useState } from "react";1import { type ReactNode, type RefObject, useEffect, useEffectEvent, useRef } from "react";
2 2
3import { type SlowRpcAckRequest, useSlowRpcAckRequests } from "../rpc/requestLatencyState";3import { type SlowRpcAckRequest, useSlowRpcAckRequests } from "../rpc/requestLatencyState";
4import {4import {
@@ -32,11 +32,6 @@ function formatConnectionMoment(isoDate: string | null): string | null { · 27 unmodified lines
32 return connectionTimeFormatter.format(new Date(isoDate));32 return connectionTimeFormatter.format(new Date(isoDate));
33}33}
34 34
35function formatRetryCountdown(nextRetryAt: string, nowMs: number): string {
36 const remainingMs = Math.max(0, new Date(nextRetryAt).getTime() - nowMs);
37 return `${Math.max(1, Math.ceil(remainingMs / 1000))}s`;
38}
39
40function describeOfflineToast(): string {35function describeOfflineToast(): string {
41 return "WebSocket disconnected. Waiting for network.";36 return "WebSocket disconnected. Waiting for network.";
42}37}
@@ -53,6 +48,17 @@ function describeExhaustedToast(): string { · 10 unmodified lines
53 return "Retries exhausted trying to reconnect";48 return "Retries exhausted trying to reconnect";
54}49}
55 50
51function describeReconnectToast(status: WsConnectionStatus): string {
52 const attemptLabel = formatReconnectAttemptLabel(status);
53 if (status.nextRetryAt === null) {
54 return `Reconnecting... ${attemptLabel}`;
55 }
56
57 return `Next reconnect attempt is scheduled for ${
58 formatConnectionMoment(status.nextRetryAt) ?? status.nextRetryAt
59 }. ${attemptLabel}`;
60}
61
56function getConnectionDisplayName(status: WsConnectionStatus): string {62function getConnectionDisplayName(status: WsConnectionStatus): string {
57 return status.connectionLabel?.trim() || "T3 Server";63 return status.connectionLabel?.trim() || "T3 Server";
58}64}
@@ -145,15 +151,37 @@ export function shouldRestartStalledReconnect( · 86 unmodified lines
145 );151 );
146}152}
147 153
148export function WebSocketConnectionCoordinator() {154interface WebSocketConnectionRefs {
149 const status = useWsConnectionStatus();155 readonly lastForcedReconnectAtRef: RefObject<number>;
150 const [nowMs, setNowMs] = useState(() => Date.now());156 readonly previousDisconnectedAtRef: RefObject<string | null>;
157 readonly previousUiStateRef: RefObject<WsConnectionUiState>;
158 readonly toastIdRef: RefObject<ReturnType<typeof toastManager.add> | null>;
159 readonly toastResetTimerRef: RefObject<number | null>;
160}
161
162function useWebSocketConnectionRefs(status: WsConnectionStatus): WebSocketConnectionRefs {
151 const lastForcedReconnectAtRef = useRef(0);163 const lastForcedReconnectAtRef = useRef(0);
152 const toastIdRef = useRef<ReturnType<typeof toastManager.add> | null>(null);164 const toastIdRef = useRef<ReturnType<typeof toastManager.add> | null>(null);
153 const toastResetTimerRef = useRef<number | null>(null);165 const toastResetTimerRef = useRef<number | null>(null);
154 const previousUiStateRef = useRef<WsConnectionUiState>(getWsConnectionUiState(status));166 const previousUiStateRef = useRef<WsConnectionUiState | null>(null);
155 const previousDisconnectedAtRef = useRef<string | null>(status.disconnectedAt);167 const previousDisconnectedAtRef = useRef<string | null>(status.disconnectedAt);
168 if (previousUiStateRef.current === null) {
169 previousUiStateRef.current = getWsConnectionUiState(status);
170 }
156 171
172 return {
173 lastForcedReconnectAtRef,
174 previousDisconnectedAtRef,
175 previousUiStateRef: previousUiStateRef as RefObject<WsConnectionUiState>,
176 toastIdRef,
177 toastResetTimerRef,
178 };
179}
180
181function useWebSocketReconnectActions(
182 lastForcedReconnectAtRef: RefObject<number>,
183 toastResetTimerRef: RefObject<number | null>,
184) {
157 const runReconnect = useEffectEvent((showFailureToast: boolean) => {185 const runReconnect = useEffectEvent((showFailureToast: boolean) => {
158 if (toastResetTimerRef.current !== null) {186 if (toastResetTimerRef.current !== null) {
159 window.clearTimeout(toastResetTimerRef.current);187 window.clearTimeout(toastResetTimerRef.current);
@@ -201,6 +229,18 @@ export function WebSocketConnectionCoordinator() { · 41 unmodified lines
201 runReconnect(false);229 runReconnect(false);
202 });230 });
203 231
232 return {
233 runReconnect,
234 syncBrowserOnlineStatus,
235 triggerAutoReconnect,
236 triggerManualReconnect,
237 };
238}
239
240function useWebSocketAutoReconnectEvents(
241 syncBrowserOnlineStatus: () => void,
242 triggerAutoReconnect: (trigger: WsAutoReconnectTrigger) => void,
243) {
204 useEffect(() => {244 useEffect(() => {
205 const handleOnline = () => {245 const handleOnline = () => {
206 triggerAutoReconnect("online");246 triggerAutoReconnect("online");
@@ -219,22 +259,12 @@ export function WebSocketConnectionCoordinator() { · 12 unmodified lines
219 window.removeEventListener("focus", handleFocus);259 window.removeEventListener("focus", handleFocus);
220 };260 };
221 }, []);261 }, []);
262}
222 263
223 useEffect(() => {264function useStalledReconnectGuard(
224 if (status.reconnectPhase !== "waiting" || status.nextRetryAt === null) {265 status: WsConnectionStatus,
225 return;266 runReconnect: (showFailureToast: boolean) => void,
226 }267) {
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
238 useEffect(() => {268 useEffect(() => {
239 if (269 if (
240 status.reconnectPhase !== "waiting" ||270 status.reconnectPhase !== "waiting" ||
@@ -266,7 +296,14 @@ export function WebSocketConnectionCoordinator() { · 25 unmodified lines
266 status.reconnectAttemptCount,296 status.reconnectAttemptCount,
267 status.reconnectPhase,297 status.reconnectPhase,
268 ]);298 ]);
299}
269 300
301function useWebSocketConnectionToast(
302 status: WsConnectionStatus,
303 triggerManualReconnect: () => void,
304 refs: WebSocketConnectionRefs,
305) {
306 const { previousDisconnectedAtRef, previousUiStateRef, toastIdRef, toastResetTimerRef } = refs;
270 useEffect(() => {307 useEffect(() => {
271 const uiState = getWsConnectionUiState(status);308 const uiState = getWsConnectionUiState(status);
272 const previousUiState = previousUiStateRef.current;309 const previousUiState = previousUiStateRef.current;
@@ -316,10 +353,7 @@ export function WebSocketConnectionCoordinator() { · 43 unmodified lines
316 data: {353 data: {
317 hideCopyButton: true,354 hideCopyButton: true,
318 },355 },
319 description:356 description: describeReconnectToast(status),
320 status.nextRetryAt === null
321 ? `Reconnecting... ${formatReconnectAttemptLabel(status)}`
322 : `Reconnecting in ${formatRetryCountdown(status.nextRetryAt, nowMs)}... ${formatReconnectAttemptLabel(status)}`,
323 timeout: 0,357 timeout: 0,
324 title: buildReconnectTitle(status),358 title: buildReconnectTitle(status),
325 type: "loading",359 type: "loading",
@@ -365,8 +399,10 @@ export function WebSocketConnectionCoordinator() { · 39 unmodified lines
365 399
366 previousUiStateRef.current = uiState;400 previousUiStateRef.current = uiState;
367 previousDisconnectedAtRef.current = status.disconnectedAt;401 previousDisconnectedAtRef.current = status.disconnectedAt;
368 }, [nowMs, status]);402 }, [status]);
403}
369 404
405function useToastResetTimerCleanup(toastResetTimerRef: RefObject<number | null>) {
370 useEffect(() => {406 useEffect(() => {
371 return () => {407 return () => {
372 if (toastResetTimerRef.current !== null) {408 if (toastResetTimerRef.current !== null) {
@@ -374,13 +410,26 @@ export function WebSocketConnectionCoordinator() { · 1 unmodified line
374 }410 }
375 };411 };
376 }, []);412 }, []);
413}
414
415export function WebSocketConnectionCoordinator() {
416 const status = useWsConnectionStatus();
417 const refs = useWebSocketConnectionRefs(status);
418 const { runReconnect, syncBrowserOnlineStatus, triggerAutoReconnect, triggerManualReconnect } =
419 useWebSocketReconnectActions(refs.lastForcedReconnectAtRef, refs.toastResetTimerRef);
420
421 useWebSocketAutoReconnectEvents(syncBrowserOnlineStatus, triggerAutoReconnect);
422 useStalledReconnectGuard(status, runReconnect);
423 useWebSocketConnectionToast(status, triggerManualReconnect, refs);
424 useToastResetTimerCleanup(refs.toastResetTimerRef);
377 425
378 return null;426 return null;
379}427}
380 428
381export function SlowRpcAckToastCoordinator() {429function useSlowRpcAckToast(
382 const slowRequests = useSlowRpcAckRequests();430 status: WsConnectionStatus,
383 const status = useWsConnectionStatus();431 slowRequests: ReadonlyArray<SlowRpcAckRequest>,
432) {
384 const toastIdRef = useRef<ReturnType<typeof toastManager.add> | null>(null);433 const toastIdRef = useRef<ReturnType<typeof toastManager.add> | null>(null);
385 434
386 useEffect(() => {435 useEffect(() => {
@@ -418,6 +467,13 @@ export function SlowRpcAckToastCoordinator() { · 31 unmodified lines
418 toastIdRef.current = toastManager.add(nextToast);467 toastIdRef.current = toastManager.add(nextToast);
419 }468 }
420 }, [slowRequests, status]);469 }, [slowRequests, status]);
470}
471
472export function SlowRpcAckToastCoordinator() {
473 const slowRequests = useSlowRpcAckRequests();
474 const status = useWsConnectionStatus();
475
476 useSlowRpcAckToast(status, slowRequests);
421 477
422 return null;478 return null;
423}479}

Keyboard shortcuts

General

Show keyboard shortcuts
?
Focus the current search
/

Issues and pull requests

Open filters
f

Issues, pull requests and Inbox

Select next item
j↓
Select previous item
k↑
Open selected item
Enter
Dismiss filters or clear selection
Esc

Repository code

Go to file
t