@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.
- package/.turbo/turbo-build.log +8 -8
- package/CHANGELOG.md +17 -0
- package/dist/index.d.mts +4 -0
- package/dist/index.d.ts +4 -0
- package/dist/index.js +144 -103
- package/dist/index.mjs +144 -103
- package/package.json +2 -2
- package/src/client.ts +116 -100
- package/src/index.ts +43 -13
package/.turbo/turbo-build.log
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
|
|
2
|
-
> @flagix/js-sdk@1.
|
|
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
|
[34mCLI[39m Building entry: src/index.ts
|
|
@@ -9,11 +9,11 @@
|
|
|
9
9
|
[34mCLI[39m Cleaning output folder
|
|
10
10
|
[34mCJS[39m Build start
|
|
11
11
|
[34mESM[39m Build start
|
|
12
|
-
[
|
|
13
|
-
[
|
|
14
|
-
[
|
|
15
|
-
[
|
|
12
|
+
[32mCJS[39m [1mdist/index.js [22m[32m18.09 KB[39m
|
|
13
|
+
[32mCJS[39m ⚡️ Build success in 48ms
|
|
14
|
+
[32mESM[39m [1mdist/index.mjs [22m[32m16.41 KB[39m
|
|
15
|
+
[32mESM[39m ⚡️ Build success in 48ms
|
|
16
16
|
[34mDTS[39m Build start
|
|
17
|
-
[32mDTS[39m ⚡️ Build success in
|
|
18
|
-
[32mDTS[39m [1mdist/index.d.ts [22m[32m2.
|
|
19
|
-
[32mDTS[39m [1mdist/index.d.mts [22m[32m2.
|
|
17
|
+
[32mDTS[39m ⚡️ Build success in 1875ms
|
|
18
|
+
[32mDTS[39m [1mdist/index.d.ts [22m[32m2.41 KB[39m
|
|
19
|
+
[32mDTS[39m [1mdist/index.d.mts [22m[32m2.41 KB[39m
|
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
|
-
|
|
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
|
-
|
|
221
|
-
|
|
222
|
-
|
|
223
|
-
|
|
224
|
-
|
|
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.
|
|
235
|
-
|
|
236
|
-
|
|
237
|
-
|
|
238
|
-
|
|
239
|
-
|
|
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
|
-
"
|
|
242
|
-
"[Flagix SDK]
|
|
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
|
-
|
|
247
|
-
|
|
248
|
-
|
|
249
|
-
|
|
250
|
-
|
|
251
|
-
|
|
252
|
-
|
|
253
|
-
|
|
254
|
-
|
|
255
|
-
|
|
256
|
-
|
|
257
|
-
"[Flagix SDK]
|
|
258
|
-
|
|
259
|
-
|
|
260
|
-
|
|
261
|
-
|
|
262
|
-
|
|
263
|
-
|
|
264
|
-
|
|
265
|
-
|
|
266
|
-
|
|
267
|
-
|
|
268
|
-
|
|
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"
|
|
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
|
-
|
|
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
|
-
|
|
464
|
-
|
|
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
|
-
|
|
471
|
-
|
|
472
|
-
|
|
473
|
-
|
|
474
|
-
|
|
475
|
-
|
|
476
|
-
|
|
477
|
-
|
|
478
|
-
|
|
479
|
-
|
|
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
|
-
|
|
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
|
-
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
|
|
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.
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
|
|
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
|
-
"
|
|
206
|
-
"[Flagix SDK]
|
|
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
|
-
|
|
211
|
-
|
|
212
|
-
|
|
213
|
-
|
|
214
|
-
|
|
215
|
-
|
|
216
|
-
|
|
217
|
-
|
|
218
|
-
|
|
219
|
-
|
|
220
|
-
|
|
221
|
-
"[Flagix SDK]
|
|
222
|
-
|
|
223
|
-
|
|
224
|
-
|
|
225
|
-
|
|
226
|
-
|
|
227
|
-
|
|
228
|
-
|
|
229
|
-
|
|
230
|
-
|
|
231
|
-
|
|
232
|
-
|
|
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"
|
|
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
|
-
|
|
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
|
-
|
|
428
|
-
|
|
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
|
-
|
|
435
|
-
|
|
436
|
-
|
|
437
|
-
|
|
438
|
-
|
|
439
|
-
|
|
440
|
-
|
|
441
|
-
|
|
442
|
-
|
|
443
|
-
|
|
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.
|
|
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.
|
|
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
|
-
)
|
|
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
|
-
)
|
|
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
|
-
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
|
|
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
|
-
|
|
208
|
-
|
|
209
|
-
|
|
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
|
-
|
|
226
|
-
};
|
|
228
|
+
this.sseConnection = source;
|
|
227
229
|
|
|
228
|
-
|
|
229
|
-
|
|
230
|
-
|
|
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
|
-
|
|
233
|
-
|
|
234
|
-
|
|
235
|
-
|
|
236
|
-
|
|
237
|
-
|
|
238
|
-
|
|
239
|
-
|
|
240
|
-
|
|
241
|
-
|
|
242
|
-
|
|
243
|
-
|
|
244
|
-
|
|
245
|
-
|
|
246
|
-
|
|
247
|
-
|
|
248
|
-
|
|
249
|
-
|
|
250
|
-
|
|
251
|
-
|
|
252
|
-
|
|
253
|
-
|
|
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
|
-
|
|
257
|
-
|
|
258
|
-
|
|
259
|
-
|
|
260
|
-
|
|
261
|
-
|
|
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
|
-
|
|
295
|
+
log("info", `[Flagix SDK] Received update for ${flagKey} (${type}).`);
|
|
265
296
|
|
|
266
|
-
|
|
267
|
-
|
|
268
|
-
|
|
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"
|
|
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
|
-
|
|
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
|
-
|
|
22
|
-
|
|
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
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
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
|
|