1- import { type ReactNode , useEffect , useEffectEvent , useRef , useState } from "react" ;
1+ import { type ReactNode , type RefObject , useEffect , useEffectEvent , useRef } from "react" ;
22
33import { type SlowRpcAckRequest , useSlowRpcAckRequests } from "../rpc/requestLatencyState" ;
44import {
@@ -53,6 +53,8 @@ function describeExhaustedToast(): string {
5353 return "Retries exhausted trying to reconnect" ;
5454}
5555
56+ type ThreadToastId = ReturnType < typeof toastManager . add > ;
57+
5658function 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+
93159function 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
148214export 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