@@ -18,6 +18,9 @@ import {assertExists, reportError} from '../base/logging';
1818import { EngineBase } from '../trace_processor/engine' ;
1919
2020const RPC_CONNECT_TIMEOUT_MS = 2000 ;
21+ const INITIAL_RETRY_DELAY_MS = 100 ;
22+ const MAX_RETRY_DELAY_MS = 30000 ;
23+ const BACKOFF_MULTIPLIER = 2 ;
2124
2225export interface HttpRpcState {
2326 connected : boolean ;
@@ -34,6 +37,8 @@ export class HttpRpcEngine extends EngineBase {
3437 private disposed = false ;
3538 private queue : Blob [ ] = [ ] ;
3639 private isProcessingQueue = false ;
40+ private retryDelayMs = INITIAL_RETRY_DELAY_MS ;
41+ private retryTimeoutId ?: ReturnType < typeof setTimeout > ;
3742
3843 // Can be changed by frontend/index.ts when passing ?rpc_port=1234 .
3944 static defaultRpcPort = '9001' ;
@@ -47,47 +52,109 @@ export class HttpRpcEngine extends EngineBase {
4752 }
4853
4954 rpcSendRequestBytes ( data : Uint8Array ) : void {
50- if ( this . websocket === undefined ) {
51- if ( this . disposed ) return ;
52- const wsUrl = `ws://${ HttpRpcEngine . getHostAndPort ( this . port ) } /websocket` ;
53- this . websocket = new WebSocket ( wsUrl ) ;
54- this . websocket . onopen = ( ) => this . onWebsocketConnected ( ) ;
55- this . websocket . onmessage = ( e ) => this . onWebsocketMessage ( e ) ;
56- this . websocket . onclose = ( e ) => this . onWebsocketClosed ( e ) ;
57- this . websocket . onerror = ( e ) =>
58- super . fail (
59- `WebSocket error rs=${ ( e . target as WebSocket ) ?. readyState } (ERR:ws)` ,
60- ) ;
61- }
55+ if ( this . disposed ) return ;
56+ const websocket = this . getOrCreateWebSocket ( ) ;
6257
6358 if ( this . connected ) {
64- this . websocket . send ( data ) ;
59+ websocket . send ( data ) ;
6560 } else {
6661 this . requestQueue . push ( data ) ; // onWebsocketConnected() will flush this.
6762 }
6863 }
6964
65+ /**
66+ * Returns the existing WebSocket if one exists and is not closed,
67+ * otherwise creates a new one (closing any stale socket first).
68+ */
69+ private getOrCreateWebSocket ( ) : WebSocket {
70+ // If we have an active websocket that's not closed/closing, reuse it
71+ if (
72+ this . websocket !== undefined &&
73+ this . websocket . readyState !== WebSocket . CLOSED &&
74+ this . websocket . readyState !== WebSocket . CLOSING
75+ ) {
76+ return this . websocket ;
77+ }
78+
79+ // Close any stale websocket before creating a new one
80+ this . closeWebSocket ( ) ;
81+
82+ const wsUrl = `ws://${ HttpRpcEngine . getHostAndPort ( this . port ) } /websocket` ;
83+ this . websocket = new WebSocket ( wsUrl ) ;
84+ this . websocket . onopen = ( ) => this . onWebsocketConnected ( ) ;
85+ this . websocket . onmessage = ( e ) => this . onWebsocketMessage ( e ) ;
86+ this . websocket . onclose = ( e ) => this . onWebsocketClosed ( e ) ;
87+ this . websocket . onerror = ( e ) => this . onWebsocketError ( e ) ;
88+ return this . websocket ;
89+ }
90+
91+ /**
92+ * Closes the current websocket if one exists, clearing event handlers
93+ * to prevent spurious callbacks.
94+ */
95+ private closeWebSocket ( ) : void {
96+ if ( this . websocket === undefined ) return ;
97+
98+ // Clear handlers to prevent callbacks from a closing socket
99+ this . websocket . onopen = null ;
100+ this . websocket . onmessage = null ;
101+ this . websocket . onclose = null ;
102+ this . websocket . onerror = null ;
103+ this . websocket . close ( ) ;
104+ this . websocket = undefined ;
105+ }
106+
107+ private onWebsocketError ( e : Event ) : void {
108+ if ( this . disposed ) return ;
109+ const readyState = ( e . target as WebSocket ) ?. readyState ;
110+ console . warn ( `WebSocket error rs=${ readyState } , will retry with backoff` ) ;
111+ // The close event will fire after this, which will trigger the retry logic
112+ }
113+
114+ private scheduleReconnect ( ) : void {
115+ if ( this . disposed ) return ;
116+
117+ console . debug (
118+ `Scheduling WebSocket reconnection in ${ this . retryDelayMs } ms` ,
119+ ) ;
120+
121+ this . retryTimeoutId = setTimeout ( ( ) => {
122+ if ( this . disposed ) return ;
123+ console . debug ( 'Attempting WebSocket reconnection...' ) ;
124+ this . getOrCreateWebSocket ( ) ;
125+ } , this . retryDelayMs ) ;
126+
127+ // Exponential backoff with cap
128+ this . retryDelayMs = Math . min (
129+ this . retryDelayMs * BACKOFF_MULTIPLIER ,
130+ MAX_RETRY_DELAY_MS ,
131+ ) ;
132+ }
133+
70134 private onWebsocketConnected ( ) {
135+ // Reset retry delay on successful connection
136+ this . retryDelayMs = INITIAL_RETRY_DELAY_MS ;
137+
71138 for ( ; ; ) {
72139 const queuedMsg = this . requestQueue . shift ( ) ;
73140 if ( queuedMsg === undefined ) break ;
74141 assertExists ( this . websocket ) . send ( queuedMsg ) ;
75142 }
143+ console . debug ( 'WebSocket (re)connected on port' , this . port ) ;
76144 this . connected = true ;
77145 }
78146
79147 private onWebsocketClosed ( e : CloseEvent ) {
80148 if ( this . disposed ) return ;
81- if ( e . code === 1006 && this . connected ) {
82- // On macbooks the act of closing the lid / suspending often causes socket
83- // disconnections. Try to gracefully re-connect.
84- console . log ( 'Websocket closed, reconnecting' ) ;
85- this . websocket = undefined ;
86- this . connected = false ;
87- this . rpcSendRequestBytes ( new Uint8Array ( ) ) ; // Triggers a reconnection.
88- } else {
89- super . fail ( `Websocket closed (${ e . code } : ${ e . reason } ) (ERR:ws)` ) ;
90- }
149+
150+ // Always attempt to reconnect with backoff, regardless of close code
151+ console . debug (
152+ `WebSocket closed (code=${ e . code } , reason=${ e . reason || 'none' } , wasConnected=${ this . connected } ), scheduling reconnect` ,
153+ ) ;
154+
155+ this . websocket = undefined ;
156+ this . connected = false ;
157+ this . scheduleReconnect ( ) ;
91158 }
92159
93160 private onWebsocketMessage ( e : MessageEvent ) {
@@ -147,8 +214,13 @@ export class HttpRpcEngine extends EngineBase {
147214 [ Symbol . dispose ] ( ) {
148215 this . disposed = true ;
149216 this . connected = false ;
150- const websocket = this . websocket ;
151- this . websocket = undefined ;
152- websocket ?. close ( ) ;
217+
218+ // Clear any pending retry timeout
219+ if ( this . retryTimeoutId !== undefined ) {
220+ clearTimeout ( this . retryTimeoutId ) ;
221+ this . retryTimeoutId = undefined ;
222+ }
223+
224+ this . closeWebSocket ( ) ;
153225 }
154226}
0 commit comments