@splitsoftware/splitio-commons 2.4.2-rc.1 → 2.4.2-rc.3

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.
Files changed (64) hide show
  1. package/CHANGES.txt +13 -1
  2. package/cjs/logger/messages/error.js +1 -1
  3. package/cjs/sdkClient/sdkClient.js +0 -1
  4. package/cjs/sdkFactory/index.js +3 -10
  5. package/cjs/services/splitHttpClient.js +1 -1
  6. package/cjs/storages/AbstractSplitsCacheSync.js +5 -1
  7. package/cjs/storages/inLocalStorage/MySegmentsCacheInLocal.js +16 -16
  8. package/cjs/storages/inLocalStorage/RBSegmentsCacheInLocal.js +17 -17
  9. package/cjs/storages/inLocalStorage/SplitsCacheInLocal.js +33 -37
  10. package/cjs/storages/inLocalStorage/index.js +31 -13
  11. package/cjs/storages/inLocalStorage/storageAdapter.js +54 -0
  12. package/cjs/storages/inLocalStorage/validateCache.js +28 -23
  13. package/cjs/sync/offline/syncTasks/fromObjectSyncTask.js +2 -3
  14. package/cjs/sync/polling/pollingManagerCS.js +5 -4
  15. package/cjs/sync/polling/updaters/mySegmentsUpdater.js +5 -2
  16. package/cjs/sync/polling/updaters/splitChangesUpdater.js +3 -1
  17. package/cjs/sync/syncManagerOnline.js +31 -26
  18. package/cjs/trackers/impressionsTracker.js +4 -4
  19. package/cjs/utils/env/isLocalStorageAvailable.js +28 -5
  20. package/cjs/utils/settingsValidation/splitFilters.js +0 -6
  21. package/cjs/utils/settingsValidation/storage/storageCS.js +1 -1
  22. package/esm/logger/messages/error.js +1 -1
  23. package/esm/sdkClient/sdkClient.js +0 -1
  24. package/esm/sdkFactory/index.js +3 -10
  25. package/esm/services/splitHttpClient.js +1 -1
  26. package/esm/storages/AbstractSplitsCacheSync.js +3 -0
  27. package/esm/storages/inLocalStorage/MySegmentsCacheInLocal.js +16 -16
  28. package/esm/storages/inLocalStorage/RBSegmentsCacheInLocal.js +17 -17
  29. package/esm/storages/inLocalStorage/SplitsCacheInLocal.js +33 -37
  30. package/esm/storages/inLocalStorage/index.js +32 -14
  31. package/esm/storages/inLocalStorage/storageAdapter.js +50 -0
  32. package/esm/storages/inLocalStorage/validateCache.js +28 -23
  33. package/esm/sync/offline/syncTasks/fromObjectSyncTask.js +2 -3
  34. package/esm/sync/polling/pollingManagerCS.js +5 -4
  35. package/esm/sync/polling/updaters/mySegmentsUpdater.js +5 -2
  36. package/esm/sync/polling/updaters/splitChangesUpdater.js +3 -1
  37. package/esm/sync/syncManagerOnline.js +31 -26
  38. package/esm/trackers/impressionsTracker.js +4 -4
  39. package/esm/utils/env/isLocalStorageAvailable.js +24 -3
  40. package/esm/utils/settingsValidation/splitFilters.js +0 -6
  41. package/esm/utils/settingsValidation/storage/storageCS.js +1 -1
  42. package/package.json +1 -1
  43. package/src/logger/messages/error.ts +1 -1
  44. package/src/sdkClient/sdkClient.ts +0 -1
  45. package/src/sdkFactory/index.ts +3 -13
  46. package/src/services/splitHttpClient.ts +1 -1
  47. package/src/storages/AbstractSplitsCacheSync.ts +5 -1
  48. package/src/storages/inLocalStorage/MySegmentsCacheInLocal.ts +18 -17
  49. package/src/storages/inLocalStorage/RBSegmentsCacheInLocal.ts +19 -18
  50. package/src/storages/inLocalStorage/SplitsCacheInLocal.ts +34 -37
  51. package/src/storages/inLocalStorage/index.ts +37 -16
  52. package/src/storages/inLocalStorage/storageAdapter.ts +62 -0
  53. package/src/storages/inLocalStorage/validateCache.ts +29 -23
  54. package/src/storages/types.ts +19 -1
  55. package/src/sync/offline/syncTasks/fromObjectSyncTask.ts +1 -2
  56. package/src/sync/polling/pollingManagerCS.ts +5 -4
  57. package/src/sync/polling/updaters/mySegmentsUpdater.ts +5 -2
  58. package/src/sync/polling/updaters/splitChangesUpdater.ts +4 -2
  59. package/src/sync/syncManagerOnline.ts +30 -24
  60. package/src/trackers/impressionsTracker.ts +3 -3
  61. package/src/utils/env/isLocalStorageAvailable.ts +24 -3
  62. package/src/utils/settingsValidation/splitFilters.ts +0 -6
  63. package/src/utils/settingsValidation/storage/storageCS.ts +1 -1
  64. package/types/splitio.d.ts +72 -16
@@ -0,0 +1,62 @@
1
+ import { ILogger } from '../../logger/types';
2
+ import SplitIO from '../../../types/splitio';
3
+ import { LOG_PREFIX } from './constants';
4
+ import { StorageAdapter } from '../types';
5
+
6
+
7
+ export function storageAdapter(log: ILogger, prefix: string, wrapper: SplitIO.SyncStorageWrapper | SplitIO.AsyncStorageWrapper): Required<StorageAdapter> {
8
+ let keys: string[] = [];
9
+ let cache: Record<string, string> = {};
10
+
11
+ let loadPromise: Promise<void> | undefined;
12
+ let savePromise = Promise.resolve();
13
+
14
+ return {
15
+ load() {
16
+ return loadPromise || (loadPromise = Promise.resolve().then(() => {
17
+ return wrapper.getItem(prefix);
18
+ }).then((storedCache) => {
19
+ cache = JSON.parse(storedCache || '{}');
20
+ keys = Object.keys(cache);
21
+ }).catch((e) => {
22
+ log.error(LOG_PREFIX + 'Rejected promise calling wrapper `getItem` method, with error: ' + e);
23
+ }));
24
+ },
25
+
26
+ save() {
27
+ return savePromise = savePromise.then(() => {
28
+ return Promise.resolve(wrapper.setItem(prefix, JSON.stringify(cache)));
29
+ }).catch((e) => {
30
+ log.error(LOG_PREFIX + 'Rejected promise calling wrapper `setItem` method, with error: ' + e);
31
+ });
32
+ },
33
+
34
+ whenSaved() {
35
+ return savePromise;
36
+ },
37
+
38
+ get length() {
39
+ return keys.length;
40
+ },
41
+
42
+ getItem(key: string) {
43
+ return cache[key] || null;
44
+ },
45
+
46
+ key(index: number) {
47
+ return keys[index] || null;
48
+ },
49
+
50
+ removeItem(key: string) {
51
+ const index = keys.indexOf(key);
52
+ if (index === -1) return;
53
+ keys.splice(index, 1);
54
+ delete cache[key];
55
+ },
56
+
57
+ setItem(key: string, value: string) {
58
+ if (keys.indexOf(key) === -1) keys.push(key);
59
+ cache[key] = value;
60
+ }
61
+ };
62
+ }
@@ -7,6 +7,7 @@ import type { RBSegmentsCacheInLocal } from './RBSegmentsCacheInLocal';
7
7
  import type { MySegmentsCacheInLocal } from './MySegmentsCacheInLocal';
8
8
  import { KeyBuilderCS } from '../KeyBuilderCS';
9
9
  import SplitIO from '../../../types/splitio';
10
+ import { StorageAdapter } from '../types';
10
11
 
11
12
  const DEFAULT_CACHE_EXPIRATION_IN_DAYS = 10;
12
13
  const MILLIS_IN_A_DAY = 86400000;
@@ -16,11 +17,11 @@ const MILLIS_IN_A_DAY = 86400000;
16
17
  *
17
18
  * @returns `true` if cache should be cleared, `false` otherwise
18
19
  */
19
- function validateExpiration(options: SplitIO.InLocalStorageOptions, settings: ISettings, keys: KeyBuilderCS, currentTimestamp: number, isThereCache: boolean) {
20
+ function validateExpiration(options: SplitIO.InLocalStorageOptions, storage: StorageAdapter, settings: ISettings, keys: KeyBuilderCS, currentTimestamp: number, isThereCache: boolean) {
20
21
  const { log } = settings;
21
22
 
22
23
  // Check expiration
23
- const lastUpdatedTimestamp = parseInt(localStorage.getItem(keys.buildLastUpdatedKey()) as string, 10);
24
+ const lastUpdatedTimestamp = parseInt(storage.getItem(keys.buildLastUpdatedKey()) as string, 10);
24
25
  if (!isNaNNumber(lastUpdatedTimestamp)) {
25
26
  const cacheExpirationInDays = isFiniteNumber(options.expirationDays) && options.expirationDays >= 1 ? options.expirationDays : DEFAULT_CACHE_EXPIRATION_IN_DAYS;
26
27
  const expirationTimestamp = currentTimestamp - MILLIS_IN_A_DAY * cacheExpirationInDays;
@@ -32,12 +33,12 @@ function validateExpiration(options: SplitIO.InLocalStorageOptions, settings: IS
32
33
 
33
34
  // Check hash
34
35
  const storageHashKey = keys.buildHashKey();
35
- const storageHash = localStorage.getItem(storageHashKey);
36
+ const storageHash = storage.getItem(storageHashKey);
36
37
  const currentStorageHash = getStorageHash(settings);
37
38
 
38
39
  if (storageHash !== currentStorageHash) {
39
40
  try {
40
- localStorage.setItem(storageHashKey, currentStorageHash);
41
+ storage.setItem(storageHashKey, currentStorageHash);
41
42
  } catch (e) {
42
43
  log.error(LOG_PREFIX + e);
43
44
  }
@@ -50,7 +51,7 @@ function validateExpiration(options: SplitIO.InLocalStorageOptions, settings: IS
50
51
 
51
52
  // Clear on init
52
53
  if (options.clearOnInit) {
53
- const lastClearTimestamp = parseInt(localStorage.getItem(keys.buildLastClear()) as string, 10);
54
+ const lastClearTimestamp = parseInt(storage.getItem(keys.buildLastClear()) as string, 10);
54
55
 
55
56
  if (isNaNNumber(lastClearTimestamp) || lastClearTimestamp < currentTimestamp - MILLIS_IN_A_DAY) {
56
57
  log.info(LOG_PREFIX + 'clearOnInit was set and cache was not cleared in the last 24 hours. Cleaning up cache');
@@ -67,27 +68,32 @@ function validateExpiration(options: SplitIO.InLocalStorageOptions, settings: IS
67
68
  *
68
69
  * @returns `true` if cache is ready to be used, `false` otherwise (cache was cleared or there is no cache)
69
70
  */
70
- export function validateCache(options: SplitIO.InLocalStorageOptions, settings: ISettings, keys: KeyBuilderCS, splits: SplitsCacheInLocal, rbSegments: RBSegmentsCacheInLocal, segments: MySegmentsCacheInLocal, largeSegments: MySegmentsCacheInLocal): boolean {
71
+ export function validateCache(options: SplitIO.InLocalStorageOptions, storage: StorageAdapter, settings: ISettings, keys: KeyBuilderCS, splits: SplitsCacheInLocal, rbSegments: RBSegmentsCacheInLocal, segments: MySegmentsCacheInLocal, largeSegments: MySegmentsCacheInLocal): Promise<boolean> {
71
72
 
72
- const currentTimestamp = Date.now();
73
- const isThereCache = splits.getChangeNumber() > -1;
73
+ return Promise.resolve(storage.load && storage.load()).then(() => {
74
+ const currentTimestamp = Date.now();
75
+ const isThereCache = splits.getChangeNumber() > -1;
74
76
 
75
- if (validateExpiration(options, settings, keys, currentTimestamp, isThereCache)) {
76
- splits.clear();
77
- rbSegments.clear();
78
- segments.clear();
79
- largeSegments.clear();
77
+ if (validateExpiration(options, storage, settings, keys, currentTimestamp, isThereCache)) {
78
+ splits.clear();
79
+ rbSegments.clear();
80
+ segments.clear();
81
+ largeSegments.clear();
80
82
 
81
- // Update last clear timestamp
82
- try {
83
- localStorage.setItem(keys.buildLastClear(), currentTimestamp + '');
84
- } catch (e) {
85
- settings.log.error(LOG_PREFIX + e);
86
- }
83
+ // Update last clear timestamp
84
+ try {
85
+ storage.setItem(keys.buildLastClear(), currentTimestamp + '');
86
+ } catch (e) {
87
+ settings.log.error(LOG_PREFIX + e);
88
+ }
87
89
 
88
- return false;
89
- }
90
+ // Persist clear
91
+ if (storage.save) storage.save();
92
+
93
+ return false;
94
+ }
90
95
 
91
- // Check if ready from cache
92
- return isThereCache;
96
+ // Check if ready from cache
97
+ return isThereCache;
98
+ });
93
99
  }
@@ -4,6 +4,23 @@ import { MySegmentsData } from '../sync/polling/types';
4
4
  import { EventDataType, HttpErrors, HttpLatencies, ImpressionDataType, LastSync, Method, MethodExceptions, MethodLatencies, MultiMethodExceptions, MultiMethodLatencies, MultiConfigs, OperationType, StoredEventWithMetadata, StoredImpressionWithMetadata, StreamingEvent, UniqueKeysPayloadCs, UniqueKeysPayloadSs, TelemetryUsageStatsPayload, UpdatesFromSSEEnum } from '../sync/submitters/types';
5
5
  import { ISettings } from '../types';
6
6
 
7
+ /**
8
+ * Internal interface based on a subset of the Web Storage API interface
9
+ * (https://developer.mozilla.org/en-US/docs/Web/API/Storage) used by the SDK
10
+ */
11
+ export interface StorageAdapter {
12
+ // Methods to support async storages
13
+ load?: () => Promise<void>;
14
+ save?: () => Promise<void>;
15
+ whenSaved?: () => Promise<void>;
16
+ // Methods based on https://developer.mozilla.org/en-US/docs/Web/API/Storage
17
+ readonly length: number;
18
+ key(index: number): string | null;
19
+ getItem(key: string): string | null;
20
+ removeItem(key: string): void;
21
+ setItem(key: string, value: string): void;
22
+ }
23
+
7
24
  /**
8
25
  * Interface of a pluggable storage wrapper.
9
26
  */
@@ -466,6 +483,7 @@ export interface IStorageBase<
466
483
  uniqueKeys: TUniqueKeysCache,
467
484
  destroy(): void | Promise<void>,
468
485
  shared?: (matchingKey: string, onReadyCb: (error?: any) => void) => this
486
+ save?: () => void | Promise<void>,
469
487
  }
470
488
 
471
489
  export interface IStorageSync extends IStorageBase<
@@ -479,7 +497,7 @@ export interface IStorageSync extends IStorageBase<
479
497
  IUniqueKeysCacheSync
480
498
  > {
481
499
  // Defined in client-side
482
- validateCache?: () => boolean, // @TODO support async
500
+ validateCache?: () => Promise<boolean>,
483
501
  largeSegments?: ISegmentsCacheSync,
484
502
  }
485
503
 
@@ -59,8 +59,7 @@ export function fromObjectUpdaterFactory(
59
59
 
60
60
  if (startingUp) {
61
61
  startingUp = false;
62
- const isCacheLoaded = storage.validateCache ? storage.validateCache() : false;
63
- Promise.resolve().then(() => {
62
+ Promise.resolve(storage.validateCache ? storage.validateCache() : false).then((isCacheLoaded) => {
64
63
  // Emits SDK_READY_FROM_CACHE
65
64
  if (isCacheLoaded) readiness.splits.emit(SDK_SPLITS_CACHE_LOADED);
66
65
  // Emits SDK_READY
@@ -8,6 +8,7 @@ import { getMatching } from '../../utils/key';
8
8
  import { SDK_SPLITS_ARRIVED, SDK_SEGMENTS_ARRIVED } from '../../readiness/constants';
9
9
  import { POLLING_SMART_PAUSING, POLLING_START, POLLING_STOP } from '../../logger/constants';
10
10
  import { ISdkFactoryContextSync } from '../../sdkFactory/types';
11
+ import { usesSegmentsSync } from '../../storages/AbstractSplitsCacheSync';
11
12
 
12
13
  /**
13
14
  * Expose start / stop mechanism for polling data from services.
@@ -43,7 +44,7 @@ export function pollingManagerCSFactory(
43
44
  // smart pausing
44
45
  readiness.splits.on(SDK_SPLITS_ARRIVED, () => {
45
46
  if (!splitsSyncTask.isRunning()) return; // noop if not doing polling
46
- const usingSegments = storage.splits.usesSegments() || storage.rbSegments.usesSegments();
47
+ const usingSegments = usesSegmentsSync(storage);
47
48
  if (usingSegments !== mySegmentsSyncTask.isRunning()) {
48
49
  log.info(POLLING_SMART_PAUSING, [usingSegments ? 'ON' : 'OFF']);
49
50
  if (usingSegments) {
@@ -59,9 +60,9 @@ export function pollingManagerCSFactory(
59
60
 
60
61
  // smart ready
61
62
  function smartReady() {
62
- if (!readiness.isReady() && !storage.splits.usesSegments() && !storage.rbSegments.usesSegments()) readiness.segments.emit(SDK_SEGMENTS_ARRIVED);
63
+ if (!readiness.isReady() && !usesSegmentsSync(storage)) readiness.segments.emit(SDK_SEGMENTS_ARRIVED);
63
64
  }
64
- if (!storage.splits.usesSegments() && !storage.rbSegments.usesSegments()) setTimeout(smartReady, 0);
65
+ if (!usesSegmentsSync(storage)) setTimeout(smartReady, 0);
65
66
  else readiness.splits.once(SDK_SPLITS_ARRIVED, smartReady);
66
67
 
67
68
  mySegmentsSyncTasks[matchingKey] = mySegmentsSyncTask;
@@ -77,7 +78,7 @@ export function pollingManagerCSFactory(
77
78
  log.info(POLLING_START);
78
79
 
79
80
  splitsSyncTask.start();
80
- if (storage.splits.usesSegments() || storage.rbSegments.usesSegments()) startMySegmentsSyncTasks();
81
+ if (usesSegmentsSync(storage)) startMySegmentsSyncTasks();
81
82
  },
82
83
 
83
84
  // Stop periodic fetching (polling)
@@ -8,6 +8,7 @@ import { SYNC_MYSEGMENTS_FETCH_RETRY } from '../../../logger/constants';
8
8
  import { MySegmentsData } from '../types';
9
9
  import { IMembershipsResponse } from '../../../dtos/types';
10
10
  import { MEMBERSHIPS_LS_UPDATE } from '../../streaming/constants';
11
+ import { usesSegmentsSync } from '../../../storages/AbstractSplitsCacheSync';
11
12
 
12
13
  type IMySegmentsUpdater = (segmentsData?: MySegmentsData, noCache?: boolean, till?: number) => Promise<boolean>
13
14
 
@@ -27,7 +28,7 @@ export function mySegmentsUpdaterFactory(
27
28
  matchingKey: string
28
29
  ): IMySegmentsUpdater {
29
30
 
30
- const { splits, rbSegments, segments, largeSegments } = storage;
31
+ const { segments, largeSegments } = storage;
31
32
  let readyOnAlreadyExistentState = true;
32
33
  let startingUp = true;
33
34
 
@@ -50,8 +51,10 @@ export function mySegmentsUpdaterFactory(
50
51
  shouldNotifyUpdate = largeSegments!.resetSegments((segmentsData as IMembershipsResponse).ls || {}) || shouldNotifyUpdate;
51
52
  }
52
53
 
54
+ if (storage.save) storage.save();
55
+
53
56
  // Notify update if required
54
- if ((splits.usesSegments() || rbSegments.usesSegments()) && (shouldNotifyUpdate || readyOnAlreadyExistentState)) {
57
+ if (usesSegmentsSync(storage) && (shouldNotifyUpdate || readyOnAlreadyExistentState)) {
55
58
  readyOnAlreadyExistentState = false;
56
59
  segmentsEventEmitter.emit(SDK_SEGMENTS_ARRIVED);
57
60
  }
@@ -59,7 +59,7 @@ interface ISplitMutations<T extends ISplit | IRBSegment> {
59
59
 
60
60
  /**
61
61
  * If there are defined filters and one feature flag doesn't match with them, its status is changed to 'ARCHIVE' to avoid storing it
62
- * If there are set filter defined, names filter is ignored
62
+ * If there is `bySet` filter, `byName` and `byPrefix` filters are ignored
63
63
  *
64
64
  * @param featureFlag - feature flag to be evaluated
65
65
  * @param filters - splitFiltersValidation bySet | byName
@@ -117,7 +117,7 @@ export function computeMutation<T extends ISplit | IRBSegment>(rules: Array<T>,
117
117
  export function splitChangesUpdaterFactory(
118
118
  log: ILogger,
119
119
  splitChangesFetcher: ISplitChangesFetcher,
120
- storage: Pick<IStorageBase, 'splits' | 'rbSegments' | 'segments'>,
120
+ storage: Pick<IStorageBase, 'splits' | 'rbSegments' | 'segments' | 'save'>,
121
121
  splitFiltersValidation: ISplitFiltersValidation,
122
122
  splitsEventEmitter?: ISplitsEventEmitter,
123
123
  requestTimeoutBeforeReady: number = 0,
@@ -185,6 +185,8 @@ export function splitChangesUpdaterFactory(
185
185
  // @TODO if at least 1 segment fetch fails due to 404 and other segments are updated in the storage, SDK_UPDATE is not emitted
186
186
  segments.registerSegments(setToArray(usedSegments))
187
187
  ]).then(([ffChanged, rbsChanged]) => {
188
+ if (storage.save) storage.save();
189
+
188
190
  if (splitsEventEmitter) {
189
191
  // To emit SDK_SPLITS_ARRIVED for server-side SDK, we must check that all registered segments have been fetched
190
192
  return Promise.resolve(!splitsEventEmitter.splitsArrived || ((ffChanged || rbsChanged) && (isClientSide || checkAllSegmentsExist(segments))))
@@ -10,6 +10,7 @@ import { isConsentGranted } from '../consent';
10
10
  import { POLLING, STREAMING, SYNC_MODE_UPDATE } from '../utils/constants';
11
11
  import { ISdkFactoryContextSync } from '../sdkFactory/types';
12
12
  import { SDK_SPLITS_CACHE_LOADED } from '../readiness/constants';
13
+ import { usesSegmentsSync } from '../storages/AbstractSplitsCacheSync';
13
14
 
14
15
  /**
15
16
  * Online SyncManager factory.
@@ -88,36 +89,41 @@ export function syncManagerOnlineFactory(
88
89
  start() {
89
90
  running = true;
90
91
 
91
- if (startFirstTime) {
92
- const isCacheLoaded = storage.validateCache ? storage.validateCache() : false;
93
- if (isCacheLoaded) Promise.resolve().then(() => { readiness.splits.emit(SDK_SPLITS_CACHE_LOADED); });
94
- }
92
+ // @TODO once event, impression and telemetry storages support persistence, call when `validateCache` promise is resolved
93
+ submitterManager.start(!isConsentGranted(settings));
95
94
 
96
- // start syncing splits and segments
97
- if (pollingManager) {
95
+ return Promise.resolve(storage.validateCache ? storage.validateCache() : false).then((isCacheLoaded) => {
96
+ if (!running) return;
98
97
 
99
- // If synchronization is disabled pushManager and pollingManager should not start
100
- if (syncEnabled) {
101
- if (pushManager) {
102
- // Doesn't call `syncAll` when the syncManager is resuming
98
+ if (startFirstTime) {
99
+ // Emits SDK_READY_FROM_CACHE
100
+ if (isCacheLoaded) readiness.splits.emit(SDK_SPLITS_CACHE_LOADED);
101
+
102
+ }
103
+
104
+ // start syncing splits and segments
105
+ if (pollingManager) {
106
+
107
+ // If synchronization is disabled pushManager and pollingManager should not start
108
+ if (syncEnabled) {
109
+ if (pushManager) {
110
+ // Doesn't call `syncAll` when the syncManager is resuming
111
+ if (startFirstTime) {
112
+ pollingManager.syncAll();
113
+ }
114
+ pushManager.start();
115
+ } else {
116
+ pollingManager.start();
117
+ }
118
+ } else {
103
119
  if (startFirstTime) {
104
120
  pollingManager.syncAll();
105
121
  }
106
- pushManager.start();
107
- } else {
108
- pollingManager.start();
109
- }
110
- } else {
111
- if (startFirstTime) {
112
- pollingManager.syncAll();
113
122
  }
114
123
  }
115
- }
116
-
117
- // start periodic data recording (events, impressions, telemetry).
118
- submitterManager.start(!isConsentGranted(settings));
119
124
 
120
- startFirstTime = false;
125
+ startFirstTime = false;
126
+ });
121
127
  },
122
128
 
123
129
  /**
@@ -155,14 +161,14 @@ export function syncManagerOnlineFactory(
155
161
  if (pushManager) {
156
162
  if (pollingManager.isRunning()) {
157
163
  // if doing polling, we must start the periodic fetch of data
158
- if (storage.splits.usesSegments() || storage.rbSegments.usesSegments()) mySegmentsSyncTask.start();
164
+ if (usesSegmentsSync(storage)) mySegmentsSyncTask.start();
159
165
  } else {
160
166
  // if not polling, we must execute the sync task for the initial fetch
161
167
  // of segments since `syncAll` was already executed when starting the main client
162
168
  mySegmentsSyncTask.execute();
163
169
  }
164
170
  } else {
165
- if (storage.splits.usesSegments() || storage.rbSegments.usesSegments()) mySegmentsSyncTask.start();
171
+ if (usesSegmentsSync(storage)) mySegmentsSyncTask.start();
166
172
  }
167
173
  } else {
168
174
  if (!readinessManager.isReady()) mySegmentsSyncTask.execute();
@@ -20,7 +20,7 @@ export function impressionsTrackerFactory(
20
20
  telemetryCache?: ITelemetryCacheSync | ITelemetryCacheAsync,
21
21
  ): IImpressionsTracker {
22
22
 
23
- const { log, runtime: { ip, hostname }, version } = settings;
23
+ const { log, impressionListener, runtime: { ip, hostname }, version } = settings;
24
24
 
25
25
  return {
26
26
  track(impressions: ImpressionDecorated[], attributes?: SplitIO.Attributes) {
@@ -56,7 +56,7 @@ export function impressionsTrackerFactory(
56
56
  }
57
57
 
58
58
  // @TODO next block might be handled by the integration manager. In that case, the metadata object doesn't need to be passed in the constructor
59
- if (settings.impressionListener || integrationsManager) {
59
+ if (impressionListener || integrationsManager) {
60
60
  for (let i = 0; i < impressionsLength; i++) {
61
61
  const impressionData: SplitIO.ImpressionData = {
62
62
  // copy of impression, to avoid unexpected behavior if modified by integrations or impressionListener
@@ -74,7 +74,7 @@ export function impressionsTrackerFactory(
74
74
  if (integrationsManager) integrationsManager.handleImpression(impressionData);
75
75
 
76
76
  try { // @ts-ignore. An exception on the listeners should not break the SDK.
77
- if (settings.impressionListener) settings.impressionListener.logImpression(impressionData);
77
+ if (impressionListener) impressionListener.logImpression(impressionData);
78
78
  } catch (err) {
79
79
  log.error(ERROR_IMPRESSIONS_LISTENER, [err]);
80
80
  }
@@ -1,11 +1,32 @@
1
- /* eslint-disable no-undef */
2
1
  export function isLocalStorageAvailable(): boolean {
2
+ try {
3
+ // eslint-disable-next-line no-undef
4
+ return isValidStorageWrapper(localStorage);
5
+ } catch (e) {
6
+ return false;
7
+ }
8
+ }
9
+
10
+ export function isValidStorageWrapper(wrapper: any): boolean {
3
11
  var mod = '__SPLITSOFTWARE__';
4
12
  try {
5
- localStorage.setItem(mod, mod);
6
- localStorage.removeItem(mod);
13
+ wrapper.setItem(mod, mod);
14
+ wrapper.getItem(mod);
15
+ wrapper.removeItem(mod);
7
16
  return true;
8
17
  } catch (e) {
9
18
  return false;
10
19
  }
11
20
  }
21
+
22
+ export function isWebStorage(wrapper: any): boolean {
23
+ if (typeof wrapper.length === 'number') {
24
+ try {
25
+ wrapper.key(0);
26
+ return true;
27
+ } catch (e) {
28
+ return false;
29
+ }
30
+ }
31
+ return false;
32
+ }
@@ -69,12 +69,6 @@ function validateSplitFilter(log: ILogger, type: SplitIO.SplitFilterType, values
69
69
  /**
70
70
  * Returns a string representing the URL encoded query component of /splitChanges URL.
71
71
  *
72
- * The possible formats of the query string are:
73
- * - null: if all filters are empty
74
- * - '&names=<comma-separated-values>': if only `byPrefix` filter is undefined
75
- * - '&prefixes=<comma-separated-values>': if only `byName` filter is undefined
76
- * - '&names=<comma-separated-values>&prefixes=<comma-separated-values>': if no one is undefined
77
- *
78
72
  * @param groupedFilters - object of filters. Each filter must be a list of valid, unique and ordered string values.
79
73
  * @returns null or string with the `split filter query` component of the URL.
80
74
  */
@@ -8,7 +8,7 @@ import { IStorageFactoryParams, IStorageSync } from '../../../storages/types';
8
8
 
9
9
  export function __InLocalStorageMockFactory(params: IStorageFactoryParams): IStorageSync {
10
10
  const result = InMemoryStorageCSFactory(params);
11
- result.validateCache = () => true; // to emit SDK_READY_FROM_CACHE
11
+ result.validateCache = () => Promise.resolve(true); // to emit SDK_READY_FROM_CACHE
12
12
  return result;
13
13
  }
14
14
  __InLocalStorageMockFactory.type = STORAGE_MEMORY;
@@ -24,7 +24,11 @@ interface ISharedSettings {
24
24
  sync?: {
25
25
  /**
26
26
  * List of feature flag filters. These filters are used to fetch a subset of the feature flag definitions in your environment, in order to reduce the delay of the SDK to be ready.
27
- * This configuration is only meaningful when the SDK is working in "standalone" mode.
27
+ *
28
+ * NOTES:
29
+ * - This configuration is only meaningful when the SDK is working in `"standalone"` mode.
30
+ * - If `bySet` filter is provided, `byName` and `byPrefix` filters are ignored.
31
+ * - If both `byName` and `byPrefix` filters are provided, the intersection of the two groups of feature flags is fetched.
28
32
  *
29
33
  * Example:
30
34
  * ```
@@ -66,12 +70,17 @@ interface ISharedSettings {
66
70
  *
67
71
  * @example
68
72
  * ```
69
- * const getHeaderOverrides = (context) => {
70
- * return {
71
- * 'Authorization': context.headers['Authorization'] + ', other-value',
72
- * 'custom-header': 'custom-value'
73
- * };
74
- * };
73
+ * const factory = SplitFactory({
74
+ * ...
75
+ * sync: {
76
+ * getHeaderOverrides: (context) => {
77
+ * return {
78
+ * 'Authorization': context.headers['Authorization'] + ', other-value',
79
+ * 'custom-header': 'custom-value'
80
+ * };
81
+ * }
82
+ * }
83
+ * });
75
84
  * ```
76
85
  */
77
86
  getHeaderOverrides?: (context: { headers: Record<string, string> }) => Record<string, string>;
@@ -449,6 +458,36 @@ interface IClientSideSyncSharedSettings extends IClientSideSharedSettings, ISync
449
458
  */
450
459
  declare namespace SplitIO {
451
460
 
461
+ interface SyncStorageWrapper {
462
+ /**
463
+ * Returns the value associated with the given key, or null if the key does not exist.
464
+ */
465
+ getItem(key: string): string | null;
466
+ /**
467
+ * Sets the value for the given key, creating a new key/value pair if key does not exist.
468
+ */
469
+ setItem(key: string, value: string): void;
470
+ /**
471
+ * Removes the key/value pair for the given key, if the key exists.
472
+ */
473
+ removeItem(key: string): void;
474
+ }
475
+
476
+ interface AsyncStorageWrapper {
477
+ /**
478
+ * Returns a promise that resolves to the value associated with the given key, or null if the key does not exist.
479
+ */
480
+ getItem(key: string): Promise<string | null>;
481
+ /**
482
+ * Returns a promise that resolves when the value of the pair identified by key is set to value, creating a new key/value pair if key does not exist.
483
+ */
484
+ setItem(key: string, value: string): Promise<void>;
485
+ /**
486
+ * Returns a promise that resolves when the key/value pair for the given key is removed, if the key exists.
487
+ */
488
+ removeItem(key: string): Promise<void>;
489
+ }
490
+
452
491
  /**
453
492
  * EventEmitter interface based on a subset of the Node.js EventEmitter methods.
454
493
  */
@@ -952,7 +991,7 @@ declare namespace SplitIO {
952
991
  */
953
992
  prefix?: string;
954
993
  /**
955
- * Number of days before cached data expires if it was not updated. If cache expires, it is cleared on initialization.
994
+ * Number of days before cached data expires if it was not successfully synchronized (i.e., last SDK_READY or SDK_UPDATE event emitted). If cache expires, it is cleared on initialization.
956
995
  *
957
996
  * @defaultValue `10`
958
997
  */
@@ -963,6 +1002,12 @@ declare namespace SplitIO {
963
1002
  * @defaultValue `false`
964
1003
  */
965
1004
  clearOnInit?: boolean;
1005
+ /**
1006
+ * Optional storage wrapper to persist rollout plan related data. If not provided, the SDK will use the default localStorage Web API.
1007
+ *
1008
+ * @defaultValue `window.localStorage`
1009
+ */
1010
+ wrapper?: SyncStorageWrapper | AsyncStorageWrapper;
966
1011
  }
967
1012
  /**
968
1013
  * Storage for asynchronous (consumer) SDK.
@@ -1130,7 +1175,7 @@ declare namespace SplitIO {
1130
1175
  */
1131
1176
  type: SplitFilterType;
1132
1177
  /**
1133
- * List of values: feature flag names for 'byName' filter type, and feature flag name prefixes for 'byPrefix' type.
1178
+ * List of values: flag set names for 'bySet' filter type, feature flag names for 'byName' filter type, and feature flag name prefixes for 'byPrefix' type.
1134
1179
  */
1135
1180
  values: string[];
1136
1181
  }
@@ -1292,7 +1337,7 @@ declare namespace SplitIO {
1292
1337
  */
1293
1338
  prefix?: string;
1294
1339
  /**
1295
- * Optional settings for the 'LOCALSTORAGE' storage type. It specifies the number of days before cached data expires if it was not updated. If cache expires, it is cleared on initialization.
1340
+ * Optional settings for the 'LOCALSTORAGE' storage type. It specifies the number of days before cached data expires if it was not successfully synchronized (i.e., last SDK_READY or SDK_UPDATE event emitted). If cache expires, it is cleared on initialization.
1296
1341
  *
1297
1342
  * @defaultValue `10`
1298
1343
  */
@@ -1303,6 +1348,12 @@ declare namespace SplitIO {
1303
1348
  * @defaultValue `false`
1304
1349
  */
1305
1350
  clearOnInit?: boolean;
1351
+ /**
1352
+ * Optional storage wrapper to persist rollout plan related data. If not provided, the SDK will use the default localStorage Web API.
1353
+ *
1354
+ * @defaultValue `window.localStorage`
1355
+ */
1356
+ wrapper?: SyncStorageWrapper | AsyncStorageWrapper;
1306
1357
  };
1307
1358
  }
1308
1359
  /**
@@ -1350,12 +1401,17 @@ declare namespace SplitIO {
1350
1401
  *
1351
1402
  * @example
1352
1403
  * ```
1353
- * const getHeaderOverrides = (context) => {
1354
- * return {
1355
- * 'Authorization': context.headers['Authorization'] + ', other-value',
1356
- * 'custom-header': 'custom-value'
1357
- * };
1358
- * };
1404
+ * const factory = SplitFactory({
1405
+ * ...
1406
+ * sync: {
1407
+ * getHeaderOverrides: (context) => {
1408
+ * return {
1409
+ * 'Authorization': context.headers['Authorization'] + ', other-value',
1410
+ * 'custom-header': 'custom-value'
1411
+ * };
1412
+ * }
1413
+ * }
1414
+ * });
1359
1415
  * ```
1360
1416
  */
1361
1417
  getHeaderOverrides?: (context: { headers: Record<string, string> }) => Record<string, string>;