@@ -53,6 +53,7 @@ export class ReconnectingWebSocket<
5353 #isDisposed = false ; // Permanent disposal, cannot be resumed
5454 #isConnecting = false ;
5555 #pendingReconnect = false ;
56+ #certRefreshAttempted = false ; // Tracks if cert refresh was already attempted this connection cycle
5657 readonly #onDispose?: ( ) => void ;
5758
5859 private constructor (
@@ -86,45 +87,23 @@ export class ReconnectingWebSocket<
8687 onDispose ,
8788 ) ;
8889
89- try {
90- await instance . connect ( ) ;
91- } catch ( error ) {
92- const certError = ClientCertificateError . fromError ( error ) ;
93- if ( ! certError ) {
94- throw error ;
95- }
96-
97- const refreshed = await instance . handleClientCertificateError ( certError ) ;
98- if ( refreshed ) {
99- try {
100- await instance . connect ( ) ;
101- return instance ;
102- } catch ( retryError ) {
103- // Check if still a cert error after refresh
104- const retryCertError = ClientCertificateError . fromError ( retryError ) ;
105- if ( retryCertError ) {
106- void retryCertError . showNotification ( ) ;
107- } else {
108- instance . #logger. error (
109- "Connection failed after certificate refresh" ,
110- retryError ,
111- ) ;
112- }
113- }
114- }
115-
116- // Either refresh failed or retry failed - return disconnected instance
117- instance . disconnect ( ) ;
118- return instance ;
119- }
120-
90+ // connect() handles all errors internally
91+ await instance . connect ( ) ;
12192 return instance ;
12293 }
12394
12495 get url ( ) : string {
12596 return this . #currentSocket?. url ?? "" ;
12697 }
12798
99+ /**
100+ * Returns true if the socket is temporarily disconnected and not attempting to reconnect.
101+ * Use reconnect() to resume.
102+ */
103+ get isDisconnected ( ) : boolean {
104+ return this . #isDisconnected;
105+ }
106+
128107 /**
129108 * Extract the route (pathname + search) from the current socket URL for logging.
130109 * Falls back to the last known route when the socket is closed.
@@ -160,6 +139,7 @@ export class ReconnectingWebSocket<
160139 if ( this . #isDisconnected) {
161140 this . #isDisconnected = false ;
162141 this . #backoffMs = this . #options. initialBackoffMs ;
142+ this . #certRefreshAttempted = false ; // User-initiated reconnect, allow retry
163143 }
164144
165145 if ( this . #isDisposed) {
@@ -171,14 +151,13 @@ export class ReconnectingWebSocket<
171151 this . #reconnectTimeoutId = null ;
172152 }
173153
174- // If already connecting, schedule reconnect after current attempt
175154 if ( this . #isConnecting) {
176155 this . #pendingReconnect = true ;
177156 return ;
178157 }
179158
180- // connect() will close any existing socket
181- this . connect ( ) . catch ( ( error ) => this . handleConnectionError ( error ) ) ;
159+ // connect() handles all errors internally
160+ void this . connect ( ) ;
182161 }
183162
184163 /**
@@ -239,6 +218,7 @@ export class ReconnectingWebSocket<
239218
240219 socket . addEventListener ( "open" , ( event ) => {
241220 this . #backoffMs = this . #options. initialBackoffMs ;
221+ this . #certRefreshAttempted = false ; // Reset on successful connection
242222 this . executeHandlers ( "open" , event ) ;
243223 } ) ;
244224
@@ -248,17 +228,11 @@ export class ReconnectingWebSocket<
248228
249229 socket . addEventListener ( "error" , ( event ) => {
250230 this . executeHandlers ( "error" , event ) ;
251-
252- // Check for unrecoverable HTTP errors in the error event
253- // HTTP errors during handshake fire 'error' then 'close' with 1006
254- // We need to suspend here to prevent infinite reconnect loops
255- const errorMessage = toError ( event . error , event . message ) . message ;
256- if ( this . isUnrecoverableHttpError ( errorMessage ) ) {
257- this . #logger. error (
258- `Unrecoverable HTTP error for ${ this . #route} : ${ errorMessage } ` ,
259- ) ;
260- this . disconnect ( ) ;
261- }
231+ // Errors during initial connection are caught by the factory (waitForOpen).
232+ // This handler is for errors AFTER successful connection.
233+ // Route through handleConnectionError for consistent handling.
234+ const error = toError ( event . error , event . message ) ;
235+ void this . handleConnectionError ( error ) ;
262236 } ) ;
263237
264238 socket . addEventListener ( "close" , ( event ) => {
@@ -272,19 +246,18 @@ export class ReconnectingWebSocket<
272246 this . #logger. error (
273247 `WebSocket connection closed with unrecoverable error code ${ event . code } ` ,
274248 ) ;
275- // Suspend instead of dispose - allows recovery when credentials change
276249 this . disconnect ( ) ;
277250 return ;
278251 }
279252
280- // Don't reconnect on normal closure
281253 if ( NORMAL_CLOSURE_CODES . has ( event . code ) ) {
282254 return ;
283255 }
284256
285- // Reconnect on abnormal closures (e.g., 1006) or other unexpected codes
286257 this . scheduleReconnect ( ) ;
287258 } ) ;
259+ } catch ( error ) {
260+ await this . handleConnectionError ( error ) ;
288261 } finally {
289262 this . #isConnecting = false ;
290263
@@ -314,7 +287,8 @@ export class ReconnectingWebSocket<
314287
315288 this . #reconnectTimeoutId = setTimeout ( ( ) => {
316289 this . #reconnectTimeoutId = null ;
317- this . connect ( ) . catch ( ( error ) => this . handleConnectionError ( error ) ) ;
290+ // connect() handles all errors internally
291+ void this . connect ( ) ;
318292 } , delayMs ) ;
319293
320294 this . #backoffMs = Math . min ( this . #backoffMs * 2 , this . #options. maxBackoffMs ) ;
@@ -343,7 +317,15 @@ export class ReconnectingWebSocket<
343317 private async handleClientCertificateError (
344318 certError : ClientCertificateError ,
345319 ) : Promise < boolean > {
320+ // Only attempt refresh once per connection cycle
321+ if ( this . #certRefreshAttempted) {
322+ this . #logger. warn ( "Certificate refresh already attempted, not retrying" ) ;
323+ void certError . showNotification ( ) ;
324+ return false ;
325+ }
326+
346327 if ( certError . isRefreshable ) {
328+ this . #certRefreshAttempted = true ; // Mark that we're attempting
347329 this . #logger. info (
348330 `Client certificate error (alert ${ certError . alertCode } ), attempting refresh...` ,
349331 ) ;
0 commit comments