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/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 a bit and check state again
262
+ // In this case, wait for loading to complete using Promise
237
263
  if (this.state.isLoading) {
238
- // Wait up to 5 seconds for loading to complete
239
- for (let i = 0; i < 50; i++) {
240
- await new Promise((resolve) => setTimeout(resolve, 100));
241
- if (!this.state.isLoading && this.state.flags[flagName] !== undefined) {
242
- return {
243
- projectId: this.config.projectId,
244
- flagName,
245
- environment: this.config.environment || "all",
246
- value: this.state.flags[flagName],
247
- timestamp: ((_b = this.state.lastFetch) === null || _b === void 0 ? void 0 : _b.toISOString()) || new Date().toISOString(),
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
- let sdk;
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");
558
+ const sdk = useMemo(() => {
559
+ try {
560
+ const context = useFeatureFlagContext();
561
+ return context.sdk;
448
562
  }
449
- sdk = new FlipFlagSDK(options.config);
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
- let sdk;
542
- try {
543
- const context = useFeatureFlagContext();
544
- sdk = context.sdk;
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
- sdk = new FlipFlagSDK(options.config);
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
- // Wait up to 5 seconds for loading to complete
584
- for (let i = 0; i < 50; i++) {
585
- await new Promise((resolve) => setTimeout(resolve, 100));
586
- const currentState = sdk.getState();
587
- if (!currentState.isLoading) {
588
- // Check again if we now have all flags
589
- const updatedStateFlags = {};
590
- let updatedHasAllFlags = true;
591
- flagNames.forEach((flagName) => {
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
- }, []); // Remove fetchFlags dependency to prevent infinite loop
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
- let sdk;
691
- try {
692
- const context = useFeatureFlagContext();
693
- sdk = context.sdk;
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
- sdk = new FlipFlagSDK(options.config);
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;