flipflag-sdk 1.1.21 → 1.1.22
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 +131 -13
- package/dist/index.js.map +1 -1
- package/dist/index.mjs +131 -13
- package/dist/index.mjs.map +1 -1
- package/dist/js.js +141 -13
- package/dist/js.js.map +1 -1
- package/dist/js.mjs +141 -13
- package/dist/js.mjs.map +1 -1
- package/dist/next.js +133 -27
- package/dist/next.js.map +1 -1
- package/dist/next.mjs +133 -27
- package/dist/next.mjs.map +1 -1
- package/dist/react-native.js +131 -13
- package/dist/react-native.js.map +1 -1
- package/dist/react-native.mjs +131 -13
- package/dist/react-native.mjs.map +1 -1
- package/dist/react.js +206 -65
- package/dist/react.js.map +1 -1
- package/dist/react.mjs +207 -66
- package/dist/react.mjs.map +1 -1
- package/dist/types/core/FlipFlagSDK.d.ts +17 -4
- package/dist/types/js/featureFlagManager.d.ts +4 -0
- package/dist/types/next/server-utils.d.ts +1 -1
- package/dist/types/next/types.d.ts +1 -0
- package/dist/types/types/index.d.ts +1 -1
- package/dist/vue.js +131 -13
- package/dist/vue.js.map +1 -1
- package/dist/vue.mjs +131 -13
- 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,6 +33,15 @@ 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
47
|
this.clearABTestCache();
|
|
@@ -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
|
}
|
|
@@ -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,17 +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
|
-
|
|
442
|
-
|
|
443
|
-
|
|
444
|
-
}
|
|
445
|
-
catch (_b) {
|
|
446
|
-
if (!options.config) {
|
|
447
|
-
throw new Error("useFeatureFlag must be used within FeatureFlagProvider or config must be provided");
|
|
558
|
+
const sdk = useMemo(() => {
|
|
559
|
+
try {
|
|
560
|
+
const context = useFeatureFlagContext();
|
|
561
|
+
return context.sdk;
|
|
448
562
|
}
|
|
449
|
-
|
|
450
|
-
|
|
563
|
+
catch (_a) {
|
|
564
|
+
if (!options.config) {
|
|
565
|
+
throw new Error("useFeatureFlag must be used within FeatureFlagProvider or config must be provided");
|
|
566
|
+
}
|
|
567
|
+
return new FlipFlagSDK(options.config);
|
|
568
|
+
}
|
|
569
|
+
}, [options.config]);
|
|
451
570
|
const updateFromState = useCallback(() => {
|
|
452
571
|
if (!options.enabled && options.enabled !== undefined) {
|
|
453
572
|
return;
|
|
@@ -524,6 +643,27 @@ function useFeatureFlag(flagName, options = {}) {
|
|
|
524
643
|
};
|
|
525
644
|
}
|
|
526
645
|
|
|
646
|
+
/**
|
|
647
|
+
* Wait for SDK loading to complete with timeout
|
|
648
|
+
*/
|
|
649
|
+
function waitForSDKLoading(sdk, timeout = 5000) {
|
|
650
|
+
const startTime = Date.now();
|
|
651
|
+
return new Promise((resolve, reject) => {
|
|
652
|
+
const checkLoading = () => {
|
|
653
|
+
if (!sdk.getState().isLoading) {
|
|
654
|
+
resolve();
|
|
655
|
+
return;
|
|
656
|
+
}
|
|
657
|
+
if (Date.now() - startTime > timeout) {
|
|
658
|
+
reject(new Error("Timeout waiting for SDK loading to complete"));
|
|
659
|
+
return;
|
|
660
|
+
}
|
|
661
|
+
// Check again after a short delay
|
|
662
|
+
setTimeout(checkLoading, 50);
|
|
663
|
+
};
|
|
664
|
+
checkLoading();
|
|
665
|
+
});
|
|
666
|
+
}
|
|
527
667
|
function useFeatureFlags(flagNames, options = {}) {
|
|
528
668
|
const [flags, setFlags] = useState(() => {
|
|
529
669
|
// Initialize with fallback values
|
|
@@ -538,17 +678,18 @@ function useFeatureFlags(flagNames, options = {}) {
|
|
|
538
678
|
const [error, setError] = useState(null);
|
|
539
679
|
const [isInitialized, setIsInitialized] = useState(false);
|
|
540
680
|
// 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");
|
|
681
|
+
const sdk = useMemo(() => {
|
|
682
|
+
try {
|
|
683
|
+
const context = useFeatureFlagContext();
|
|
684
|
+
return context.sdk;
|
|
549
685
|
}
|
|
550
|
-
|
|
551
|
-
|
|
686
|
+
catch (_a) {
|
|
687
|
+
if (!options.config) {
|
|
688
|
+
throw new Error("useFeatureFlags must be used within FeatureFlagProvider or config must be provided");
|
|
689
|
+
}
|
|
690
|
+
return new FlipFlagSDK(options.config);
|
|
691
|
+
}
|
|
692
|
+
}, [options.config]);
|
|
552
693
|
const fetchFlags = useCallback(async () => {
|
|
553
694
|
if (!options.enabled && options.enabled !== undefined) {
|
|
554
695
|
return;
|
|
@@ -580,30 +721,29 @@ function useFeatureFlags(flagNames, options = {}) {
|
|
|
580
721
|
}
|
|
581
722
|
// If SDK is currently loading, wait for it to complete
|
|
582
723
|
if (sdk.getState().isLoading) {
|
|
583
|
-
|
|
584
|
-
|
|
585
|
-
|
|
586
|
-
const
|
|
587
|
-
|
|
588
|
-
|
|
589
|
-
const
|
|
590
|
-
|
|
591
|
-
|
|
592
|
-
const stateValue = sdk.getFlagValue(flagName);
|
|
593
|
-
if (stateValue !== undefined) {
|
|
594
|
-
updatedStateFlags[flagName] = stateValue;
|
|
595
|
-
}
|
|
596
|
-
else {
|
|
597
|
-
updatedHasAllFlags = false;
|
|
598
|
-
}
|
|
599
|
-
});
|
|
600
|
-
if (updatedHasAllFlags) {
|
|
601
|
-
setFlags(updatedStateFlags);
|
|
602
|
-
setLoading(false);
|
|
603
|
-
return;
|
|
724
|
+
try {
|
|
725
|
+
await waitForSDKLoading(sdk);
|
|
726
|
+
// Check again if we now have all flags
|
|
727
|
+
const updatedStateFlags = {};
|
|
728
|
+
let updatedHasAllFlags = true;
|
|
729
|
+
flagNames.forEach((flagName) => {
|
|
730
|
+
const stateValue = sdk.getFlagValue(flagName);
|
|
731
|
+
if (stateValue !== undefined) {
|
|
732
|
+
updatedStateFlags[flagName] = stateValue;
|
|
604
733
|
}
|
|
734
|
+
else {
|
|
735
|
+
updatedHasAllFlags = false;
|
|
736
|
+
}
|
|
737
|
+
});
|
|
738
|
+
if (updatedHasAllFlags) {
|
|
739
|
+
setFlags(updatedStateFlags);
|
|
740
|
+
setLoading(false);
|
|
741
|
+
return;
|
|
605
742
|
}
|
|
606
743
|
}
|
|
744
|
+
catch (error) {
|
|
745
|
+
console.warn("Timeout waiting for SDK loading, proceeding with API call");
|
|
746
|
+
}
|
|
607
747
|
}
|
|
608
748
|
// If we still don't have all flags, make HTTP request
|
|
609
749
|
const response = await sdk.getFlags();
|
|
@@ -646,7 +786,7 @@ function useFeatureFlags(flagNames, options = {}) {
|
|
|
646
786
|
}, [sdk, fetchFlags]);
|
|
647
787
|
useEffect(() => {
|
|
648
788
|
fetchFlags();
|
|
649
|
-
}, []); //
|
|
789
|
+
}, [fetchFlags]); // Include fetchFlags dependency
|
|
650
790
|
// Subscribe to state changes for automatic updates
|
|
651
791
|
useEffect(() => {
|
|
652
792
|
const unsubscribe = sdk.subscribe((state) => {
|
|
@@ -687,17 +827,18 @@ function useABTest(testName, options) {
|
|
|
687
827
|
const [loading, setLoading] = useState(true);
|
|
688
828
|
const [error, setError] = useState(null);
|
|
689
829
|
// 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");
|
|
830
|
+
const sdk = useMemo(() => {
|
|
831
|
+
try {
|
|
832
|
+
const context = useFeatureFlagContext();
|
|
833
|
+
return context.sdk;
|
|
698
834
|
}
|
|
699
|
-
|
|
700
|
-
|
|
835
|
+
catch (_a) {
|
|
836
|
+
if (!options.config) {
|
|
837
|
+
throw new Error("useABTest must be used within FeatureFlagProvider or config must be provided");
|
|
838
|
+
}
|
|
839
|
+
return new FlipFlagSDK(options.config);
|
|
840
|
+
}
|
|
841
|
+
}, [options.config]);
|
|
701
842
|
const fetchVariant = useCallback(async () => {
|
|
702
843
|
if (!options.enabled && options.enabled !== undefined) {
|
|
703
844
|
return;
|