@@ -30,6 +30,13 @@ export class FlagixClient {
3030 private isInitialized = false ;
3131 private sseConnection : FlagStreamConnection | null = null ;
3232 private readonly emitter : FlagixEventEmitter ;
33+ private reconnectAttempts = 0 ;
34+ private reconnectTimeoutId : ReturnType < typeof setTimeout > | null = null ;
35+ private isReconnecting = false ;
36+ private hasEstablishedConnection = false ;
37+ private readonly maxReconnectAttempts = Number . POSITIVE_INFINITY ;
38+ private readonly baseReconnectDelay = 1000 ;
39+ private readonly maxReconnectDelay = 30_000 ;
3340
3441 constructor ( options : FlagixClientOptions ) {
3542 this . apiKey = options . apiKey ;
@@ -161,23 +168,87 @@ export class FlagixClient {
161168 }
162169
163170 private async setupSSEListener ( ) : Promise < void > {
171+ if ( this . sseConnection ) {
172+ try {
173+ this . sseConnection . close ( ) ;
174+ } catch ( error ) {
175+ log (
176+ "warn" ,
177+ "[Flagix SDK] Error closing existing SSE connection" ,
178+ error
179+ ) ;
180+ }
181+ this . sseConnection = null ;
182+ }
183+
164184 const url = `${ this . apiBaseUrl } /api/sse/stream` ;
165185
166186 const source = await createEventSource ( url , this . apiKey ) ;
167187 if ( ! source ) {
188+ log ( "warn" , "[Flagix SDK] Failed to create EventSource. Retrying..." ) ;
189+ this . scheduleReconnect ( ) ;
168190 return ;
169191 }
170192
171193 this . sseConnection = source ;
172194
173195 source . onopen = ( ) => {
196+ this . reconnectAttempts = 0 ;
197+ this . isReconnecting = false ;
198+ if ( this . reconnectTimeoutId ) {
199+ clearTimeout ( this . reconnectTimeoutId ) ;
200+ this . reconnectTimeoutId = null ;
201+ }
202+
203+ // If this is a reconnection and not the first connection, refresh the cache
204+ // this ensures we have the latest flag values that may have changed while disconnected
205+ if ( this . hasEstablishedConnection && this . isInitialized ) {
206+ log (
207+ "info" ,
208+ "[Flagix SDK] SSE reconnected. Refreshing cache to sync with server..."
209+ ) ;
210+ this . fetchInitialConfig ( ) . catch ( ( error ) => {
211+ log (
212+ "error" ,
213+ "[Flagix SDK] Failed to refresh cache after reconnection" ,
214+ error
215+ ) ;
216+ } ) ;
217+ } else {
218+ this . hasEstablishedConnection = true ;
219+ }
220+
174221 log ( "info" , "[Flagix SDK] SSE connection established." ) ;
175222 } ;
176223
177224 source . onerror = ( error ) => {
178- log ( "error" , "[Flagix SDK] SSE error" , error ) ;
225+ const eventSource = error . target as EventSource ;
226+ const readyState = eventSource ?. readyState ;
227+
228+ // EventSource.readyState: 0 = CONNECTING, 1 = OPEN, 2 = CLOSED
229+ if ( readyState === 2 ) {
230+ log (
231+ "warn" ,
232+ "[Flagix SDK] SSE connection closed. Attempting to reconnect..."
233+ ) ;
234+ this . handleReconnect ( ) ;
235+ } else if ( readyState === 0 ) {
236+ log (
237+ "warn" ,
238+ "[Flagix SDK] SSE connection error (connecting state)" ,
239+ error
240+ ) ;
241+ } else {
242+ log ( "error" , "[Flagix SDK] SSE error" , error ) ;
243+ this . handleReconnect ( ) ;
244+ }
179245 } ;
180246
247+ // Listen for the "connected" event from the server
248+ source . addEventListener ( "connected" , ( ) => {
249+ log ( "info" , "[Flagix SDK] SSE connection confirmed by server." ) ;
250+ } ) ;
251+
181252 source . addEventListener ( EVENT_TO_LISTEN , ( event ) => {
182253 try {
183254 const data = JSON . parse ( event . data ) ;
@@ -195,6 +266,54 @@ export class FlagixClient {
195266 } ) ;
196267 }
197268
269+ private handleReconnect ( ) : void {
270+ if ( this . isReconnecting || ! this . isInitialized ) {
271+ return ;
272+ }
273+
274+ if ( this . reconnectAttempts >= this . maxReconnectAttempts ) {
275+ log (
276+ "error" ,
277+ "[Flagix SDK] Max reconnection attempts reached. Stopping reconnection."
278+ ) ;
279+ return ;
280+ }
281+
282+ this . isReconnecting = true ;
283+ this . scheduleReconnect ( ) ;
284+ }
285+
286+ private scheduleReconnect ( ) : void {
287+ if ( this . reconnectTimeoutId ) {
288+ clearTimeout ( this . reconnectTimeoutId ) ;
289+ }
290+
291+ // Calculate exponential backoff delay with jitter
292+ const delay = Math . min (
293+ this . baseReconnectDelay * 2 ** this . reconnectAttempts ,
294+ this . maxReconnectDelay
295+ ) ;
296+ // Add ±25% jitter to prevent thundering herd
297+ const jitter = delay * 0.25 * ( Math . random ( ) * 2 - 1 ) ;
298+ const finalDelay = Math . max ( 100 , delay + jitter ) ;
299+
300+ this . reconnectAttempts ++ ;
301+
302+ log (
303+ "info" ,
304+ `[Flagix SDK] Scheduling SSE reconnection attempt ${ this . reconnectAttempts } in ${ Math . round ( finalDelay ) } ms...`
305+ ) ;
306+
307+ this . reconnectTimeoutId = setTimeout ( ( ) => {
308+ this . isReconnecting = false ;
309+ this . reconnectTimeoutId = null ;
310+ this . setupSSEListener ( ) . catch ( ( error ) => {
311+ log ( "error" , "[Flagix SDK] Failed to reconnect SSE" , error ) ;
312+ this . handleReconnect ( ) ;
313+ } ) ;
314+ } , finalDelay ) ;
315+ }
316+
198317 private async fetchSingleFlagConfig (
199318 flagKey : string ,
200319 type : FlagUpdateType
@@ -281,8 +400,21 @@ export class FlagixClient {
281400 * Closes the Server-Sent Events (SSE) connection and cleans up resources.
282401 */
283402 close ( ) : void {
403+ if ( this . reconnectTimeoutId ) {
404+ clearTimeout ( this . reconnectTimeoutId ) ;
405+ this . reconnectTimeoutId = null ;
406+ }
407+
408+ this . isReconnecting = false ;
409+ this . reconnectAttempts = 0 ;
410+ this . hasEstablishedConnection = false ;
411+
284412 if ( this . sseConnection ) {
285- this . sseConnection . close ( ) ;
413+ try {
414+ this . sseConnection . close ( ) ;
415+ } catch ( error ) {
416+ log ( "warn" , "[Flagix SDK] Error closing SSE connection" , error ) ;
417+ }
286418 this . sseConnection = null ;
287419 log ( "info" , "[Flagix SDK] SSE connection closed." ) ;
288420 }
0 commit comments