@flagix/js-sdk 1.2.0 → 1.3.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -1,5 +1,5 @@
1
1
 
2
- > @flagix/js-sdk@1.2.0 build /home/runner/work/flagix/flagix/sdk/javascript
2
+ > @flagix/js-sdk@1.3.0 build /home/runner/work/flagix/flagix/sdk/javascript
3
3
  > tsup src/index.ts --format cjs,esm --dts --clean
4
4
 
5
5
  CLI Building entry: src/index.ts
@@ -9,11 +9,11 @@
9
9
  CLI Cleaning output folder
10
10
  CJS Build start
11
11
  ESM Build start
12
- ESM dist/index.mjs 15.58 KB
13
- ESM ⚡️ Build success in 46ms
14
- CJS dist/index.js 17.27 KB
15
- CJS ⚡️ Build success in 46ms
12
+ ESM dist/index.mjs 17.07 KB
13
+ ESM ⚡️ Build success in 57ms
14
+ CJS dist/index.js 18.75 KB
15
+ CJS ⚡️ Build success in 58ms
16
16
  DTS Build start
17
- DTS ⚡️ Build success in 2398ms
18
- DTS dist/index.d.ts 2.30 KB
19
- DTS dist/index.d.mts 2.30 KB
17
+ DTS ⚡️ Build success in 1849ms
18
+ DTS dist/index.d.ts 2.41 KB
19
+ DTS dist/index.d.mts 2.41 KB
package/CHANGELOG.md CHANGED
@@ -1,5 +1,16 @@
1
1
  # @flagix/js-sdk
2
2
 
3
+ ## 1.3.0
4
+
5
+ ### Minor Changes
6
+
7
+ - 4e879d8: Improved SDK stability and lifecycle management. Added identify method for explicit user identity switching, fixed race conditions during initialization and SSE setup, and ensured feature flags gracefully fallback to 'off' variations when disabled. Fixed potential memory leaks in React hooks and added support for runtime API key changes.
8
+
9
+ ### Patch Changes
10
+
11
+ - Updated dependencies [4e879d8]
12
+ - @flagix/evaluation-core@1.2.0
13
+
3
14
  ## 1.2.0
4
15
 
5
16
  ### Minor Changes
package/dist/index.d.mts CHANGED
@@ -40,6 +40,10 @@ declare const Flagix: {
40
40
  * @param contextOverrides Optional context.
41
41
  */
42
42
  track(eventName: string, properties?: Record<string, unknown>, contextOverrides?: EvaluationContext): void;
43
+ /**
44
+ * Replaces the global evaluation context.
45
+ */
46
+ identify(newContext: EvaluationContext): void;
43
47
  /**
44
48
  * Sets or updates the global evaluation context.
45
49
  * @param newContext New context attributes to merge or replace.
package/dist/index.d.ts CHANGED
@@ -40,6 +40,10 @@ declare const Flagix: {
40
40
  * @param contextOverrides Optional context.
41
41
  */
42
42
  track(eventName: string, properties?: Record<string, unknown>, contextOverrides?: EvaluationContext): void;
43
+ /**
44
+ * Replaces the global evaluation context.
45
+ */
46
+ identify(newContext: EvaluationContext): void;
43
47
  /**
44
48
  * Sets or updates the global evaluation context.
45
49
  * @param newContext New context attributes to merge or replace.
package/dist/index.js CHANGED
@@ -102,23 +102,27 @@ var FlagixClient = class {
102
102
  this.maxReconnectAttempts = Number.POSITIVE_INFINITY;
103
103
  this.baseReconnectDelay = 1e3;
104
104
  this.maxReconnectDelay = 3e4;
105
+ this.isConnectingSSE = false;
106
+ /**
107
+ * Subscribes a listener to a flag update event.
108
+ */
109
+ this.on = (event, listener) => {
110
+ this.emitter.on(event, listener);
111
+ };
112
+ /**
113
+ * Unsubscribes a listener from a flag update event.
114
+ */
115
+ this.off = (event, listener) => {
116
+ this.emitter.off(event, listener);
117
+ };
105
118
  this.apiKey = options.apiKey;
106
119
  this.apiBaseUrl = options.apiBaseUrl.replace(REMOVE_TRAILING_SLASH, "");
107
120
  this.context = options.initialContext || {};
108
121
  this.emitter = new FlagixEventEmitter();
109
122
  setLogLevel(options.logs?.level ?? "none");
110
123
  }
111
- /**
112
- * Subscribes a listener to a flag update event.
113
- */
114
- on(event, listener) {
115
- this.emitter.on(event, listener);
116
- }
117
- /**
118
- * Unsubscribes a listener from a flag update event.
119
- */
120
- off(event, listener) {
121
- this.emitter.off(event, listener);
124
+ getApiKey() {
125
+ return this.apiKey;
122
126
  }
123
127
  /**
124
128
  * Fetches all flag configurations from the API, populates the local cache,
@@ -164,6 +168,14 @@ var FlagixClient = class {
164
168
  }
165
169
  return result?.value ?? null;
166
170
  }
171
+ /**
172
+ * Replaces the global evaluation context.
173
+ */
174
+ identify(newContext) {
175
+ this.context = newContext;
176
+ log("info", "[Flagix SDK] Context replaced");
177
+ this.refreshAllFlags();
178
+ }
167
179
  /**
168
180
  * Sets or updates the global evaluation context.
169
181
  * @param newContext New context attributes to merge or replace.
@@ -174,6 +186,12 @@ var FlagixClient = class {
174
186
  "info",
175
187
  "[Flagix SDK] Context updated. Evaluations will use the new context."
176
188
  );
189
+ this.refreshAllFlags();
190
+ }
191
+ /**
192
+ * Helper to refresh all flags by emitting update events for each cached flag.
193
+ */
194
+ refreshAllFlags() {
177
195
  for (const flagKey of this.localCache.keys()) {
178
196
  this.emitter.emit(FLAG_UPDATE_EVENT, flagKey);
179
197
  }
@@ -204,6 +222,9 @@ var FlagixClient = class {
204
222
  }
205
223
  }
206
224
  async setupSSEListener() {
225
+ if (this.isConnectingSSE) {
226
+ return;
227
+ }
207
228
  if (this.sseConnection) {
208
229
  try {
209
230
  this.sseConnection.close();
@@ -216,71 +237,83 @@ var FlagixClient = class {
216
237
  }
217
238
  this.sseConnection = null;
218
239
  }
240
+ this.isConnectingSSE = true;
219
241
  const url = `${this.apiBaseUrl}/api/sse/stream`;
220
- const source = await createEventSource(url, this.apiKey);
221
- if (!source) {
222
- log("warn", "[Flagix SDK] Failed to create EventSource. Retrying...");
223
- this.scheduleReconnect();
224
- return;
225
- }
226
- this.sseConnection = source;
227
- source.onopen = () => {
228
- this.reconnectAttempts = 0;
229
- this.isReconnecting = false;
230
- if (this.reconnectTimeoutId) {
231
- clearTimeout(this.reconnectTimeoutId);
232
- this.reconnectTimeoutId = null;
242
+ try {
243
+ const source = await createEventSource(url, this.apiKey);
244
+ if (!source) {
245
+ log("warn", "[Flagix SDK] Failed to create EventSource. Retrying...");
246
+ this.scheduleReconnect();
247
+ return;
233
248
  }
234
- if (this.hasEstablishedConnection && this.isInitialized) {
235
- log(
236
- "info",
237
- "[Flagix SDK] SSE reconnected. Refreshing cache to sync with server..."
238
- );
239
- this.fetchInitialConfig().catch((error) => {
249
+ if (!this.isInitialized && !this.isReconnecting) {
250
+ source.close();
251
+ return;
252
+ }
253
+ this.sseConnection = source;
254
+ source.onopen = () => {
255
+ this.reconnectAttempts = 0;
256
+ this.isReconnecting = false;
257
+ if (this.reconnectTimeoutId) {
258
+ clearTimeout(this.reconnectTimeoutId);
259
+ this.reconnectTimeoutId = null;
260
+ }
261
+ if (this.hasEstablishedConnection && this.isInitialized) {
240
262
  log(
241
- "error",
242
- "[Flagix SDK] Failed to refresh cache after reconnection",
263
+ "info",
264
+ "[Flagix SDK] SSE reconnected. Refreshing cache to sync with server..."
265
+ );
266
+ this.fetchInitialConfig().catch((error) => {
267
+ log(
268
+ "error",
269
+ "[Flagix SDK] Failed to refresh cache after reconnection",
270
+ error
271
+ );
272
+ });
273
+ } else {
274
+ this.hasEstablishedConnection = true;
275
+ }
276
+ log("info", "[Flagix SDK] SSE connection established.");
277
+ };
278
+ source.onerror = (error) => {
279
+ const eventSource = error.target;
280
+ const readyState = eventSource?.readyState;
281
+ if (readyState === 2) {
282
+ log(
283
+ "warn",
284
+ "[Flagix SDK] SSE connection closed. Attempting to reconnect..."
285
+ );
286
+ this.handleReconnect();
287
+ } else if (readyState === 0) {
288
+ log(
289
+ "warn",
290
+ "[Flagix SDK] SSE connection error (connecting state)",
243
291
  error
244
292
  );
245
- });
246
- } else {
247
- this.hasEstablishedConnection = true;
248
- }
249
- log("info", "[Flagix SDK] SSE connection established.");
250
- };
251
- source.onerror = (error) => {
252
- const eventSource = error.target;
253
- const readyState = eventSource?.readyState;
254
- if (readyState === 2) {
255
- log(
256
- "warn",
257
- "[Flagix SDK] SSE connection closed. Attempting to reconnect..."
258
- );
259
- this.handleReconnect();
260
- } else if (readyState === 0) {
261
- log(
262
- "warn",
263
- "[Flagix SDK] SSE connection error (connecting state)",
264
- error
265
- );
266
- } else {
267
- log("error", "[Flagix SDK] SSE error", error);
268
- this.handleReconnect();
269
- }
270
- };
271
- source.addEventListener("connected", () => {
272
- log("info", "[Flagix SDK] SSE connection confirmed by server.");
273
- });
274
- source.addEventListener(EVENT_TO_LISTEN, (event) => {
275
- try {
276
- const data = JSON.parse(event.data);
277
- const { flagKey, type } = data;
278
- log("info", `[Flagix SDK] Received update for ${flagKey} (${type}).`);
279
- this.fetchSingleFlagConfig(flagKey, type);
280
- } catch (error) {
281
- log("error", "[Flagix SDK] Failed to parse SSE event data.", error);
282
- }
283
- });
293
+ } else {
294
+ log("error", "[Flagix SDK] SSE error", error);
295
+ this.handleReconnect();
296
+ }
297
+ };
298
+ source.addEventListener("connected", () => {
299
+ log("info", "[Flagix SDK] SSE connection confirmed by server.");
300
+ });
301
+ source.addEventListener(EVENT_TO_LISTEN, (event) => {
302
+ try {
303
+ const data = JSON.parse(event.data);
304
+ const { flagKey, type } = data;
305
+ log("info", `[Flagix SDK] Received update for ${flagKey} (${type}).`);
306
+ this.fetchSingleFlagConfig(flagKey, type);
307
+ } catch (error) {
308
+ log("error", "[Flagix SDK] Failed to parse SSE event data.", error);
309
+ }
310
+ });
311
+ } catch (error) {
312
+ log("error", "[Flagix SDK] Failed during SSE setup", error);
313
+ this.handleReconnect();
314
+ } finally {
315
+ this.isConnectingSSE = false;
316
+ }
284
317
  }
285
318
  handleReconnect() {
286
319
  if (this.isReconnecting || !this.isInitialized) {
@@ -322,7 +355,7 @@ var FlagixClient = class {
322
355
  }
323
356
  async fetchSingleFlagConfig(flagKey, type) {
324
357
  const url = `${this.apiBaseUrl}/api/flag-config/${flagKey}`;
325
- if (type === "FLAG_DELETED" || type === "RULE_DELETED") {
358
+ if (type === "FLAG_DELETED") {
326
359
  this.localCache.delete(flagKey);
327
360
  log("info", `[Flagix SDK] Flag ${flagKey} deleted from cache.`);
328
361
  this.emitter.emit(FLAG_UPDATE_EVENT, flagKey);
@@ -460,24 +493,37 @@ var Flagix = {
460
493
  */
461
494
  async initialize(options) {
462
495
  if (clientInstance) {
463
- log("warn", "Flagix SDK already initialized. Ignoring subsequent call.");
464
- return;
496
+ if (clientInstance.getApiKey() === options.apiKey) {
497
+ if (clientInstance.getIsInitialized()) {
498
+ return;
499
+ }
500
+ if (isInitializing && initializationPromise) {
501
+ return initializationPromise;
502
+ }
503
+ } else {
504
+ log(
505
+ "info",
506
+ "[Flagix SDK] API Key change detected. Resetting client..."
507
+ );
508
+ this.close();
509
+ }
465
510
  }
466
511
  if (isInitializing && initializationPromise) {
467
512
  return initializationPromise;
468
513
  }
469
514
  isInitializing = true;
470
- try {
471
- clientInstance = new FlagixClient(options);
472
- initializationPromise = clientInstance.initialize();
473
- await initializationPromise;
474
- } catch (error) {
475
- log("error", "Flagix SDK failed during initialization:", error);
476
- throw error;
477
- } finally {
478
- isInitializing = false;
479
- initializationPromise = null;
480
- }
515
+ initializationPromise = (async () => {
516
+ try {
517
+ clientInstance = new FlagixClient(options);
518
+ await clientInstance.initialize();
519
+ } catch (error) {
520
+ clientInstance = null;
521
+ throw error;
522
+ } finally {
523
+ isInitializing = false;
524
+ }
525
+ })();
526
+ return await initializationPromise;
481
527
  },
482
528
  /**
483
529
  * Evaluates a flag based on the local cache and current context.
@@ -510,6 +556,16 @@ var Flagix = {
510
556
  }
511
557
  clientInstance.track(eventName, properties, contextOverrides);
512
558
  },
559
+ /**
560
+ * Replaces the global evaluation context.
561
+ */
562
+ identify(newContext) {
563
+ if (!clientInstance) {
564
+ log("error", "Flagix SDK not initialized.");
565
+ return;
566
+ }
567
+ clientInstance.identify(newContext);
568
+ },
513
569
  /**
514
570
  * Sets or updates the global evaluation context.
515
571
  * @param newContext New context attributes to merge or replace.
@@ -528,6 +584,8 @@ var Flagix = {
528
584
  if (clientInstance) {
529
585
  clientInstance.close();
530
586
  clientInstance = null;
587
+ initializationPromise = null;
588
+ isInitializing = false;
531
589
  }
532
590
  },
533
591
  /**
package/dist/index.mjs CHANGED
@@ -66,23 +66,27 @@ var FlagixClient = class {
66
66
  this.maxReconnectAttempts = Number.POSITIVE_INFINITY;
67
67
  this.baseReconnectDelay = 1e3;
68
68
  this.maxReconnectDelay = 3e4;
69
+ this.isConnectingSSE = false;
70
+ /**
71
+ * Subscribes a listener to a flag update event.
72
+ */
73
+ this.on = (event, listener) => {
74
+ this.emitter.on(event, listener);
75
+ };
76
+ /**
77
+ * Unsubscribes a listener from a flag update event.
78
+ */
79
+ this.off = (event, listener) => {
80
+ this.emitter.off(event, listener);
81
+ };
69
82
  this.apiKey = options.apiKey;
70
83
  this.apiBaseUrl = options.apiBaseUrl.replace(REMOVE_TRAILING_SLASH, "");
71
84
  this.context = options.initialContext || {};
72
85
  this.emitter = new FlagixEventEmitter();
73
86
  setLogLevel(options.logs?.level ?? "none");
74
87
  }
75
- /**
76
- * Subscribes a listener to a flag update event.
77
- */
78
- on(event, listener) {
79
- this.emitter.on(event, listener);
80
- }
81
- /**
82
- * Unsubscribes a listener from a flag update event.
83
- */
84
- off(event, listener) {
85
- this.emitter.off(event, listener);
88
+ getApiKey() {
89
+ return this.apiKey;
86
90
  }
87
91
  /**
88
92
  * Fetches all flag configurations from the API, populates the local cache,
@@ -128,6 +132,14 @@ var FlagixClient = class {
128
132
  }
129
133
  return result?.value ?? null;
130
134
  }
135
+ /**
136
+ * Replaces the global evaluation context.
137
+ */
138
+ identify(newContext) {
139
+ this.context = newContext;
140
+ log("info", "[Flagix SDK] Context replaced");
141
+ this.refreshAllFlags();
142
+ }
131
143
  /**
132
144
  * Sets or updates the global evaluation context.
133
145
  * @param newContext New context attributes to merge or replace.
@@ -138,6 +150,12 @@ var FlagixClient = class {
138
150
  "info",
139
151
  "[Flagix SDK] Context updated. Evaluations will use the new context."
140
152
  );
153
+ this.refreshAllFlags();
154
+ }
155
+ /**
156
+ * Helper to refresh all flags by emitting update events for each cached flag.
157
+ */
158
+ refreshAllFlags() {
141
159
  for (const flagKey of this.localCache.keys()) {
142
160
  this.emitter.emit(FLAG_UPDATE_EVENT, flagKey);
143
161
  }
@@ -168,6 +186,9 @@ var FlagixClient = class {
168
186
  }
169
187
  }
170
188
  async setupSSEListener() {
189
+ if (this.isConnectingSSE) {
190
+ return;
191
+ }
171
192
  if (this.sseConnection) {
172
193
  try {
173
194
  this.sseConnection.close();
@@ -180,71 +201,83 @@ var FlagixClient = class {
180
201
  }
181
202
  this.sseConnection = null;
182
203
  }
204
+ this.isConnectingSSE = true;
183
205
  const url = `${this.apiBaseUrl}/api/sse/stream`;
184
- const source = await createEventSource(url, this.apiKey);
185
- if (!source) {
186
- log("warn", "[Flagix SDK] Failed to create EventSource. Retrying...");
187
- this.scheduleReconnect();
188
- return;
189
- }
190
- this.sseConnection = source;
191
- source.onopen = () => {
192
- this.reconnectAttempts = 0;
193
- this.isReconnecting = false;
194
- if (this.reconnectTimeoutId) {
195
- clearTimeout(this.reconnectTimeoutId);
196
- this.reconnectTimeoutId = null;
206
+ try {
207
+ const source = await createEventSource(url, this.apiKey);
208
+ if (!source) {
209
+ log("warn", "[Flagix SDK] Failed to create EventSource. Retrying...");
210
+ this.scheduleReconnect();
211
+ return;
197
212
  }
198
- if (this.hasEstablishedConnection && this.isInitialized) {
199
- log(
200
- "info",
201
- "[Flagix SDK] SSE reconnected. Refreshing cache to sync with server..."
202
- );
203
- this.fetchInitialConfig().catch((error) => {
213
+ if (!this.isInitialized && !this.isReconnecting) {
214
+ source.close();
215
+ return;
216
+ }
217
+ this.sseConnection = source;
218
+ source.onopen = () => {
219
+ this.reconnectAttempts = 0;
220
+ this.isReconnecting = false;
221
+ if (this.reconnectTimeoutId) {
222
+ clearTimeout(this.reconnectTimeoutId);
223
+ this.reconnectTimeoutId = null;
224
+ }
225
+ if (this.hasEstablishedConnection && this.isInitialized) {
204
226
  log(
205
- "error",
206
- "[Flagix SDK] Failed to refresh cache after reconnection",
227
+ "info",
228
+ "[Flagix SDK] SSE reconnected. Refreshing cache to sync with server..."
229
+ );
230
+ this.fetchInitialConfig().catch((error) => {
231
+ log(
232
+ "error",
233
+ "[Flagix SDK] Failed to refresh cache after reconnection",
234
+ error
235
+ );
236
+ });
237
+ } else {
238
+ this.hasEstablishedConnection = true;
239
+ }
240
+ log("info", "[Flagix SDK] SSE connection established.");
241
+ };
242
+ source.onerror = (error) => {
243
+ const eventSource = error.target;
244
+ const readyState = eventSource?.readyState;
245
+ if (readyState === 2) {
246
+ log(
247
+ "warn",
248
+ "[Flagix SDK] SSE connection closed. Attempting to reconnect..."
249
+ );
250
+ this.handleReconnect();
251
+ } else if (readyState === 0) {
252
+ log(
253
+ "warn",
254
+ "[Flagix SDK] SSE connection error (connecting state)",
207
255
  error
208
256
  );
209
- });
210
- } else {
211
- this.hasEstablishedConnection = true;
212
- }
213
- log("info", "[Flagix SDK] SSE connection established.");
214
- };
215
- source.onerror = (error) => {
216
- const eventSource = error.target;
217
- const readyState = eventSource?.readyState;
218
- if (readyState === 2) {
219
- log(
220
- "warn",
221
- "[Flagix SDK] SSE connection closed. Attempting to reconnect..."
222
- );
223
- this.handleReconnect();
224
- } else if (readyState === 0) {
225
- log(
226
- "warn",
227
- "[Flagix SDK] SSE connection error (connecting state)",
228
- error
229
- );
230
- } else {
231
- log("error", "[Flagix SDK] SSE error", error);
232
- this.handleReconnect();
233
- }
234
- };
235
- source.addEventListener("connected", () => {
236
- log("info", "[Flagix SDK] SSE connection confirmed by server.");
237
- });
238
- source.addEventListener(EVENT_TO_LISTEN, (event) => {
239
- try {
240
- const data = JSON.parse(event.data);
241
- const { flagKey, type } = data;
242
- log("info", `[Flagix SDK] Received update for ${flagKey} (${type}).`);
243
- this.fetchSingleFlagConfig(flagKey, type);
244
- } catch (error) {
245
- log("error", "[Flagix SDK] Failed to parse SSE event data.", error);
246
- }
247
- });
257
+ } else {
258
+ log("error", "[Flagix SDK] SSE error", error);
259
+ this.handleReconnect();
260
+ }
261
+ };
262
+ source.addEventListener("connected", () => {
263
+ log("info", "[Flagix SDK] SSE connection confirmed by server.");
264
+ });
265
+ source.addEventListener(EVENT_TO_LISTEN, (event) => {
266
+ try {
267
+ const data = JSON.parse(event.data);
268
+ const { flagKey, type } = data;
269
+ log("info", `[Flagix SDK] Received update for ${flagKey} (${type}).`);
270
+ this.fetchSingleFlagConfig(flagKey, type);
271
+ } catch (error) {
272
+ log("error", "[Flagix SDK] Failed to parse SSE event data.", error);
273
+ }
274
+ });
275
+ } catch (error) {
276
+ log("error", "[Flagix SDK] Failed during SSE setup", error);
277
+ this.handleReconnect();
278
+ } finally {
279
+ this.isConnectingSSE = false;
280
+ }
248
281
  }
249
282
  handleReconnect() {
250
283
  if (this.isReconnecting || !this.isInitialized) {
@@ -286,7 +319,7 @@ var FlagixClient = class {
286
319
  }
287
320
  async fetchSingleFlagConfig(flagKey, type) {
288
321
  const url = `${this.apiBaseUrl}/api/flag-config/${flagKey}`;
289
- if (type === "FLAG_DELETED" || type === "RULE_DELETED") {
322
+ if (type === "FLAG_DELETED") {
290
323
  this.localCache.delete(flagKey);
291
324
  log("info", `[Flagix SDK] Flag ${flagKey} deleted from cache.`);
292
325
  this.emitter.emit(FLAG_UPDATE_EVENT, flagKey);
@@ -424,24 +457,37 @@ var Flagix = {
424
457
  */
425
458
  async initialize(options) {
426
459
  if (clientInstance) {
427
- log("warn", "Flagix SDK already initialized. Ignoring subsequent call.");
428
- return;
460
+ if (clientInstance.getApiKey() === options.apiKey) {
461
+ if (clientInstance.getIsInitialized()) {
462
+ return;
463
+ }
464
+ if (isInitializing && initializationPromise) {
465
+ return initializationPromise;
466
+ }
467
+ } else {
468
+ log(
469
+ "info",
470
+ "[Flagix SDK] API Key change detected. Resetting client..."
471
+ );
472
+ this.close();
473
+ }
429
474
  }
430
475
  if (isInitializing && initializationPromise) {
431
476
  return initializationPromise;
432
477
  }
433
478
  isInitializing = true;
434
- try {
435
- clientInstance = new FlagixClient(options);
436
- initializationPromise = clientInstance.initialize();
437
- await initializationPromise;
438
- } catch (error) {
439
- log("error", "Flagix SDK failed during initialization:", error);
440
- throw error;
441
- } finally {
442
- isInitializing = false;
443
- initializationPromise = null;
444
- }
479
+ initializationPromise = (async () => {
480
+ try {
481
+ clientInstance = new FlagixClient(options);
482
+ await clientInstance.initialize();
483
+ } catch (error) {
484
+ clientInstance = null;
485
+ throw error;
486
+ } finally {
487
+ isInitializing = false;
488
+ }
489
+ })();
490
+ return await initializationPromise;
445
491
  },
446
492
  /**
447
493
  * Evaluates a flag based on the local cache and current context.
@@ -474,6 +520,16 @@ var Flagix = {
474
520
  }
475
521
  clientInstance.track(eventName, properties, contextOverrides);
476
522
  },
523
+ /**
524
+ * Replaces the global evaluation context.
525
+ */
526
+ identify(newContext) {
527
+ if (!clientInstance) {
528
+ log("error", "Flagix SDK not initialized.");
529
+ return;
530
+ }
531
+ clientInstance.identify(newContext);
532
+ },
477
533
  /**
478
534
  * Sets or updates the global evaluation context.
479
535
  * @param newContext New context attributes to merge or replace.
@@ -492,6 +548,8 @@ var Flagix = {
492
548
  if (clientInstance) {
493
549
  clientInstance.close();
494
550
  clientInstance = null;
551
+ initializationPromise = null;
552
+ isInitializing = false;
495
553
  }
496
554
  },
497
555
  /**
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@flagix/js-sdk",
3
- "version": "1.2.0",
3
+ "version": "1.3.0",
4
4
  "description": "Flagix Javascript SDK",
5
5
  "main": "./dist/index.js",
6
6
  "module": "./dist/index.mjs",
@@ -30,7 +30,7 @@
30
30
  "dependencies": {
31
31
  "eventemitter3": "^5.0.1",
32
32
  "eventsource": "^4.1.0",
33
- "@flagix/evaluation-core": "^1.1.0"
33
+ "@flagix/evaluation-core": "^1.2.0"
34
34
  },
35
35
  "devDependencies": {
36
36
  "@types/node": "^22.15.3",
package/src/client.ts CHANGED
@@ -37,6 +37,7 @@ export class FlagixClient {
37
37
  private readonly maxReconnectAttempts = Number.POSITIVE_INFINITY;
38
38
  private readonly baseReconnectDelay = 1000;
39
39
  private readonly maxReconnectDelay = 30_000;
40
+ private isConnectingSSE = false;
40
41
 
41
42
  constructor(options: FlagixClientOptions) {
42
43
  this.apiKey = options.apiKey;
@@ -49,21 +50,25 @@ export class FlagixClient {
49
50
  /**
50
51
  * Subscribes a listener to a flag update event.
51
52
  */
52
- on(
53
+ on = (
53
54
  event: typeof FLAG_UPDATE_EVENT,
54
55
  listener: (flagKey: string) => void
55
- ): void {
56
+ ) => {
56
57
  this.emitter.on(event, listener);
57
- }
58
+ };
58
59
 
59
60
  /**
60
61
  * Unsubscribes a listener from a flag update event.
61
62
  */
62
- off(
63
+ off = (
63
64
  event: typeof FLAG_UPDATE_EVENT,
64
65
  listener: (flagKey: string) => void
65
- ): void {
66
+ ) => {
66
67
  this.emitter.off(event, listener);
68
+ };
69
+
70
+ getApiKey(): string {
71
+ return this.apiKey;
67
72
  }
68
73
 
69
74
  /**
@@ -125,6 +130,15 @@ export class FlagixClient {
125
130
  return (result?.value as T) ?? null;
126
131
  }
127
132
 
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
+
128
142
  /**
129
143
  * Sets or updates the global evaluation context.
130
144
  * @param newContext New context attributes to merge or replace.
@@ -135,7 +149,13 @@ export class FlagixClient {
135
149
  "info",
136
150
  "[Flagix SDK] Context updated. Evaluations will use the new context."
137
151
  );
152
+ this.refreshAllFlags();
153
+ }
138
154
 
155
+ /**
156
+ * Helper to refresh all flags by emitting update events for each cached flag.
157
+ */
158
+ private refreshAllFlags(): void {
139
159
  for (const flagKey of this.localCache.keys()) {
140
160
  this.emitter.emit(FLAG_UPDATE_EVENT, flagKey);
141
161
  }
@@ -172,6 +192,10 @@ export class FlagixClient {
172
192
  }
173
193
 
174
194
  private async setupSSEListener(): Promise<void> {
195
+ if (this.isConnectingSSE) {
196
+ return;
197
+ }
198
+
175
199
  if (this.sseConnection) {
176
200
  try {
177
201
  this.sseConnection.close();
@@ -185,89 +209,102 @@ export class FlagixClient {
185
209
  this.sseConnection = null;
186
210
  }
187
211
 
212
+ this.isConnectingSSE = true;
188
213
  const url = `${this.apiBaseUrl}/api/sse/stream`;
189
214
 
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;
205
221
  }
206
222
 
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;
223
226
  }
224
227
 
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;
231
229
 
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
+ }
250
237
 
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
+ });
255
286
 
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
+ };
263
294
 
264
- log("info", `[Flagix SDK] Received update for ${flagKey} (${type}).`);
295
+ log("info", `[Flagix SDK] Received update for ${flagKey} (${type}).`);
265
296
 
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
+ }
271
308
  }
272
309
 
273
310
  private handleReconnect(): void {
@@ -324,7 +361,7 @@ export class FlagixClient {
324
361
  ): Promise<void> {
325
362
  const url = `${this.apiBaseUrl}/api/flag-config/${flagKey}`;
326
363
 
327
- if (type === "FLAG_DELETED" || type === "RULE_DELETED") {
364
+ if (type === "FLAG_DELETED") {
328
365
  this.localCache.delete(flagKey);
329
366
  log("info", `[Flagix SDK] Flag ${flagKey} deleted from cache.`);
330
367
  this.emitter.emit(FLAG_UPDATE_EVENT, flagKey);
package/src/index.ts CHANGED
@@ -17,9 +17,24 @@ export const Flagix = {
17
17
  * Initializes the Flagix SDK, fetches all flags, and sets up an SSE connection.
18
18
  */
19
19
  async initialize(options: FlagixClientOptions): Promise<void> {
20
+ // this check ensures that we are able to watch for api key changes and re-initialize accordingly
21
+ // this ensures we dont use stale clients across different api keys
20
22
  if (clientInstance) {
21
- log("warn", "Flagix SDK already initialized. Ignoring subsequent call.");
22
- return;
23
+ if (clientInstance.getApiKey() === options.apiKey) {
24
+ if (clientInstance.getIsInitialized()) {
25
+ return;
26
+ }
27
+
28
+ if (isInitializing && initializationPromise) {
29
+ return initializationPromise;
30
+ }
31
+ } else {
32
+ log(
33
+ "info",
34
+ "[Flagix SDK] API Key change detected. Resetting client..."
35
+ );
36
+ this.close();
37
+ }
23
38
  }
24
39
 
25
40
  if (isInitializing && initializationPromise) {
@@ -28,17 +43,19 @@ export const Flagix = {
28
43
 
29
44
  isInitializing = true;
30
45
 
31
- try {
32
- clientInstance = new FlagixClient(options);
33
- initializationPromise = clientInstance.initialize();
34
- await initializationPromise;
35
- } catch (error) {
36
- log("error", "Flagix SDK failed during initialization:", error);
37
- throw error;
38
- } finally {
39
- isInitializing = false;
40
- initializationPromise = null;
41
- }
46
+ initializationPromise = (async () => {
47
+ try {
48
+ clientInstance = new FlagixClient(options);
49
+ await clientInstance.initialize();
50
+ } catch (error) {
51
+ clientInstance = null;
52
+ throw error;
53
+ } finally {
54
+ isInitializing = false;
55
+ }
56
+ })();
57
+
58
+ return await initializationPromise;
42
59
  },
43
60
 
44
61
  /**
@@ -82,6 +99,17 @@ export const Flagix = {
82
99
  clientInstance.track(eventName, properties, contextOverrides);
83
100
  },
84
101
 
102
+ /**
103
+ * Replaces the global evaluation context.
104
+ */
105
+ identify(newContext: EvaluationContext): void {
106
+ if (!clientInstance) {
107
+ log("error", "Flagix SDK not initialized.");
108
+ return;
109
+ }
110
+ clientInstance.identify(newContext);
111
+ },
112
+
85
113
  /**
86
114
  * Sets or updates the global evaluation context.
87
115
  * @param newContext New context attributes to merge or replace.
@@ -101,6 +129,8 @@ export const Flagix = {
101
129
  if (clientInstance) {
102
130
  clientInstance.close();
103
131
  clientInstance = null;
132
+ initializationPromise = null;
133
+ isInitializing = false;
104
134
  }
105
135
  },
106
136