@flagix/js-sdk 1.2.0 → 1.3.1

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.1 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
+ CJS dist/index.js 18.09 KB
13
+ CJS ⚡️ Build success in 48ms
14
+ ESM dist/index.mjs 16.41 KB
15
+ ESM ⚡️ Build success in 48ms
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 1875ms
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,22 @@
1
1
  # @flagix/js-sdk
2
2
 
3
+ ## 1.3.1
4
+
5
+ ### Patch Changes
6
+
7
+ - 818fc23: Fixed an issue where evaluation and event tracking requests were being blocked by certain browser privacy extensions and ad-blockers. Replaced navigator.sendBeacon with fetch + keepalive to improve the delivery reliability
8
+
9
+ ## 1.3.0
10
+
11
+ ### Minor Changes
12
+
13
+ - 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.
14
+
15
+ ### Patch Changes
16
+
17
+ - Updated dependencies [4e879d8]
18
+ - @flagix/evaluation-core@1.2.0
19
+
3
20
  ## 1.2.0
4
21
 
5
22
  ### 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);
@@ -373,12 +406,6 @@ var FlagixClient = class {
373
406
  timestamp: (/* @__PURE__ */ new Date()).toISOString()
374
407
  };
375
408
  const payloadJson = JSON.stringify(payload);
376
- if (typeof navigator !== "undefined" && navigator.sendBeacon) {
377
- const blob = new Blob([payloadJson], { type: "application/json" });
378
- if (navigator.sendBeacon(url, blob)) {
379
- return;
380
- }
381
- }
382
409
  this.fireAndForgetFetch(url, payloadJson);
383
410
  }
384
411
  /**
@@ -422,18 +449,7 @@ var FlagixClient = class {
422
449
  evaluatedAt: (/* @__PURE__ */ new Date()).toISOString()
423
450
  };
424
451
  const payloadJson = JSON.stringify(payload);
425
- if (typeof navigator !== "undefined" && navigator.sendBeacon) {
426
- const blob = new Blob([payloadJson], { type: "application/json" });
427
- const success = navigator.sendBeacon(url, blob);
428
- if (success) {
429
- log("info", `Successfully queued beacon for ${flagKey}.`);
430
- return;
431
- }
432
- log("warn", `Beacon queue full for ${flagKey}. Falling back to fetch.`);
433
- this.fireAndForgetFetch(url, payloadJson);
434
- } else {
435
- this.fireAndForgetFetch(url, payloadJson);
436
- }
452
+ this.fireAndForgetFetch(url, payloadJson);
437
453
  }
438
454
  fireAndForgetFetch(url, payloadJson) {
439
455
  fetch(url, {
@@ -460,24 +476,37 @@ var Flagix = {
460
476
  */
461
477
  async initialize(options) {
462
478
  if (clientInstance) {
463
- log("warn", "Flagix SDK already initialized. Ignoring subsequent call.");
464
- return;
479
+ if (clientInstance.getApiKey() === options.apiKey) {
480
+ if (clientInstance.getIsInitialized()) {
481
+ return;
482
+ }
483
+ if (isInitializing && initializationPromise) {
484
+ return initializationPromise;
485
+ }
486
+ } else {
487
+ log(
488
+ "info",
489
+ "[Flagix SDK] API Key change detected. Resetting client..."
490
+ );
491
+ this.close();
492
+ }
465
493
  }
466
494
  if (isInitializing && initializationPromise) {
467
495
  return initializationPromise;
468
496
  }
469
497
  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
- }
498
+ initializationPromise = (async () => {
499
+ try {
500
+ clientInstance = new FlagixClient(options);
501
+ await clientInstance.initialize();
502
+ } catch (error) {
503
+ clientInstance = null;
504
+ throw error;
505
+ } finally {
506
+ isInitializing = false;
507
+ }
508
+ })();
509
+ return await initializationPromise;
481
510
  },
482
511
  /**
483
512
  * Evaluates a flag based on the local cache and current context.
@@ -510,6 +539,16 @@ var Flagix = {
510
539
  }
511
540
  clientInstance.track(eventName, properties, contextOverrides);
512
541
  },
542
+ /**
543
+ * Replaces the global evaluation context.
544
+ */
545
+ identify(newContext) {
546
+ if (!clientInstance) {
547
+ log("error", "Flagix SDK not initialized.");
548
+ return;
549
+ }
550
+ clientInstance.identify(newContext);
551
+ },
513
552
  /**
514
553
  * Sets or updates the global evaluation context.
515
554
  * @param newContext New context attributes to merge or replace.
@@ -528,6 +567,8 @@ var Flagix = {
528
567
  if (clientInstance) {
529
568
  clientInstance.close();
530
569
  clientInstance = null;
570
+ initializationPromise = null;
571
+ isInitializing = false;
531
572
  }
532
573
  },
533
574
  /**
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);
@@ -337,12 +370,6 @@ var FlagixClient = class {
337
370
  timestamp: (/* @__PURE__ */ new Date()).toISOString()
338
371
  };
339
372
  const payloadJson = JSON.stringify(payload);
340
- if (typeof navigator !== "undefined" && navigator.sendBeacon) {
341
- const blob = new Blob([payloadJson], { type: "application/json" });
342
- if (navigator.sendBeacon(url, blob)) {
343
- return;
344
- }
345
- }
346
373
  this.fireAndForgetFetch(url, payloadJson);
347
374
  }
348
375
  /**
@@ -386,18 +413,7 @@ var FlagixClient = class {
386
413
  evaluatedAt: (/* @__PURE__ */ new Date()).toISOString()
387
414
  };
388
415
  const payloadJson = JSON.stringify(payload);
389
- if (typeof navigator !== "undefined" && navigator.sendBeacon) {
390
- const blob = new Blob([payloadJson], { type: "application/json" });
391
- const success = navigator.sendBeacon(url, blob);
392
- if (success) {
393
- log("info", `Successfully queued beacon for ${flagKey}.`);
394
- return;
395
- }
396
- log("warn", `Beacon queue full for ${flagKey}. Falling back to fetch.`);
397
- this.fireAndForgetFetch(url, payloadJson);
398
- } else {
399
- this.fireAndForgetFetch(url, payloadJson);
400
- }
416
+ this.fireAndForgetFetch(url, payloadJson);
401
417
  }
402
418
  fireAndForgetFetch(url, payloadJson) {
403
419
  fetch(url, {
@@ -424,24 +440,37 @@ var Flagix = {
424
440
  */
425
441
  async initialize(options) {
426
442
  if (clientInstance) {
427
- log("warn", "Flagix SDK already initialized. Ignoring subsequent call.");
428
- return;
443
+ if (clientInstance.getApiKey() === options.apiKey) {
444
+ if (clientInstance.getIsInitialized()) {
445
+ return;
446
+ }
447
+ if (isInitializing && initializationPromise) {
448
+ return initializationPromise;
449
+ }
450
+ } else {
451
+ log(
452
+ "info",
453
+ "[Flagix SDK] API Key change detected. Resetting client..."
454
+ );
455
+ this.close();
456
+ }
429
457
  }
430
458
  if (isInitializing && initializationPromise) {
431
459
  return initializationPromise;
432
460
  }
433
461
  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
- }
462
+ initializationPromise = (async () => {
463
+ try {
464
+ clientInstance = new FlagixClient(options);
465
+ await clientInstance.initialize();
466
+ } catch (error) {
467
+ clientInstance = null;
468
+ throw error;
469
+ } finally {
470
+ isInitializing = false;
471
+ }
472
+ })();
473
+ return await initializationPromise;
445
474
  },
446
475
  /**
447
476
  * Evaluates a flag based on the local cache and current context.
@@ -474,6 +503,16 @@ var Flagix = {
474
503
  }
475
504
  clientInstance.track(eventName, properties, contextOverrides);
476
505
  },
506
+ /**
507
+ * Replaces the global evaluation context.
508
+ */
509
+ identify(newContext) {
510
+ if (!clientInstance) {
511
+ log("error", "Flagix SDK not initialized.");
512
+ return;
513
+ }
514
+ clientInstance.identify(newContext);
515
+ },
477
516
  /**
478
517
  * Sets or updates the global evaluation context.
479
518
  * @param newContext New context attributes to merge or replace.
@@ -492,6 +531,8 @@ var Flagix = {
492
531
  if (clientInstance) {
493
532
  clientInstance.close();
494
533
  clientInstance = null;
534
+ initializationPromise = null;
535
+ isInitializing = false;
495
536
  }
496
537
  },
497
538
  /**
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.1",
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
- };
228
+ this.sseConnection = source;
227
229
 
228
- source.onerror = (error) => {
229
- const eventSource = error.target as EventSource;
230
- const readyState = eventSource?.readyState;
230
+ source.onopen = () => {
231
+ this.reconnectAttempts = 0;
232
+ this.isReconnecting = false;
233
+ if (this.reconnectTimeoutId) {
234
+ clearTimeout(this.reconnectTimeoutId);
235
+ this.reconnectTimeoutId = null;
236
+ }
231
237
 
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
- };
250
-
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);
@@ -390,13 +427,6 @@ export class FlagixClient {
390
427
 
391
428
  const payloadJson = JSON.stringify(payload);
392
429
 
393
- if (typeof navigator !== "undefined" && navigator.sendBeacon) {
394
- const blob = new Blob([payloadJson], { type: "application/json" });
395
- if (navigator.sendBeacon(url, blob)) {
396
- return;
397
- }
398
- }
399
-
400
430
  this.fireAndForgetFetch(url, payloadJson);
401
431
  }
402
432
 
@@ -453,21 +483,7 @@ export class FlagixClient {
453
483
 
454
484
  const payloadJson = JSON.stringify(payload);
455
485
 
456
- if (typeof navigator !== "undefined" && navigator.sendBeacon) {
457
- const blob = new Blob([payloadJson], { type: "application/json" });
458
-
459
- const success = navigator.sendBeacon(url, blob);
460
-
461
- if (success) {
462
- log("info", `Successfully queued beacon for ${flagKey}.`);
463
- return;
464
- }
465
-
466
- log("warn", `Beacon queue full for ${flagKey}. Falling back to fetch.`);
467
- this.fireAndForgetFetch(url, payloadJson);
468
- } else {
469
- this.fireAndForgetFetch(url, payloadJson);
470
- }
486
+ this.fireAndForgetFetch(url, payloadJson);
471
487
  }
472
488
 
473
489
  private fireAndForgetFetch(url: string, payloadJson: string): void {
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