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