@@ -16,11 +16,17 @@ export const getReactotronAppId = () => {
1616 return reactotronAppId
1717}
1818
19+ let reconnectAttempts = 0
20+ let reconnectTimeout : NodeJS . Timeout | null = null
21+ const MAX_RECONNECT_ATTEMPTS = 10
22+ const INITIAL_RECONNECT_DELAY = 1000 // 1 second
23+
1924/**
2025 * Connects to the reactotron-core-server via websocket.
2126 *
2227 * Populates the following global state:
2328 * - isConnected: boolean
29+ * - connectionStatus: string (Connecting, Connected, Disconnected, Retrying...)
2430 * - error: Error | null
2531 * - clientIds: string[]
2632 * - timelineItems: TimelineItem[]
@@ -30,6 +36,7 @@ export const getReactotronAppId = () => {
3036export function connectToServer ( props : { port : number } = { port : 9292 } ) : UnsubscribeFn {
3137 const reactotronAppId = getReactotronAppId ( )
3238 const [ _c , setIsConnected ] = withGlobal ( "isConnected" , false )
39+ const [ _cs , setConnectionStatus ] = withGlobal < string > ( "connectionStatus" , "Connecting..." )
3340 const [ _e , setError ] = withGlobal < Error | null > ( "error" , null )
3441 const [ clientIds , setClientIds ] = withGlobal < string [ ] > ( "clientIds" , [ ] )
3542 const [ , setActiveClientId ] = withGlobal ( "activeClientId" , "" )
@@ -38,11 +45,36 @@ export function connectToServer(props: { port: number } = { port: 9292 }): Unsub
3845 [ clientId : string ] : StateSubscription [ ]
3946 } > ( "stateSubscriptionsByClientId" , { } )
4047
41- ws . socket = new WebSocket ( `ws://localhost:${ props . port } ` )
42- if ( ! ws . socket ) throw new Error ( "Failed to connect to Reactotron server" )
48+ // Clear any existing reconnect timeout
49+ if ( reconnectTimeout ) {
50+ clearTimeout ( reconnectTimeout )
51+ reconnectTimeout = null
52+ }
53+
54+ setConnectionStatus ( "Connecting..." )
55+ setError ( null )
56+
57+ try {
58+ ws . socket = new WebSocket ( `ws://localhost:${ props . port } ` )
59+ } catch ( error ) {
60+ setError ( error as Error )
61+ setConnectionStatus ( "Failed to connect" )
62+ scheduleReconnect ( props )
63+ return ( ) => { }
64+ }
65+
66+ if ( ! ws . socket ) {
67+ setError ( new Error ( "Failed to create WebSocket" ) )
68+ setConnectionStatus ( "Failed to connect" )
69+ scheduleReconnect ( props )
70+ return ( ) => { }
71+ }
4372
4473 // Tell the server we are a Reactotron app, not a React client.
4574 ws . socket . onopen = ( ) => {
75+ reconnectAttempts = 0 // Reset on successful connection
76+ setConnectionStatus ( "Connected" )
77+ setError ( null )
4678 ws . socket ?. send (
4779 JSON . stringify ( {
4880 type : "reactotron.subscribe" ,
@@ -54,16 +86,25 @@ export function connectToServer(props: { port: number } = { port: 9292 }): Unsub
5486 }
5587
5688 // Handle errors
57- ws . socket . onerror = ( event ) => setError ( new Error ( `WebSocket error: ${ event . message } ` ) )
89+ ws . socket . onerror = ( event ) => {
90+ const errorMsg = event . message || "Connection failed"
91+ setError ( new Error ( `WebSocket error: ${ errorMsg } ` ) )
92+ setConnectionStatus ( `Error: ${ errorMsg } ` )
93+ }
5894
5995 // Handle messages coming from the server, intended to be sent to the client or Reactotron app.
6096 ws . socket . onmessage = ( event ) => {
6197 const data = JSON . parse ( event . data )
98+ console . tron . log ( "Received message from server:" , data . type )
6299
63- if ( data . type === "reactotron.connected" ) setIsConnected ( true )
100+ if ( data . type === "reactotron.connected" ) {
101+ console . tron . log ( "Reactotron app marked as connected" )
102+ setIsConnected ( true )
103+ }
64104
65105 if ( data . type === "connectionEstablished" ) {
66106 const clientId = data ?. conn ?. clientId
107+ console . tron . log ( "connectionEstablished event for client:" , clientId )
67108 if ( ! clientIds . includes ( clientId ) ) {
68109 setClientIds ( ( prev ) => [ ...prev , clientId ] )
69110 setActiveClientId ( clientId )
@@ -75,6 +116,12 @@ export function connectToServer(props: { port: number } = { port: 9292 }): Unsub
75116 }
76117
77118 if ( data . type === "connectedClients" ) {
119+ console . tron . log (
120+ "connectedClients event. Count:" ,
121+ data . clients ?. length ,
122+ "Clients:" ,
123+ data . clients ,
124+ )
78125 let newestClientId = ""
79126 data . clients . forEach ( ( client : any ) => {
80127 // Store the client data in global state
@@ -89,6 +136,7 @@ export function connectToServer(props: { port: number } = { port: 9292 }): Unsub
89136
90137 if ( newestClientId ) {
91138 // Set the active client to the newest client
139+ console . tron . log ( "Setting active client to:" , newestClientId )
92140 setActiveClientId ( newestClientId )
93141 }
94142 }
@@ -159,8 +207,9 @@ export function connectToServer(props: { port: number } = { port: 9292 }): Unsub
159207 }
160208
161209 // Clean up after disconnect
162- ws . socket . onclose = ( ) => {
163- console . tron . log ( "Reactotron server disconnected" )
210+ ws . socket . onclose = ( event ) => {
211+ console . tron . log ( "Reactotron server disconnected" , event . code , event . reason )
212+
164213 // Clear individual client data
165214 clientIds . forEach ( ( clientId ) => {
166215 const [ _ , setClientData ] = withGlobal ( `client-${ clientId } ` , { } )
@@ -172,6 +221,14 @@ export function connectToServer(props: { port: number } = { port: 9292 }): Unsub
172221 setActiveClientId ( "" )
173222 setTimelineItems ( [ ] )
174223 setStateSubscriptionsByClientId ( { } )
224+
225+ // Only attempt reconnect if it wasn't a normal close
226+ if ( event . code !== 1000 ) {
227+ setConnectionStatus ( "Disconnected" )
228+ scheduleReconnect ( props )
229+ } else {
230+ setConnectionStatus ( "Disconnected" )
231+ }
175232 }
176233
177234 // Send a message to the server (which will be forwarded to the client)
@@ -181,9 +238,47 @@ export function connectToServer(props: { port: number } = { port: 9292 }): Unsub
181238 }
182239
183240 return ( ) => {
184- ws . socket ?. close ( )
241+ if ( reconnectTimeout ) {
242+ clearTimeout ( reconnectTimeout )
243+ reconnectTimeout = null
244+ }
245+ reconnectAttempts = 0
246+ ws . socket ?. close ( 1000 ) // Normal closure
247+ ws . socket = null
248+ }
249+ }
250+
251+ function scheduleReconnect ( props : { port : number } ) {
252+ if ( reconnectAttempts >= MAX_RECONNECT_ATTEMPTS ) {
253+ const [ _ , setConnectionStatus ] = withGlobal < string > ( "connectionStatus" , "" )
254+ setConnectionStatus ( `Failed after ${ MAX_RECONNECT_ATTEMPTS } attempts` )
255+ return
256+ }
257+
258+ reconnectAttempts ++
259+ const delay = Math . min ( INITIAL_RECONNECT_DELAY * Math . pow ( 2 , reconnectAttempts - 1 ) , 30000 )
260+ const [ _ , setConnectionStatus ] = withGlobal < string > ( "connectionStatus" , "" )
261+ setConnectionStatus (
262+ `Retrying in ${ Math . round ( delay / 1000 ) } s... (${ reconnectAttempts } /${ MAX_RECONNECT_ATTEMPTS } )` ,
263+ )
264+
265+ reconnectTimeout = setTimeout ( ( ) => {
266+ console . tron . log ( `Reconnecting... attempt ${ reconnectAttempts } ` )
267+ connectToServer ( props )
268+ } , delay )
269+ }
270+
271+ export function manualReconnect ( ) {
272+ reconnectAttempts = 0
273+ if ( reconnectTimeout ) {
274+ clearTimeout ( reconnectTimeout )
275+ reconnectTimeout = null
276+ }
277+ if ( ws . socket ) {
278+ ws . socket . close ( 1000 )
185279 ws . socket = null
186280 }
281+ connectToServer ( )
187282}
188283
189284export function sendToClient ( message : string | object , payload ?: object , clientId ?: string ) {
0 commit comments