@@ -8,7 +8,6 @@ export interface SocketTransportOptions {
8
8
onConnect ?: ( ) => void | Promise < void >
9
9
onDisconnect ?: ( reason : string ) => void
10
10
onReconnect ?: ( attemptNumber : number ) => void | Promise < void >
11
- onError ?: ( error : Error ) => void
12
11
logger ?: {
13
12
log : ( message : string , ...args : unknown [ ] ) => void
14
13
error : ( message : string , ...args : unknown [ ] ) => void
@@ -23,7 +22,6 @@ export interface SocketTransportOptions {
23
22
export class SocketTransport {
24
23
private socket : Socket | null = null
25
24
private connectionState : ConnectionState = ConnectionState . DISCONNECTED
26
- private retryAttempt : number = 0
27
25
private retryTimeout : NodeJS . Timeout | null = null
28
26
private hasConnectedOnce : boolean = false
29
27
@@ -45,6 +43,9 @@ export class SocketTransport {
45
43
}
46
44
}
47
45
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.
48
49
public async connect ( ) : Promise < void > {
49
50
if ( this . connectionState === ConnectionState . CONNECTED ) {
50
51
console . log ( `[SocketTransport] Already connected` )
@@ -56,49 +57,25 @@ export class SocketTransport {
56
57
return
57
58
}
58
59
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
76
61
let delay = this . retryConfig . initialDelay
77
62
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
85
66
67
+ try {
68
+ await this . _connect ( )
86
69
console . log ( `[SocketTransport] Connected to ${ this . options . url } ` )
87
-
88
70
this . connectionState = ConnectionState . CONNECTED
89
- this . retryAttempt = 0
90
-
91
- this . clearRetryTimeouts ( )
92
71
93
72
if ( this . options . onConnect ) {
94
73
await this . options . onConnect ( )
95
74
}
96
75
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 ++
102
79
103
80
if ( this . socket ) {
104
81
this . socket . disconnect ( )
@@ -107,40 +84,55 @@ export class SocketTransport {
107
84
108
85
console . log ( `[SocketTransport] Waiting ${ delay } ms before retry...` )
109
86
110
- await this . delay ( delay )
87
+ const promise = new Promise ( ( resolve ) => {
88
+ this . retryTimeout = setTimeout ( resolve , delay )
89
+ } )
90
+
91
+ await promise
111
92
112
93
delay = Math . min ( delay * this . retryConfig . backoffMultiplier , this . retryConfig . maxDelay )
113
94
}
114
95
}
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
+ }
115
108
}
116
109
117
- private async connectSocket ( ) : Promise < void > {
110
+ private async _connect ( ) : Promise < void > {
118
111
return new Promise ( ( resolve , reject ) => {
119
112
this . socket = io ( this . options . url , this . options . socketOptions )
120
113
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 ` )
123
116
124
117
if ( this . connectionState !== ConnectionState . CONNECTED ) {
125
118
this . socket ?. disconnect ( )
126
119
reject ( new Error ( "Connection timeout" ) )
127
120
}
128
121
} , this . CONNECTION_TIMEOUT )
129
122
123
+ // https://socket.io/docs/v4/client-api/#event-connect
130
124
this . socket . on ( "connect" , async ( ) => {
131
- clearTimeout ( connectionTimeout )
125
+ console . log ( `[SocketTransport] on(connect)` )
132
126
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
+ }
139
131
132
+ if ( this . hasConnectedOnce ) {
140
133
this . connectionState = ConnectionState . CONNECTED
141
134
142
135
if ( this . options . onReconnect ) {
143
- // Call onReconnect to re-register instance.
144
136
await this . options . onReconnect ( 0 )
145
137
}
146
138
}
@@ -149,9 +141,19 @@ export class SocketTransport {
149
141
resolve ( )
150
142
} )
151
143
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
+ } )
154
153
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 ) } )` )
155
157
this . connectionState = ConnectionState . DISCONNECTED
156
158
157
159
if ( this . options . onDisconnect ) {
@@ -162,77 +164,82 @@ export class SocketTransport {
162
164
const isManualDisconnect = reason === "io client disconnect"
163
165
164
166
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" )
168
172
}
169
173
} )
170
174
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
+ }
177
185
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
+ } )
180
191
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 } ` )
181
196
this . connectionState = ConnectionState . CONNECTED
182
197
183
198
if ( this . options . onReconnect ) {
184
- this . options . onReconnect ( attemptNumber )
199
+ this . options . onReconnect ( attempt )
185
200
}
186
201
} )
187
202
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 } ` )
190
207
} )
191
208
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
+ } )
194
214
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
196
221
} )
197
222
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 )
200
226
201
- if ( this . connectionState !== ConnectionState . CONNECTED ) {
227
+ if ( connectionTimeout && this . connectionState !== ConnectionState . CONNECTED ) {
202
228
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" ) )
208
231
}
209
232
} )
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
- } )
216
233
} )
217
234
}
218
235
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...` )
224
238
225
- private clearRetryTimeouts ( ) {
226
239
if ( this . retryTimeout ) {
227
240
clearTimeout ( this . retryTimeout )
228
241
this . retryTimeout = null
229
242
}
230
- }
231
-
232
- public async disconnect ( ) : Promise < void > {
233
- console . log ( `[SocketTransport] Disconnecting...` )
234
-
235
- this . clearRetryTimeouts ( )
236
243
237
244
if ( this . socket ) {
238
245
this . socket . removeAllListeners ( )
@@ -241,7 +248,6 @@ export class SocketTransport {
241
248
}
242
249
243
250
this . connectionState = ConnectionState . DISCONNECTED
244
-
245
251
console . log ( `[SocketTransport] Disconnected` )
246
252
}
247
253
@@ -258,15 +264,14 @@ export class SocketTransport {
258
264
}
259
265
260
266
public async reconnect ( ) : Promise < void > {
267
+ console . log ( `[SocketTransport] Manually reconnecting...` )
268
+
261
269
if ( this . connectionState === ConnectionState . CONNECTED ) {
262
270
console . log ( `[SocketTransport] Already connected` )
263
271
return
264
272
}
265
273
266
- console . log ( `[SocketTransport] Manual reconnection requested` )
267
-
268
274
this . hasConnectedOnce = false
269
-
270
275
await this . disconnect ( )
271
276
await this . connect ( )
272
277
}
0 commit comments