@@ -37,6 +37,7 @@ export class FlagixClient {
3737 private readonly maxReconnectAttempts = Number . POSITIVE_INFINITY ;
3838 private readonly baseReconnectDelay = 1000 ;
3939 private readonly maxReconnectDelay = 30_000 ;
40+ private isConnectingSSE = false ;
4041
4142 constructor ( options : FlagixClientOptions ) {
4243 this . apiKey = options . apiKey ;
@@ -49,21 +50,25 @@ export class FlagixClient {
4950 /**
5051 * Subscribes a listener to a flag update event.
5152 */
52- on (
53+ on = (
5354 event : typeof FLAG_UPDATE_EVENT ,
5455 listener : ( flagKey : string ) => void
55- ) : void {
56+ ) => {
5657 this . emitter . on ( event , listener ) ;
57- }
58+ } ;
5859
5960 /**
6061 * Unsubscribes a listener from a flag update event.
6162 */
62- off (
63+ off = (
6364 event : typeof FLAG_UPDATE_EVENT ,
6465 listener : ( flagKey : string ) => void
65- ) : void {
66+ ) => {
6667 this . emitter . off ( event , listener ) ;
68+ } ;
69+
70+ getApiKey ( ) : string {
71+ return this . apiKey ;
6772 }
6873
6974 /**
@@ -125,6 +130,15 @@ export class FlagixClient {
125130 return ( result ?. value as T ) ?? null ;
126131 }
127132
133+ /**
134+ * Replaces the global evaluation context.
135+ */
136+ identify ( newContext : EvaluationContext ) : void {
137+ this . context = newContext ;
138+ log ( "info" , "[Flagix SDK] Context replaced" ) ;
139+ this . refreshAllFlags ( ) ;
140+ }
141+
128142 /**
129143 * Sets or updates the global evaluation context.
130144 * @param newContext New context attributes to merge or replace.
@@ -135,7 +149,13 @@ export class FlagixClient {
135149 "info" ,
136150 "[Flagix SDK] Context updated. Evaluations will use the new context."
137151 ) ;
152+ this . refreshAllFlags ( ) ;
153+ }
138154
155+ /**
156+ * Helper to refresh all flags by emitting update events for each cached flag.
157+ */
158+ private refreshAllFlags ( ) : void {
139159 for ( const flagKey of this . localCache . keys ( ) ) {
140160 this . emitter . emit ( FLAG_UPDATE_EVENT , flagKey ) ;
141161 }
@@ -172,6 +192,10 @@ export class FlagixClient {
172192 }
173193
174194 private async setupSSEListener ( ) : Promise < void > {
195+ if ( this . isConnectingSSE ) {
196+ return ;
197+ }
198+
175199 if ( this . sseConnection ) {
176200 try {
177201 this . sseConnection . close ( ) ;
@@ -185,89 +209,102 @@ export class FlagixClient {
185209 this . sseConnection = null ;
186210 }
187211
212+ this . isConnectingSSE = true ;
188213 const url = `${ this . apiBaseUrl } /api/sse/stream` ;
189214
190- const source = await createEventSource ( url , this . apiKey ) ;
191- if ( ! source ) {
192- log ( "warn" , "[Flagix SDK] Failed to create EventSource. Retrying..." ) ;
193- this . scheduleReconnect ( ) ;
194- return ;
195- }
196-
197- this . sseConnection = source ;
198-
199- source . onopen = ( ) => {
200- this . reconnectAttempts = 0 ;
201- this . isReconnecting = false ;
202- if ( this . reconnectTimeoutId ) {
203- clearTimeout ( this . reconnectTimeoutId ) ;
204- this . reconnectTimeoutId = null ;
215+ try {
216+ const source = await createEventSource ( url , this . apiKey ) ;
217+ if ( ! source ) {
218+ log ( "warn" , "[Flagix SDK] Failed to create EventSource. Retrying..." ) ;
219+ this . scheduleReconnect ( ) ;
220+ return ;
205221 }
206222
207- // If this is a reconnection and not the first connection, refresh the cache
208- // this ensures we have the latest flag values that may have changed while disconnected
209- if ( this . hasEstablishedConnection && this . isInitialized ) {
210- log (
211- "info" ,
212- "[Flagix SDK] SSE reconnected. Refreshing cache to sync with server..."
213- ) ;
214- this . fetchInitialConfig ( ) . catch ( ( error ) => {
215- log (
216- "error" ,
217- "[Flagix SDK] Failed to refresh cache after reconnection" ,
218- error
219- ) ;
220- } ) ;
221- } else {
222- this . hasEstablishedConnection = true ;
223+ if ( ! this . isInitialized && ! this . isReconnecting ) {
224+ source . close ( ) ;
225+ return ;
223226 }
224227
225- log ( "info" , "[Flagix SDK] SSE connection established." ) ;
226- } ;
227-
228- source . onerror = ( error ) => {
229- const eventSource = error . target as EventSource ;
230- const readyState = eventSource ?. readyState ;
228+ this . sseConnection = source ;
231229
232- // EventSource.readyState: 0 = CONNECTING, 1 = OPEN, 2 = CLOSED
233- if ( readyState === 2 ) {
234- log (
235- "warn" ,
236- "[Flagix SDK] SSE connection closed. Attempting to reconnect..."
237- ) ;
238- this . handleReconnect ( ) ;
239- } else if ( readyState === 0 ) {
240- log (
241- "warn" ,
242- "[Flagix SDK] SSE connection error (connecting state)" ,
243- error
244- ) ;
245- } else {
246- log ( "error" , "[Flagix SDK] SSE error" , error ) ;
247- this . handleReconnect ( ) ;
248- }
249- } ;
230+ source . onopen = ( ) => {
231+ this . reconnectAttempts = 0 ;
232+ this . isReconnecting = false ;
233+ if ( this . reconnectTimeoutId ) {
234+ clearTimeout ( this . reconnectTimeoutId ) ;
235+ this . reconnectTimeoutId = null ;
236+ }
250237
251- // Listen for the "connected" event from the server
252- source . addEventListener ( "connected" , ( ) => {
253- log ( "info" , "[Flagix SDK] SSE connection confirmed by server." ) ;
254- } ) ;
238+ // If this is a reconnection and not the first connection, refresh the cache
239+ // this ensures we have the latest flag values that may have changed while disconnected
240+ if ( this . hasEstablishedConnection && this . isInitialized ) {
241+ log (
242+ "info" ,
243+ "[Flagix SDK] SSE reconnected. Refreshing cache to sync with server..."
244+ ) ;
245+ this . fetchInitialConfig ( ) . catch ( ( error ) => {
246+ log (
247+ "error" ,
248+ "[Flagix SDK] Failed to refresh cache after reconnection" ,
249+ error
250+ ) ;
251+ } ) ;
252+ } else {
253+ this . hasEstablishedConnection = true ;
254+ }
255+
256+ log ( "info" , "[Flagix SDK] SSE connection established." ) ;
257+ } ;
258+
259+ source . onerror = ( error ) => {
260+ const eventSource = error . target as EventSource ;
261+ const readyState = eventSource ?. readyState ;
262+
263+ // EventSource.readyState: 0 = CONNECTING, 1 = OPEN, 2 = CLOSED
264+ if ( readyState === 2 ) {
265+ log (
266+ "warn" ,
267+ "[Flagix SDK] SSE connection closed. Attempting to reconnect..."
268+ ) ;
269+ this . handleReconnect ( ) ;
270+ } else if ( readyState === 0 ) {
271+ log (
272+ "warn" ,
273+ "[Flagix SDK] SSE connection error (connecting state)" ,
274+ error
275+ ) ;
276+ } else {
277+ log ( "error" , "[Flagix SDK] SSE error" , error ) ;
278+ this . handleReconnect ( ) ;
279+ }
280+ } ;
281+
282+ // Listen for the "connected" event from the server
283+ source . addEventListener ( "connected" , ( ) => {
284+ log ( "info" , "[Flagix SDK] SSE connection confirmed by server." ) ;
285+ } ) ;
255286
256- source . addEventListener ( EVENT_TO_LISTEN , ( event ) => {
257- try {
258- const data = JSON . parse ( event . data ) ;
259- const { flagKey, type } = data as {
260- flagKey : string ;
261- type : FlagUpdateType ;
262- } ;
287+ source . addEventListener ( EVENT_TO_LISTEN , ( event ) => {
288+ try {
289+ const data = JSON . parse ( event . data ) ;
290+ const { flagKey, type } = data as {
291+ flagKey : string ;
292+ type : FlagUpdateType ;
293+ } ;
263294
264- log ( "info" , `[Flagix SDK] Received update for ${ flagKey } (${ type } ).` ) ;
295+ log ( "info" , `[Flagix SDK] Received update for ${ flagKey } (${ type } ).` ) ;
265296
266- this . fetchSingleFlagConfig ( flagKey , type ) ;
267- } catch ( error ) {
268- log ( "error" , "[Flagix SDK] Failed to parse SSE event data." , error ) ;
269- }
270- } ) ;
297+ this . fetchSingleFlagConfig ( flagKey , type ) ;
298+ } catch ( error ) {
299+ log ( "error" , "[Flagix SDK] Failed to parse SSE event data." , error ) ;
300+ }
301+ } ) ;
302+ } catch ( error ) {
303+ log ( "error" , "[Flagix SDK] Failed during SSE setup" , error ) ;
304+ this . handleReconnect ( ) ;
305+ } finally {
306+ this . isConnectingSSE = false ;
307+ }
271308 }
272309
273310 private handleReconnect ( ) : void {
@@ -324,7 +361,7 @@ export class FlagixClient {
324361 ) : Promise < void > {
325362 const url = `${ this . apiBaseUrl } /api/flag-config/${ flagKey } ` ;
326363
327- if ( type === "FLAG_DELETED" || type === "RULE_DELETED" ) {
364+ if ( type === "FLAG_DELETED" ) {
328365 this . localCache . delete ( flagKey ) ;
329366 log ( "info" , `[Flagix SDK] Flag ${ flagKey } deleted from cache.` ) ;
330367 this . emitter . emit ( FLAG_UPDATE_EVENT , flagKey ) ;
0 commit comments