@@ -5,6 +5,7 @@ export interface WebSocketMessage {
55}
66
77export type MessageHandler = ( msg : WebSocketMessage ) => Promise < void > | void ;
8+ export type StateChangeHandler = ( transition : RelayerStateTransition , state : RelayerState ) => void ;
89
910export interface RelayerState {
1011 connected : boolean ;
@@ -15,23 +16,35 @@ export interface RelayerState {
1516
1617export type RelayerStateTransition = 'connecting' | 'connected' | 'disconnected' | 'reconnecting' | 'destroyed' ;
1718
19+ export interface WebSocketRelayerOptions {
20+ maxReconnectAttempts ?: number ;
21+ reconnectDelayMs ?: number ;
22+ onStateChange ?: StateChangeHandler ;
23+ }
24+
1825export class WebSocketRelayer {
1926 private url : string ;
2027 private ws : WebSocket | null = null ;
2128 private handlers : Map < string , Set < MessageHandler > > = new Map ( ) ;
29+ private stateChangeHandlers : Set < StateChangeHandler > = new Set ( ) ;
2230 private isDestroyed = false ;
2331 private isLocked = false ;
2432 private lockQueue : Array < ( ) => void > = [ ] ;
2533 private connectPromise : Promise < void > | null = null ;
2634 private reconnectAttempts = 0 ;
35+ private reconnectEnabled = true ;
2736 private maxReconnectAttempts : number ;
2837 private reconnectDelayMs : number ;
2938 private pendingMessages : WebSocketMessage [ ] = [ ] ;
39+ private stateTransition : RelayerStateTransition = 'disconnected' ;
3040
31- constructor ( url : string , options ?: { maxReconnectAttempts ?: number ; reconnectDelayMs ?: number } ) {
41+ constructor ( url : string , options ?: WebSocketRelayerOptions ) {
3242 this . url = url ;
3343 this . maxReconnectAttempts = options ?. maxReconnectAttempts ?? 5 ;
3444 this . reconnectDelayMs = options ?. reconnectDelayMs ?? 1000 ;
45+ if ( options ?. onStateChange ) {
46+ this . stateChangeHandlers . add ( options . onStateChange ) ;
47+ }
3548 }
3649
3750 get state ( ) : RelayerState {
@@ -62,10 +75,25 @@ export class WebSocketRelayer {
6275 }
6376 }
6477
78+ private emitStateChange ( transition : RelayerStateTransition ) : void {
79+ if ( this . stateTransition === transition ) return ;
80+
81+ this . stateTransition = transition ;
82+ const state = this . state ;
83+ for ( const handler of this . stateChangeHandlers ) {
84+ try {
85+ handler ( transition , state ) ;
86+ } catch ( err ) {
87+ console . warn ( '[WebSocketRelayer] state change handler error:' , err ) ;
88+ }
89+ }
90+ }
91+
6592 async connect ( ) : Promise < void > {
6693 if ( this . isDestroyed ) {
6794 throw new Error ( 'WebSocketRelayer has been destroyed' ) ;
6895 }
96+ this . reconnectEnabled = true ;
6997 if ( this . connectPromise ) {
7098 return this . connectPromise ;
7199 }
@@ -76,6 +104,7 @@ export class WebSocketRelayer {
76104 return ;
77105 }
78106
107+ this . emitStateChange ( 'connecting' ) ;
79108 this . connectPromise = this . establishConnection ( ) ;
80109 await this . connectPromise ;
81110 } finally {
@@ -97,6 +126,7 @@ export class WebSocketRelayer {
97126 private async establishConnection ( ) : Promise < void > {
98127 const WebSocketCtor = this . wsCtor ( ) ;
99128 if ( ! WebSocketCtor ) {
129+ this . emitStateChange ( 'disconnected' ) ;
100130 return ;
101131 }
102132
@@ -113,9 +143,14 @@ export class WebSocketRelayer {
113143 return ;
114144 }
115145
146+ let settled = false ;
147+
116148 ws . onopen = ( ) => {
149+ if ( settled ) return ;
150+ settled = true ;
117151 this . reconnectAttempts = 0 ;
118152 this . flushPendingMessages ( ) ;
153+ this . emitStateChange ( 'connected' ) ;
119154 resolve ( ) ;
120155 } ;
121156
@@ -125,13 +160,19 @@ export class WebSocketRelayer {
125160
126161 ws . onclose = ( ) => {
127162 this . ws = null ;
128- if ( ! this . isDestroyed ) {
163+ if ( ! settled ) {
164+ settled = true ;
165+ reject ( new Error ( `WebSocket connection closed before opening: ${ this . url } ` ) ) ;
166+ }
167+ this . emitStateChange ( 'disconnected' ) ;
168+ if ( ! this . isDestroyed && this . reconnectEnabled ) {
129169 this . attemptReconnect ( ) ;
130170 }
131171 } ;
132172
133173 ws . onerror = ( ) => {
134- if ( this . ws === null ) {
174+ if ( ! settled ) {
175+ settled = true ;
135176 reject ( new Error ( `WebSocket connection failed: ${ this . url } ` ) ) ;
136177 }
137178 } ;
@@ -181,6 +222,7 @@ export class WebSocketRelayer {
181222 private shouldAttemptReconnect ( ) : boolean {
182223 return (
183224 ! this . isDestroyed &&
225+ this . reconnectEnabled &&
184226 this . reconnectAttempts < this . maxReconnectAttempts &&
185227 ( ! this . ws || this . ws . readyState !== 1 )
186228 ) ;
@@ -202,6 +244,7 @@ export class WebSocketRelayer {
202244 if ( ! this . shouldAttemptReconnect ( ) ) return ;
203245
204246 this . reconnectAttempts ++ ;
247+ this . emitStateChange ( 'reconnecting' ) ;
205248 await new Promise ( ( r ) => setTimeout ( r , this . reconnectDelayMs * this . reconnectAttempts ) ) ;
206249 if ( this . isDestroyed ) return ;
207250
@@ -230,6 +273,14 @@ export class WebSocketRelayer {
230273 } ;
231274 }
232275
276+ onStateChange ( handler : StateChangeHandler ) : ( ) => void {
277+ this . stateChangeHandlers . add ( handler ) ;
278+
279+ return ( ) => {
280+ this . stateChangeHandlers . delete ( handler ) ;
281+ } ;
282+ }
283+
233284 off ( type : string , handler : MessageHandler ) : void {
234285 const handlers = this . handlers . get ( type ) ;
235286 if ( handlers ) {
@@ -266,7 +317,9 @@ export class WebSocketRelayer {
266317 }
267318
268319 disconnect ( ) : void {
320+ this . reconnectEnabled = false ;
269321 this . connectPromise = null ;
322+ this . reconnectAttempts = 0 ;
270323
271324 if ( this . ws ) {
272325 try {
@@ -276,10 +329,12 @@ export class WebSocketRelayer {
276329 }
277330 this . ws = null ;
278331 }
332+ this . emitStateChange ( 'disconnected' ) ;
279333 }
280334
281335 destroy ( ) : void {
282336 this . isDestroyed = true ;
337+ this . reconnectEnabled = false ;
283338 this . connectPromise = null ;
284339 this . pendingMessages = [ ] ;
285340 this . reconnectAttempts = this . maxReconnectAttempts ;
@@ -296,8 +351,10 @@ export class WebSocketRelayer {
296351 this . ws = null ;
297352 }
298353
354+ this . emitStateChange ( 'destroyed' ) ;
299355 this . handlers . clear ( ) ;
356+ this . stateChangeHandlers . clear ( ) ;
300357 this . lockQueue = [ ] ;
301358 this . isLocked = false ;
302359 }
303- }
360+ }
0 commit comments