@@ -8,7 +8,6 @@ export interface SocketTransportOptions {
88 onConnect ?: ( ) => void | Promise < void >
99 onDisconnect ?: ( reason : string ) => void
1010 onReconnect ?: ( attemptNumber : number ) => void | Promise < void >
11- onError ?: ( error : Error ) => void
1211 logger ?: {
1312 log : ( message : string , ...args : unknown [ ] ) => void
1413 error : ( message : string , ...args : unknown [ ] ) => void
@@ -23,7 +22,6 @@ export interface SocketTransportOptions {
2322export class SocketTransport {
2423 private socket : Socket | null = null
2524 private connectionState : ConnectionState = ConnectionState . DISCONNECTED
26- private retryAttempt : number = 0
2725 private retryTimeout : NodeJS . Timeout | null = null
2826 private hasConnectedOnce : boolean = false
2927
@@ -45,6 +43,9 @@ export class SocketTransport {
4543 }
4644 }
4745
46+ // This is the initial connnect attempt. We need to implement our own
47+ // infinite retry mechanism since Socket.io's automatic reconnection only
48+ // kicks in after a successful initial connection.
4849 public async connect ( ) : Promise < void > {
4950 if ( this . connectionState === ConnectionState . CONNECTED ) {
5051 console . log ( `[SocketTransport] Already connected` )
@@ -56,49 +57,25 @@ export class SocketTransport {
5657 return
5758 }
5859
59- // Start connection attempt without blocking.
60- this . startConnectionAttempt ( )
61- }
62-
63- private async startConnectionAttempt ( ) {
64- this . retryAttempt = 0
65-
66- try {
67- await this . connectWithRetry ( )
68- } catch ( error ) {
69- console . error (
70- `[SocketTransport] Unexpected error in connection loop: ${ error instanceof Error ? error . message : String ( error ) } ` ,
71- )
72- }
73- }
74-
75- private async connectWithRetry ( ) : Promise < void > {
60+ let attempt = 0
7661 let delay = this . retryConfig . initialDelay
7762
78- while ( this . retryAttempt < this . retryConfig . maxInitialAttempts ) {
79- try {
80- this . connectionState = this . retryAttempt === 0 ? ConnectionState . CONNECTING : ConnectionState . RETRYING
81-
82- console . log ( `[SocketTransport] Connection attempt ${ this . retryAttempt + 1 } ` )
83-
84- await this . connectSocket ( )
63+ while ( attempt < this . retryConfig . maxInitialAttempts ) {
64+ console . log ( `[SocketTransport] Initial connect attempt ${ attempt + 1 } ` )
65+ this . connectionState = attempt === 0 ? ConnectionState . CONNECTING : ConnectionState . RETRYING
8566
67+ try {
68+ await this . _connect ( )
8669 console . log ( `[SocketTransport] Connected to ${ this . options . url } ` )
87-
8870 this . connectionState = ConnectionState . CONNECTED
89- this . retryAttempt = 0
90-
91- this . clearRetryTimeouts ( )
9271
9372 if ( this . options . onConnect ) {
9473 await this . options . onConnect ( )
9574 }
9675
97- return
98- } catch ( error ) {
99- this . retryAttempt ++
100-
101- console . error ( `[SocketTransport] Connection attempt ${ this . retryAttempt } failed:` , error )
76+ break
77+ } catch ( _error ) {
78+ attempt ++
10279
10380 if ( this . socket ) {
10481 this . socket . disconnect ( )
@@ -107,40 +84,55 @@ export class SocketTransport {
10784
10885 console . log ( `[SocketTransport] Waiting ${ delay } ms before retry...` )
10986
110- await this . delay ( delay )
87+ const promise = new Promise ( ( resolve ) => {
88+ this . retryTimeout = setTimeout ( resolve , delay )
89+ } )
90+
91+ await promise
11192
11293 delay = Math . min ( delay * this . retryConfig . backoffMultiplier , this . retryConfig . maxDelay )
11394 }
11495 }
96+
97+ if ( this . retryTimeout ) {
98+ clearTimeout ( this . retryTimeout )
99+ this . retryTimeout = null
100+ }
101+
102+ if ( this . connectionState === ConnectionState . CONNECTED ) {
103+ console . log ( `[SocketTransport] Connected to ${ this . options . url } ` )
104+ } else {
105+ this . connectionState = ConnectionState . FAILED
106+ console . error ( `[SocketTransport] Failed to connect to ${ this . options . url } , giving up` )
107+ }
115108 }
116109
117- private async connectSocket ( ) : Promise < void > {
110+ private async _connect ( ) : Promise < void > {
118111 return new Promise ( ( resolve , reject ) => {
119112 this . socket = io ( this . options . url , this . options . socketOptions )
120113
121- const connectionTimeout = setTimeout ( ( ) => {
122- console . error ( `[SocketTransport] Connection timeout ` )
114+ let connectionTimeout : NodeJS . Timeout | null = setTimeout ( ( ) => {
115+ console . error ( `[SocketTransport] failed to connect after ${ this . CONNECTION_TIMEOUT } ms ` )
123116
124117 if ( this . connectionState !== ConnectionState . CONNECTED ) {
125118 this . socket ?. disconnect ( )
126119 reject ( new Error ( "Connection timeout" ) )
127120 }
128121 } , this . CONNECTION_TIMEOUT )
129122
123+ // https://socket.io/docs/v4/client-api/#event-connect
130124 this . socket . on ( "connect" , async ( ) => {
131- clearTimeout ( connectionTimeout )
125+ console . log ( `[SocketTransport] on(connect)` )
132126
133- const isReconnection = this . hasConnectedOnce
134-
135- // If this is a reconnection (not the first connect), treat it as a
136- // reconnect. This handles server restarts where 'reconnect' event might not fire.
137- if ( isReconnection ) {
138- console . log ( `[SocketTransport] Treating connect as reconnection (server may have restarted)` )
127+ if ( connectionTimeout ) {
128+ clearTimeout ( connectionTimeout )
129+ connectionTimeout = null
130+ }
139131
132+ if ( this . hasConnectedOnce ) {
140133 this . connectionState = ConnectionState . CONNECTED
141134
142135 if ( this . options . onReconnect ) {
143- // Call onReconnect to re-register instance.
144136 await this . options . onReconnect ( 0 )
145137 }
146138 }
@@ -149,9 +141,19 @@ export class SocketTransport {
149141 resolve ( )
150142 } )
151143
152- this . socket . on ( "disconnect" , ( reason : string ) => {
153- console . log ( `[SocketTransport] Disconnected (reason: ${ reason } )` )
144+ // https://socket.io/docs/v4/client-api/#event-connect_error
145+ this . socket . on ( "connect_error" , ( error ) => {
146+ if ( connectionTimeout && this . connectionState !== ConnectionState . CONNECTED ) {
147+ console . error ( `[SocketTransport] on(connect_error): ${ error . message } ` )
148+ clearTimeout ( connectionTimeout )
149+ connectionTimeout = null
150+ reject ( error )
151+ }
152+ } )
154153
154+ // https://socket.io/docs/v4/client-api/#event-disconnect
155+ this . socket . on ( "disconnect" , ( reason , details ) => {
156+ console . log ( `[SocketTransport] on(disconnect) (reason: ${ reason } , details: ${ JSON . stringify ( details ) } )` )
155157 this . connectionState = ConnectionState . DISCONNECTED
156158
157159 if ( this . options . onDisconnect ) {
@@ -162,77 +164,82 @@ export class SocketTransport {
162164 const isManualDisconnect = reason === "io client disconnect"
163165
164166 if ( ! isManualDisconnect && this . hasConnectedOnce ) {
165- // After successful initial connection, rely entirely on Socket.IO's
166- // reconnection.
167- console . log ( `[SocketTransport] Socket.IO will handle reconnection (reason: ${ reason } )` )
167+ // After successful initial connection, rely entirely on
168+ // Socket.IO's reconnection logic.
169+ console . log ( "[SocketTransport] will attempt to reconnect" )
170+ } else {
171+ console . log ( "[SocketTransport] will *NOT* attempt to reconnect" )
168172 }
169173 } )
170174
171- // Listen for reconnection attempts.
172- this . socket . on ( "reconnect_attempt" , ( attemptNumber : number ) => {
173- console . log ( `[SocketTransport] Socket.IO reconnect attempt:` , {
174- attemptNumber,
175- } )
176- } )
175+ // https://socket.io/docs/v4/client-api/#event-error
176+ // Fired upon a connection error.
177+ this . socket . io . on ( "error" , ( error ) => {
178+ // Connection error.
179+ if ( connectionTimeout && this . connectionState !== ConnectionState . CONNECTED ) {
180+ console . error ( `[SocketTransport] on(error): ${ error . message } ` )
181+ clearTimeout ( connectionTimeout )
182+ connectionTimeout = null
183+ reject ( error )
184+ }
177185
178- this . socket . on ( "reconnect" , ( attemptNumber : number ) => {
179- console . log ( `[SocketTransport] Socket reconnected (attempt: ${ attemptNumber } )` )
186+ // Post-connection error.
187+ if ( this . connectionState === ConnectionState . CONNECTED ) {
188+ console . error ( `[SocketTransport] on(error): ${ error . message } ` )
189+ }
190+ } )
180191
192+ // https://socket.io/docs/v4/client-api/#event-reconnect
193+ // Fired upon a successful reconnection.
194+ this . socket . io . on ( "reconnect" , ( attempt ) => {
195+ console . log ( `[SocketTransport] on(reconnect) - ${ attempt } ` )
181196 this . connectionState = ConnectionState . CONNECTED
182197
183198 if ( this . options . onReconnect ) {
184- this . options . onReconnect ( attemptNumber )
199+ this . options . onReconnect ( attempt )
185200 }
186201 } )
187202
188- this . socket . on ( "reconnect_error" , ( error : Error ) => {
189- console . error ( `[SocketTransport] Socket.IO reconnect error:` , error )
203+ // https://socket.io/docs/v4/client-api/#event-reconnect_attempt
204+ // Fired upon an attempt to reconnect.
205+ this . socket . io . on ( "reconnect_attempt" , ( attempt ) => {
206+ console . log ( `[SocketTransport] on(reconnect_attempt) - ${ attempt } ` )
190207 } )
191208
192- this . socket . on ( "reconnect_failed" , ( ) => {
193- console . error ( `[SocketTransport] Socket.IO reconnection failed after all attempts` )
209+ // https://socket.io/docs/v4/client-api/#event-reconnect_error
210+ // Fired upon a reconnection attempt error.
211+ this . socket . io . on ( "reconnect_error" , ( error ) => {
212+ console . error ( `[SocketTransport] on(reconnect_error): ${ error . message } ` )
213+ } )
194214
195- this . connectionState = ConnectionState . RETRYING
215+ // https://socket.io/docs/v4/client-api/#event-reconnect_failed
216+ // Fired when couldn't reconnect within `reconnectionAttempts`.
217+ // Since we use infinite retries, this should never fire.
218+ this . socket . io . on ( "reconnect_failed" , ( ) => {
219+ console . error ( `[SocketTransport] on(reconnect_failed) - giving up` )
220+ this . connectionState = ConnectionState . FAILED
196221 } )
197222
198- this . socket . on ( "error" , ( error ) => {
199- console . error ( `[SocketTransport] Socket error:` , error )
223+ // This is a custom event fired by the server.
224+ this . socket . on ( "auth_error" , ( error ) => {
225+ console . error ( `[SocketTransport] on (auth_error):` , error )
200226
201- if ( this . connectionState !== ConnectionState . CONNECTED ) {
227+ if ( connectionTimeout && this . connectionState !== ConnectionState . CONNECTED ) {
202228 clearTimeout ( connectionTimeout )
203- reject ( error )
204- }
205-
206- if ( this . options . onError ) {
207- this . options . onError ( error )
229+ connectionTimeout = null
230+ reject ( new Error ( error . message || "Authentication failed" ) )
208231 }
209232 } )
210-
211- this . socket . on ( "auth_error" , ( error ) => {
212- console . error ( `[SocketTransport] Authentication error:` , error )
213- clearTimeout ( connectionTimeout )
214- reject ( new Error ( error . message || "Authentication failed" ) )
215- } )
216233 } )
217234 }
218235
219- private delay ( ms : number ) : Promise < void > {
220- return new Promise ( ( resolve ) => {
221- this . retryTimeout = setTimeout ( resolve , ms )
222- } )
223- }
236+ public async disconnect ( ) : Promise < void > {
237+ console . log ( `[SocketTransport] Disconnecting...` )
224238
225- private clearRetryTimeouts ( ) {
226239 if ( this . retryTimeout ) {
227240 clearTimeout ( this . retryTimeout )
228241 this . retryTimeout = null
229242 }
230- }
231-
232- public async disconnect ( ) : Promise < void > {
233- console . log ( `[SocketTransport] Disconnecting...` )
234-
235- this . clearRetryTimeouts ( )
236243
237244 if ( this . socket ) {
238245 this . socket . removeAllListeners ( )
@@ -241,7 +248,6 @@ export class SocketTransport {
241248 }
242249
243250 this . connectionState = ConnectionState . DISCONNECTED
244-
245251 console . log ( `[SocketTransport] Disconnected` )
246252 }
247253
@@ -258,15 +264,14 @@ export class SocketTransport {
258264 }
259265
260266 public async reconnect ( ) : Promise < void > {
267+ console . log ( `[SocketTransport] Manually reconnecting...` )
268+
261269 if ( this . connectionState === ConnectionState . CONNECTED ) {
262270 console . log ( `[SocketTransport] Already connected` )
263271 return
264272 }
265273
266- console . log ( `[SocketTransport] Manual reconnection requested` )
267-
268274 this . hasConnectedOnce = false
269-
270275 await this . disconnect ( )
271276 await this . connect ( )
272277 }
0 commit comments