flipflag-sdk 1.1.21 → 1.1.23
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/dist/index.js +134 -16
- package/dist/index.js.map +1 -1
- package/dist/index.mjs +134 -16
- package/dist/index.mjs.map +1 -1
- package/dist/js.js +144 -16
- package/dist/js.js.map +1 -1
- package/dist/js.mjs +144 -16
- package/dist/js.mjs.map +1 -1
- package/dist/next.js +203 -82
- package/dist/next.js.map +1 -1
- package/dist/next.mjs +203 -82
- package/dist/next.mjs.map +1 -1
- package/dist/react-native.js +134 -16
- package/dist/react-native.js.map +1 -1
- package/dist/react-native.mjs +134 -16
- package/dist/react-native.mjs.map +1 -1
- package/dist/react.js +291 -120
- package/dist/react.js.map +1 -1
- package/dist/react.mjs +292 -121
- package/dist/react.mjs.map +1 -1
- package/dist/types/core/FlipFlagSDK.d.ts +23 -2
- package/dist/types/js/featureFlagManager.d.ts +4 -0
- package/dist/types/next/server-utils.d.ts +2 -2
- package/dist/types/next/types.d.ts +1 -0
- package/dist/types/types/index.d.ts +1 -1
- package/dist/vue.js +134 -16
- package/dist/vue.js.map +1 -1
- package/dist/vue.mjs +134 -16
- package/dist/vue.mjs.map +1 -1
- package/package.json +1 -1
package/dist/react.js
CHANGED
|
@@ -8,6 +8,9 @@ class FlipFlagSDK {
|
|
|
8
8
|
this.cache = new Map();
|
|
9
9
|
this.defaultBaseUrl = "https://app.flipflag.ru";
|
|
10
10
|
this.stateListeners = new Set();
|
|
11
|
+
this.maxListeners = 100; // Maximum number of listeners to prevent memory leaks
|
|
12
|
+
// Validate required configuration
|
|
13
|
+
this.validateConfig(config);
|
|
11
14
|
this.config = {
|
|
12
15
|
baseUrl: this.defaultBaseUrl,
|
|
13
16
|
cacheTimeout: 30000, // 30 seconds
|
|
@@ -32,9 +35,18 @@ class FlipFlagSDK {
|
|
|
32
35
|
* Identify user for A/B testing and user-specific flags
|
|
33
36
|
*/
|
|
34
37
|
identifyUser(options) {
|
|
38
|
+
if (!options || typeof options !== "object") {
|
|
39
|
+
throw new Error("options must be an object");
|
|
40
|
+
}
|
|
41
|
+
if (!options.userId || typeof options.userId !== "string") {
|
|
42
|
+
throw new Error("userId is required and must be a string");
|
|
43
|
+
}
|
|
44
|
+
if (options.userProperties && typeof options.userProperties !== "object") {
|
|
45
|
+
throw new Error("userProperties must be an object");
|
|
46
|
+
}
|
|
35
47
|
this.config.userId = options.userId;
|
|
36
48
|
// Clear A/B test cache since user changed
|
|
37
|
-
this.
|
|
49
|
+
this.clearAllABTestCache();
|
|
38
50
|
// If we have a user, we might want to refresh immediately
|
|
39
51
|
if (this.config.pullInterval && this.config.pullInterval > 0) {
|
|
40
52
|
this.pullData();
|
|
@@ -50,6 +62,9 @@ class FlipFlagSDK {
|
|
|
50
62
|
* Subscribe to state changes
|
|
51
63
|
*/
|
|
52
64
|
subscribe(callback) {
|
|
65
|
+
if (this.stateListeners.size >= this.maxListeners) {
|
|
66
|
+
console.warn(`Maximum number of listeners (${this.maxListeners}) reached. This may cause memory leaks.`);
|
|
67
|
+
}
|
|
53
68
|
this.stateListeners.add(callback);
|
|
54
69
|
// Return unsubscribe function
|
|
55
70
|
return () => {
|
|
@@ -160,6 +175,8 @@ class FlipFlagSDK {
|
|
|
160
175
|
}
|
|
161
176
|
catch (error) {
|
|
162
177
|
console.error("State listener error:", error);
|
|
178
|
+
// Remove faulty listener to prevent repeated errors
|
|
179
|
+
this.stateListeners.delete(callback);
|
|
163
180
|
}
|
|
164
181
|
});
|
|
165
182
|
}
|
|
@@ -196,9 +213,9 @@ class FlipFlagSDK {
|
|
|
196
213
|
}
|
|
197
214
|
}
|
|
198
215
|
/**
|
|
199
|
-
* Clear A/B test cache
|
|
216
|
+
* Clear all A/B test cache (private method)
|
|
200
217
|
*/
|
|
201
|
-
|
|
218
|
+
clearAllABTestCache() {
|
|
202
219
|
const abTestKeys = Array.from(this.cache.keys()).filter((key) => key.startsWith("abtest:"));
|
|
203
220
|
abTestKeys.forEach((key) => this.cache.delete(key));
|
|
204
221
|
}
|
|
@@ -224,6 +241,15 @@ class FlipFlagSDK {
|
|
|
224
241
|
*/
|
|
225
242
|
async getFlag(flagName, projectId = this.config.projectId, environment) {
|
|
226
243
|
var _a, _b;
|
|
244
|
+
if (!flagName || typeof flagName !== "string") {
|
|
245
|
+
throw new Error("flagName is required and must be a string");
|
|
246
|
+
}
|
|
247
|
+
if (!projectId || typeof projectId !== "string") {
|
|
248
|
+
throw new Error("projectId must be a string");
|
|
249
|
+
}
|
|
250
|
+
if (environment && typeof environment !== "string") {
|
|
251
|
+
throw new Error("environment must be a string");
|
|
252
|
+
}
|
|
227
253
|
// If we have the flag in state, return it immediately
|
|
228
254
|
if (this.state.flags[flagName] !== undefined) {
|
|
229
255
|
return {
|
|
@@ -235,20 +261,18 @@ class FlipFlagSDK {
|
|
|
235
261
|
};
|
|
236
262
|
}
|
|
237
263
|
// Check if we're currently loading data (pullData in progress)
|
|
238
|
-
// In this case, wait
|
|
264
|
+
// In this case, wait for loading to complete using Promise
|
|
239
265
|
if (this.state.isLoading) {
|
|
240
|
-
|
|
241
|
-
|
|
242
|
-
|
|
243
|
-
|
|
244
|
-
|
|
245
|
-
|
|
246
|
-
|
|
247
|
-
|
|
248
|
-
|
|
249
|
-
|
|
250
|
-
};
|
|
251
|
-
}
|
|
266
|
+
await this.waitForLoadingComplete();
|
|
267
|
+
// After waiting, check state again
|
|
268
|
+
if (this.state.flags[flagName] !== undefined) {
|
|
269
|
+
return {
|
|
270
|
+
projectId: this.config.projectId,
|
|
271
|
+
flagName,
|
|
272
|
+
environment: this.config.environment || "all",
|
|
273
|
+
value: this.state.flags[flagName],
|
|
274
|
+
timestamp: ((_b = this.state.lastFetch) === null || _b === void 0 ? void 0 : _b.toISOString()) || new Date().toISOString(),
|
|
275
|
+
};
|
|
252
276
|
}
|
|
253
277
|
}
|
|
254
278
|
const cacheKey = `flag:${projectId}:${flagName}:${environment || "all"}`;
|
|
@@ -264,10 +288,76 @@ class FlipFlagSDK {
|
|
|
264
288
|
this.setCache(cacheKey, response, this.config.cacheTimeout);
|
|
265
289
|
return response;
|
|
266
290
|
}
|
|
291
|
+
/**
|
|
292
|
+
* Validate configuration
|
|
293
|
+
*/
|
|
294
|
+
validateConfig(config) {
|
|
295
|
+
if (!config.apiKey || typeof config.apiKey !== "string") {
|
|
296
|
+
throw new Error("apiKey is required and must be a string");
|
|
297
|
+
}
|
|
298
|
+
if (!config.projectId || typeof config.projectId !== "string") {
|
|
299
|
+
throw new Error("projectId is required and must be a string");
|
|
300
|
+
}
|
|
301
|
+
if (config.baseUrl && typeof config.baseUrl !== "string") {
|
|
302
|
+
throw new Error("baseUrl must be a string");
|
|
303
|
+
}
|
|
304
|
+
if (config.environment && typeof config.environment !== "string") {
|
|
305
|
+
throw new Error("environment must be a string");
|
|
306
|
+
}
|
|
307
|
+
if (config.userId && typeof config.userId !== "string") {
|
|
308
|
+
throw new Error("userId must be a string");
|
|
309
|
+
}
|
|
310
|
+
if (config.cacheTimeout &&
|
|
311
|
+
(typeof config.cacheTimeout !== "number" || config.cacheTimeout < 0)) {
|
|
312
|
+
throw new Error("cacheTimeout must be a non-negative number");
|
|
313
|
+
}
|
|
314
|
+
if (config.retryAttempts &&
|
|
315
|
+
(typeof config.retryAttempts !== "number" || config.retryAttempts < 0)) {
|
|
316
|
+
throw new Error("retryAttempts must be a non-negative number");
|
|
317
|
+
}
|
|
318
|
+
if (config.retryDelay &&
|
|
319
|
+
(typeof config.retryDelay !== "number" || config.retryDelay < 0)) {
|
|
320
|
+
throw new Error("retryDelay must be a non-negative number");
|
|
321
|
+
}
|
|
322
|
+
if (config.pullInterval &&
|
|
323
|
+
(typeof config.pullInterval !== "number" || config.pullInterval < 0)) {
|
|
324
|
+
throw new Error("pullInterval must be a non-negative number");
|
|
325
|
+
}
|
|
326
|
+
}
|
|
327
|
+
/**
|
|
328
|
+
* Wait for loading to complete with timeout
|
|
329
|
+
*/
|
|
330
|
+
async waitForLoadingComplete(timeout = 5000) {
|
|
331
|
+
const startTime = Date.now();
|
|
332
|
+
return new Promise((resolve, reject) => {
|
|
333
|
+
const checkLoading = () => {
|
|
334
|
+
if (!this.state.isLoading) {
|
|
335
|
+
resolve();
|
|
336
|
+
return;
|
|
337
|
+
}
|
|
338
|
+
if (Date.now() - startTime > timeout) {
|
|
339
|
+
reject(new Error("Timeout waiting for SDK loading to complete"));
|
|
340
|
+
return;
|
|
341
|
+
}
|
|
342
|
+
// Check again after a short delay
|
|
343
|
+
setTimeout(checkLoading, 50);
|
|
344
|
+
};
|
|
345
|
+
checkLoading();
|
|
346
|
+
});
|
|
347
|
+
}
|
|
267
348
|
/**
|
|
268
349
|
* Get A/B test variant for a user
|
|
269
350
|
*/
|
|
270
351
|
async getAbTestVariant(testName, userId, projectId = this.config.projectId) {
|
|
352
|
+
if (!testName || typeof testName !== "string") {
|
|
353
|
+
throw new Error("testName is required and must be a string");
|
|
354
|
+
}
|
|
355
|
+
if (!userId || typeof userId !== "string") {
|
|
356
|
+
throw new Error("userId is required and must be a string");
|
|
357
|
+
}
|
|
358
|
+
if (!projectId || typeof projectId !== "string") {
|
|
359
|
+
throw new Error("projectId must be a string");
|
|
360
|
+
}
|
|
271
361
|
// If we have the A/B test in state, return it immediately
|
|
272
362
|
const stateVariant = this.state.abTests[testName];
|
|
273
363
|
if (stateVariant && stateVariant.testName === testName) {
|
|
@@ -331,6 +421,34 @@ class FlipFlagSDK {
|
|
|
331
421
|
this.cache.clear();
|
|
332
422
|
}
|
|
333
423
|
}
|
|
424
|
+
/**
|
|
425
|
+
* Clear cache for specific flag
|
|
426
|
+
*/
|
|
427
|
+
clearFlagCache(flagName, environment) {
|
|
428
|
+
const cacheKey = `flag:${this.config.projectId}:${flagName}:${environment || "all"}`;
|
|
429
|
+
this.cache.delete(cacheKey);
|
|
430
|
+
}
|
|
431
|
+
/**
|
|
432
|
+
* Clear cache for all flags
|
|
433
|
+
*/
|
|
434
|
+
clearFlagsCache(environment) {
|
|
435
|
+
const cacheKey = `flags:${this.config.projectId}:${environment || "all"}`;
|
|
436
|
+
this.cache.delete(cacheKey);
|
|
437
|
+
}
|
|
438
|
+
/**
|
|
439
|
+
* Clear cache for A/B test variant
|
|
440
|
+
*/
|
|
441
|
+
clearABTestCache(testName, userId) {
|
|
442
|
+
if (testName && userId) {
|
|
443
|
+
const cacheKey = `abtest:${this.config.projectId}:${testName}:${userId}`;
|
|
444
|
+
this.cache.delete(cacheKey);
|
|
445
|
+
}
|
|
446
|
+
else {
|
|
447
|
+
// Clear all A/B test cache
|
|
448
|
+
const abTestKeys = Array.from(this.cache.keys()).filter((key) => key.startsWith("abtest:"));
|
|
449
|
+
abTestKeys.forEach((key) => this.cache.delete(key));
|
|
450
|
+
}
|
|
451
|
+
}
|
|
334
452
|
/**
|
|
335
453
|
* Update configuration
|
|
336
454
|
*/
|
|
@@ -439,37 +557,18 @@ function useFeatureFlag(flagName, options = {}) {
|
|
|
439
557
|
const [loading, setLoading] = react.useState(true);
|
|
440
558
|
const [error, setError] = react.useState(null);
|
|
441
559
|
// Try to get SDK from context, otherwise create new instance
|
|
442
|
-
|
|
443
|
-
try {
|
|
444
|
-
const context = useFeatureFlagContext();
|
|
445
|
-
sdk = context.sdk;
|
|
446
|
-
}
|
|
447
|
-
catch (_b) {
|
|
448
|
-
if (!options.config) {
|
|
449
|
-
throw new Error("useFeatureFlag must be used within FeatureFlagProvider or config must be provided");
|
|
450
|
-
}
|
|
451
|
-
sdk = new FlipFlagSDK(options.config);
|
|
452
|
-
}
|
|
453
|
-
const updateFromState = react.useCallback(() => {
|
|
454
|
-
if (!options.enabled && options.enabled !== undefined) {
|
|
455
|
-
return;
|
|
456
|
-
}
|
|
560
|
+
const sdk = react.useMemo(() => {
|
|
457
561
|
try {
|
|
458
|
-
const
|
|
459
|
-
|
|
460
|
-
setLoading(false);
|
|
461
|
-
setError(null);
|
|
562
|
+
const context = useFeatureFlagContext();
|
|
563
|
+
return context.sdk;
|
|
462
564
|
}
|
|
463
|
-
catch (
|
|
464
|
-
|
|
465
|
-
|
|
466
|
-
// Use fallback value if provided
|
|
467
|
-
if (options.fallbackValue !== undefined) {
|
|
468
|
-
setValue(options.fallbackValue);
|
|
565
|
+
catch (_a) {
|
|
566
|
+
if (!options.config) {
|
|
567
|
+
throw new Error("useFeatureFlag must be used within FeatureFlagProvider or config must be provided");
|
|
469
568
|
}
|
|
470
|
-
|
|
569
|
+
return new FlipFlagSDK(options.config);
|
|
471
570
|
}
|
|
472
|
-
}, [
|
|
571
|
+
}, [options.config]);
|
|
473
572
|
const refetch = react.useCallback(async () => {
|
|
474
573
|
// Clear cache for this specific flag and force refresh
|
|
475
574
|
sdk.clearCache(`flag:${sdk["config"].projectId}:${flagName}:${sdk["config"].environment || "all"}`);
|
|
@@ -500,24 +599,52 @@ function useFeatureFlag(flagName, options = {}) {
|
|
|
500
599
|
}
|
|
501
600
|
}, [sdk, flagName, options.fallbackValue]);
|
|
502
601
|
react.useEffect(() => {
|
|
503
|
-
// Subscribe to state changes
|
|
602
|
+
// Subscribe to state changes with proper tracking to avoid infinite loops
|
|
603
|
+
let lastFetchTime = null;
|
|
504
604
|
const unsubscribe = sdk.subscribe((state) => {
|
|
505
605
|
if (!options.enabled && options.enabled !== undefined) {
|
|
506
606
|
return;
|
|
507
607
|
}
|
|
608
|
+
// Only update if this is a new fetch or if the flag value actually changed
|
|
508
609
|
const flagValue = state.flags[flagName];
|
|
509
|
-
|
|
610
|
+
const shouldUpdate = (state.lastFetch && state.lastFetch !== lastFetchTime) ||
|
|
611
|
+
(flagValue !== undefined && flagValue !== value);
|
|
612
|
+
if (shouldUpdate) {
|
|
613
|
+
if (state.lastFetch) {
|
|
614
|
+
lastFetchTime = state.lastFetch;
|
|
615
|
+
}
|
|
510
616
|
setValue(flagValue);
|
|
511
617
|
setLoading(false);
|
|
512
618
|
setError(state.error);
|
|
513
619
|
}
|
|
514
620
|
});
|
|
515
621
|
return unsubscribe;
|
|
516
|
-
}, [sdk, flagName, options.enabled]);
|
|
517
|
-
//
|
|
622
|
+
}, [sdk, flagName, options.enabled, value]);
|
|
623
|
+
// Initial load effect
|
|
518
624
|
react.useEffect(() => {
|
|
519
|
-
|
|
520
|
-
|
|
625
|
+
if (options.enabled !== false) {
|
|
626
|
+
try {
|
|
627
|
+
const flagValue = sdk.getFlagValue(flagName);
|
|
628
|
+
if (flagValue !== undefined) {
|
|
629
|
+
setValue(flagValue);
|
|
630
|
+
setLoading(false);
|
|
631
|
+
setError(null);
|
|
632
|
+
}
|
|
633
|
+
else {
|
|
634
|
+
// If not in state, trigger a fetch
|
|
635
|
+
refetch();
|
|
636
|
+
}
|
|
637
|
+
}
|
|
638
|
+
catch (err) {
|
|
639
|
+
const errorMessage = err instanceof Error ? err.message : "Unknown error";
|
|
640
|
+
setError(errorMessage);
|
|
641
|
+
if (options.fallbackValue !== undefined) {
|
|
642
|
+
setValue(options.fallbackValue);
|
|
643
|
+
}
|
|
644
|
+
setLoading(false);
|
|
645
|
+
}
|
|
646
|
+
}
|
|
647
|
+
}, [sdk, flagName, options.fallbackValue, options.enabled, refetch]);
|
|
521
648
|
return {
|
|
522
649
|
value,
|
|
523
650
|
loading,
|
|
@@ -526,6 +653,27 @@ function useFeatureFlag(flagName, options = {}) {
|
|
|
526
653
|
};
|
|
527
654
|
}
|
|
528
655
|
|
|
656
|
+
/**
|
|
657
|
+
* Wait for SDK loading to complete with timeout
|
|
658
|
+
*/
|
|
659
|
+
function waitForSDKLoading(sdk, timeout = 5000) {
|
|
660
|
+
const startTime = Date.now();
|
|
661
|
+
return new Promise((resolve, reject) => {
|
|
662
|
+
const checkLoading = () => {
|
|
663
|
+
if (!sdk.getState().isLoading) {
|
|
664
|
+
resolve();
|
|
665
|
+
return;
|
|
666
|
+
}
|
|
667
|
+
if (Date.now() - startTime > timeout) {
|
|
668
|
+
reject(new Error("Timeout waiting for SDK loading to complete"));
|
|
669
|
+
return;
|
|
670
|
+
}
|
|
671
|
+
// Check again after a short delay
|
|
672
|
+
setTimeout(checkLoading, 50);
|
|
673
|
+
};
|
|
674
|
+
checkLoading();
|
|
675
|
+
});
|
|
676
|
+
}
|
|
529
677
|
function useFeatureFlags(flagNames, options = {}) {
|
|
530
678
|
const [flags, setFlags] = react.useState(() => {
|
|
531
679
|
// Initialize with fallback values
|
|
@@ -540,25 +688,22 @@ function useFeatureFlags(flagNames, options = {}) {
|
|
|
540
688
|
const [error, setError] = react.useState(null);
|
|
541
689
|
const [isInitialized, setIsInitialized] = react.useState(false);
|
|
542
690
|
// Try to get SDK from context, otherwise create new instance
|
|
543
|
-
|
|
544
|
-
|
|
545
|
-
|
|
546
|
-
|
|
547
|
-
}
|
|
548
|
-
catch (_a) {
|
|
549
|
-
if (!options.config) {
|
|
550
|
-
throw new Error("useFeatureFlags must be used within FeatureFlagProvider or config must be provided");
|
|
691
|
+
const sdk = react.useMemo(() => {
|
|
692
|
+
try {
|
|
693
|
+
const context = useFeatureFlagContext();
|
|
694
|
+
return context.sdk;
|
|
551
695
|
}
|
|
552
|
-
|
|
553
|
-
|
|
696
|
+
catch (_a) {
|
|
697
|
+
if (!options.config) {
|
|
698
|
+
throw new Error("useFeatureFlags must be used within FeatureFlagProvider or config must be provided");
|
|
699
|
+
}
|
|
700
|
+
return new FlipFlagSDK(options.config);
|
|
701
|
+
}
|
|
702
|
+
}, [options.config]);
|
|
554
703
|
const fetchFlags = react.useCallback(async () => {
|
|
555
704
|
if (!options.enabled && options.enabled !== undefined) {
|
|
556
705
|
return;
|
|
557
706
|
}
|
|
558
|
-
// Prevent multiple simultaneous calls
|
|
559
|
-
if (isInitialized && loading) {
|
|
560
|
-
return;
|
|
561
|
-
}
|
|
562
707
|
try {
|
|
563
708
|
setLoading(true);
|
|
564
709
|
setError(null);
|
|
@@ -578,34 +723,35 @@ function useFeatureFlags(flagNames, options = {}) {
|
|
|
578
723
|
if (hasAllFlags) {
|
|
579
724
|
setFlags(stateFlags);
|
|
580
725
|
setLoading(false);
|
|
726
|
+
setIsInitialized(true);
|
|
581
727
|
return;
|
|
582
728
|
}
|
|
583
729
|
// If SDK is currently loading, wait for it to complete
|
|
584
730
|
if (sdk.getState().isLoading) {
|
|
585
|
-
|
|
586
|
-
|
|
587
|
-
|
|
588
|
-
const
|
|
589
|
-
|
|
590
|
-
|
|
591
|
-
const
|
|
592
|
-
|
|
593
|
-
|
|
594
|
-
|
|
595
|
-
|
|
596
|
-
|
|
597
|
-
}
|
|
598
|
-
else {
|
|
599
|
-
updatedHasAllFlags = false;
|
|
600
|
-
}
|
|
601
|
-
});
|
|
602
|
-
if (updatedHasAllFlags) {
|
|
603
|
-
setFlags(updatedStateFlags);
|
|
604
|
-
setLoading(false);
|
|
605
|
-
return;
|
|
731
|
+
try {
|
|
732
|
+
await waitForSDKLoading(sdk);
|
|
733
|
+
// Check again if we now have all flags
|
|
734
|
+
const updatedStateFlags = {};
|
|
735
|
+
let updatedHasAllFlags = true;
|
|
736
|
+
flagNames.forEach((flagName) => {
|
|
737
|
+
const stateValue = sdk.getFlagValue(flagName);
|
|
738
|
+
if (stateValue !== undefined) {
|
|
739
|
+
updatedStateFlags[flagName] = stateValue;
|
|
740
|
+
}
|
|
741
|
+
else {
|
|
742
|
+
updatedHasAllFlags = false;
|
|
606
743
|
}
|
|
744
|
+
});
|
|
745
|
+
if (updatedHasAllFlags) {
|
|
746
|
+
setFlags(updatedStateFlags);
|
|
747
|
+
setLoading(false);
|
|
748
|
+
setIsInitialized(true);
|
|
749
|
+
return;
|
|
607
750
|
}
|
|
608
751
|
}
|
|
752
|
+
catch (error) {
|
|
753
|
+
console.warn("Timeout waiting for SDK loading, proceeding with API call");
|
|
754
|
+
}
|
|
609
755
|
}
|
|
610
756
|
// If we still don't have all flags, make HTTP request
|
|
611
757
|
const response = await sdk.getFlags();
|
|
@@ -633,14 +779,7 @@ function useFeatureFlags(flagNames, options = {}) {
|
|
|
633
779
|
setLoading(false);
|
|
634
780
|
setIsInitialized(true);
|
|
635
781
|
}
|
|
636
|
-
}, [
|
|
637
|
-
sdk,
|
|
638
|
-
flagNames,
|
|
639
|
-
options.fallbackValues,
|
|
640
|
-
options.enabled,
|
|
641
|
-
isInitialized,
|
|
642
|
-
loading,
|
|
643
|
-
]);
|
|
782
|
+
}, [sdk, flagNames, options.fallbackValues, options.enabled]);
|
|
644
783
|
const refetch = react.useCallback(async () => {
|
|
645
784
|
// Clear cache for flags
|
|
646
785
|
sdk.clearCache(`flags:${sdk["config"].projectId}:${sdk["config"].environment || "all"}`);
|
|
@@ -648,27 +787,32 @@ function useFeatureFlags(flagNames, options = {}) {
|
|
|
648
787
|
}, [sdk, fetchFlags]);
|
|
649
788
|
react.useEffect(() => {
|
|
650
789
|
fetchFlags();
|
|
651
|
-
}, []); //
|
|
790
|
+
}, [fetchFlags]); // Include fetchFlags dependency
|
|
652
791
|
// Subscribe to state changes for automatic updates
|
|
653
792
|
react.useEffect(() => {
|
|
793
|
+
let lastFetchTime = null;
|
|
654
794
|
const unsubscribe = sdk.subscribe((state) => {
|
|
655
795
|
if (!options.enabled && options.enabled !== undefined) {
|
|
656
796
|
return;
|
|
657
797
|
}
|
|
658
|
-
//
|
|
659
|
-
|
|
660
|
-
|
|
661
|
-
|
|
662
|
-
|
|
663
|
-
|
|
664
|
-
|
|
665
|
-
|
|
666
|
-
|
|
667
|
-
|
|
668
|
-
|
|
669
|
-
|
|
670
|
-
|
|
671
|
-
|
|
798
|
+
// Only update if this is a new fetch and has relevant flag data
|
|
799
|
+
if (state.lastFetch &&
|
|
800
|
+
state.lastFetch !== lastFetchTime &&
|
|
801
|
+
Object.keys(state.flags).length > 0) {
|
|
802
|
+
lastFetchTime = state.lastFetch;
|
|
803
|
+
// Update flags from state if available
|
|
804
|
+
const newFlags = {};
|
|
805
|
+
let hasUpdates = false;
|
|
806
|
+
flagNames.forEach((flagName) => {
|
|
807
|
+
const stateValue = state.flags[flagName];
|
|
808
|
+
if (stateValue !== undefined) {
|
|
809
|
+
newFlags[flagName] = stateValue;
|
|
810
|
+
hasUpdates = true;
|
|
811
|
+
}
|
|
812
|
+
});
|
|
813
|
+
if (hasUpdates) {
|
|
814
|
+
setFlags(newFlags);
|
|
815
|
+
setLoading(false);
|
|
672
816
|
setError(state.error);
|
|
673
817
|
}
|
|
674
818
|
}
|
|
@@ -689,17 +833,18 @@ function useABTest(testName, options) {
|
|
|
689
833
|
const [loading, setLoading] = react.useState(true);
|
|
690
834
|
const [error, setError] = react.useState(null);
|
|
691
835
|
// Try to get SDK from context, otherwise create new instance
|
|
692
|
-
|
|
693
|
-
|
|
694
|
-
|
|
695
|
-
|
|
696
|
-
}
|
|
697
|
-
catch (_a) {
|
|
698
|
-
if (!options.config) {
|
|
699
|
-
throw new Error("useABTest must be used within FeatureFlagProvider or config must be provided");
|
|
836
|
+
const sdk = react.useMemo(() => {
|
|
837
|
+
try {
|
|
838
|
+
const context = useFeatureFlagContext();
|
|
839
|
+
return context.sdk;
|
|
700
840
|
}
|
|
701
|
-
|
|
702
|
-
|
|
841
|
+
catch (_a) {
|
|
842
|
+
if (!options.config) {
|
|
843
|
+
throw new Error("useABTest must be used within FeatureFlagProvider or config must be provided");
|
|
844
|
+
}
|
|
845
|
+
return new FlipFlagSDK(options.config);
|
|
846
|
+
}
|
|
847
|
+
}, [options.config]);
|
|
703
848
|
const fetchVariant = react.useCallback(async () => {
|
|
704
849
|
if (!options.enabled && options.enabled !== undefined) {
|
|
705
850
|
return;
|
|
@@ -720,7 +865,7 @@ function useABTest(testName, options) {
|
|
|
720
865
|
finally {
|
|
721
866
|
setLoading(false);
|
|
722
867
|
}
|
|
723
|
-
}, [sdk, testName, options.userId
|
|
868
|
+
}, [sdk, testName, options.userId]);
|
|
724
869
|
const recordEvent = react.useCallback(async (event, eventData) => {
|
|
725
870
|
if (!variantId) {
|
|
726
871
|
console.warn("Cannot record event: no variant assigned yet");
|
|
@@ -733,9 +878,35 @@ function useABTest(testName, options) {
|
|
|
733
878
|
console.error("Failed to record A/B test event:", err);
|
|
734
879
|
}
|
|
735
880
|
}, [sdk, testName, options.userId, variantId]);
|
|
881
|
+
// Subscribe to SDK state changes for automatic updates
|
|
882
|
+
react.useEffect(() => {
|
|
883
|
+
let lastFetchTime = null;
|
|
884
|
+
const unsubscribe = sdk.subscribe((state) => {
|
|
885
|
+
if (!options.enabled && options.enabled !== undefined) {
|
|
886
|
+
return;
|
|
887
|
+
}
|
|
888
|
+
// Only update if this is a new fetch and has A/B test data
|
|
889
|
+
if (state.lastFetch &&
|
|
890
|
+
state.lastFetch !== lastFetchTime &&
|
|
891
|
+
state.abTests[testName]) {
|
|
892
|
+
lastFetchTime = state.lastFetch;
|
|
893
|
+
const testVariant = state.abTests[testName];
|
|
894
|
+
if (testVariant.variantId !== variantId) {
|
|
895
|
+
setVariant(testVariant.variant.value);
|
|
896
|
+
setVariantId(testVariant.variantId);
|
|
897
|
+
setLoading(false);
|
|
898
|
+
setError(state.error);
|
|
899
|
+
}
|
|
900
|
+
}
|
|
901
|
+
});
|
|
902
|
+
return unsubscribe;
|
|
903
|
+
}, [sdk, testName, options.enabled, variantId]);
|
|
904
|
+
// Initial fetch effect
|
|
736
905
|
react.useEffect(() => {
|
|
737
|
-
|
|
738
|
-
|
|
906
|
+
if (options.enabled !== false) {
|
|
907
|
+
fetchVariant();
|
|
908
|
+
}
|
|
909
|
+
}, [options.enabled, fetchVariant]);
|
|
739
910
|
return {
|
|
740
911
|
variant,
|
|
741
912
|
variantId,
|