@splitsoftware/splitio-commons 2.5.0 → 2.5.1-rc.0

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 (40) hide show
  1. package/CHANGES.txt +12 -0
  2. package/cjs/storages/inLocalStorage/MySegmentsCacheInLocal.js +16 -16
  3. package/cjs/storages/inLocalStorage/RBSegmentsCacheInLocal.js +17 -17
  4. package/cjs/storages/inLocalStorage/SplitsCacheInLocal.js +33 -37
  5. package/cjs/storages/inLocalStorage/index.js +31 -13
  6. package/cjs/storages/inLocalStorage/storageAdapter.js +54 -0
  7. package/cjs/storages/inLocalStorage/validateCache.js +28 -23
  8. package/cjs/sync/offline/syncTasks/fromObjectSyncTask.js +2 -3
  9. package/cjs/sync/polling/updaters/mySegmentsUpdater.js +2 -0
  10. package/cjs/sync/polling/updaters/splitChangesUpdater.js +2 -0
  11. package/cjs/sync/syncManagerOnline.js +28 -24
  12. package/cjs/utils/env/isLocalStorageAvailable.js +22 -1
  13. package/cjs/utils/settingsValidation/storage/storageCS.js +1 -1
  14. package/esm/storages/inLocalStorage/MySegmentsCacheInLocal.js +16 -16
  15. package/esm/storages/inLocalStorage/RBSegmentsCacheInLocal.js +17 -17
  16. package/esm/storages/inLocalStorage/SplitsCacheInLocal.js +33 -37
  17. package/esm/storages/inLocalStorage/index.js +32 -14
  18. package/esm/storages/inLocalStorage/storageAdapter.js +50 -0
  19. package/esm/storages/inLocalStorage/validateCache.js +28 -23
  20. package/esm/sync/offline/syncTasks/fromObjectSyncTask.js +2 -3
  21. package/esm/sync/polling/updaters/mySegmentsUpdater.js +2 -0
  22. package/esm/sync/polling/updaters/splitChangesUpdater.js +2 -0
  23. package/esm/sync/syncManagerOnline.js +28 -24
  24. package/esm/utils/env/isLocalStorageAvailable.js +19 -0
  25. package/esm/utils/settingsValidation/storage/storageCS.js +1 -1
  26. package/package.json +1 -1
  27. package/src/storages/inLocalStorage/MySegmentsCacheInLocal.ts +18 -17
  28. package/src/storages/inLocalStorage/RBSegmentsCacheInLocal.ts +19 -18
  29. package/src/storages/inLocalStorage/SplitsCacheInLocal.ts +34 -37
  30. package/src/storages/inLocalStorage/index.ts +37 -16
  31. package/src/storages/inLocalStorage/storageAdapter.ts +62 -0
  32. package/src/storages/inLocalStorage/validateCache.ts +29 -23
  33. package/src/storages/types.ts +19 -1
  34. package/src/sync/offline/syncTasks/fromObjectSyncTask.ts +1 -2
  35. package/src/sync/polling/updaters/mySegmentsUpdater.ts +2 -0
  36. package/src/sync/polling/updaters/splitChangesUpdater.ts +3 -1
  37. package/src/sync/syncManagerOnline.ts +27 -22
  38. package/src/utils/env/isLocalStorageAvailable.ts +20 -0
  39. package/src/utils/settingsValidation/storage/storageCS.ts +1 -1
  40. package/types/splitio.d.ts +30 -0
@@ -1,10 +1,10 @@
1
1
  import { ImpressionsCacheInMemory } from '../inMemory/ImpressionsCacheInMemory';
2
2
  import { ImpressionCountsCacheInMemory } from '../inMemory/ImpressionCountsCacheInMemory';
3
3
  import { EventsCacheInMemory } from '../inMemory/EventsCacheInMemory';
4
- import { IStorageFactoryParams, IStorageSync, IStorageSyncFactory } from '../types';
4
+ import { IStorageFactoryParams, IStorageSync, IStorageSyncFactory, StorageAdapter } from '../types';
5
5
  import { validatePrefix } from '../KeyBuilder';
6
6
  import { KeyBuilderCS, myLargeSegmentsKeyBuilder } from '../KeyBuilderCS';
7
- import { isLocalStorageAvailable } from '../../utils/env/isLocalStorageAvailable';
7
+ import { isLocalStorageAvailable, isValidStorageWrapper, isWebStorage } from '../../utils/env/isLocalStorageAvailable';
8
8
  import { SplitsCacheInLocal } from './SplitsCacheInLocal';
9
9
  import { RBSegmentsCacheInLocal } from './RBSegmentsCacheInLocal';
10
10
  import { MySegmentsCacheInLocal } from './MySegmentsCacheInLocal';
@@ -15,7 +15,24 @@ import { shouldRecordTelemetry, TelemetryCacheInMemory } from '../inMemory/Telem
15
15
  import { UniqueKeysCacheInMemoryCS } from '../inMemory/UniqueKeysCacheInMemoryCS';
16
16
  import { getMatching } from '../../utils/key';
17
17
  import { validateCache } from './validateCache';
18
+ import { ILogger } from '../../logger/types';
18
19
  import SplitIO from '../../../types/splitio';
20
+ import { storageAdapter } from './storageAdapter';
21
+
22
+ function validateStorage(log: ILogger, prefix: string, wrapper?: SplitIO.StorageWrapper): StorageAdapter | undefined {
23
+ if (wrapper) {
24
+ if (isValidStorageWrapper(wrapper)) {
25
+ return isWebStorage(wrapper) ?
26
+ wrapper as StorageAdapter: // localStorage and sessionStorage don't need adapter
27
+ storageAdapter(log, prefix, wrapper);
28
+ }
29
+ log.warn(LOG_PREFIX + 'Invalid storage provided. Falling back to LocalStorage API');
30
+ }
31
+
32
+ if (isLocalStorageAvailable()) return localStorage;
33
+
34
+ log.warn(LOG_PREFIX + 'LocalStorage API is unavailable. Falling back to default MEMORY storage');
35
+ }
19
36
 
20
37
  /**
21
38
  * InLocal storage factory for standalone client-side SplitFactory
@@ -25,21 +42,19 @@ export function InLocalStorage(options: SplitIO.InLocalStorageOptions = {}): ISt
25
42
  const prefix = validatePrefix(options.prefix);
26
43
 
27
44
  function InLocalStorageCSFactory(params: IStorageFactoryParams): IStorageSync {
45
+ const { settings, settings: { log, scheduler: { impressionsQueueSize, eventsQueueSize } } } = params;
28
46
 
29
- // Fallback to InMemoryStorage if LocalStorage API is not available
30
- if (!isLocalStorageAvailable()) {
31
- params.settings.log.warn(LOG_PREFIX + 'LocalStorage API is unavailable. Falling back to default MEMORY storage');
32
- return InMemoryStorageCSFactory(params);
33
- }
47
+ const storage = validateStorage(log, prefix, options.wrapper);
48
+ if (!storage) return InMemoryStorageCSFactory(params);
34
49
 
35
- const { settings, settings: { log, scheduler: { impressionsQueueSize, eventsQueueSize } } } = params;
36
50
  const matchingKey = getMatching(settings.core.key);
37
51
  const keys = new KeyBuilderCS(prefix, matchingKey);
38
52
 
39
- const splits = new SplitsCacheInLocal(settings, keys);
40
- const rbSegments = new RBSegmentsCacheInLocal(settings, keys);
41
- const segments = new MySegmentsCacheInLocal(log, keys);
42
- const largeSegments = new MySegmentsCacheInLocal(log, myLargeSegmentsKeyBuilder(prefix, matchingKey));
53
+ const splits = new SplitsCacheInLocal(settings, keys, storage);
54
+ const rbSegments = new RBSegmentsCacheInLocal(settings, keys, storage);
55
+ const segments = new MySegmentsCacheInLocal(log, keys, storage);
56
+ const largeSegments = new MySegmentsCacheInLocal(log, myLargeSegmentsKeyBuilder(prefix, matchingKey), storage);
57
+ let validateCachePromise: Promise<boolean> | undefined;
43
58
 
44
59
  return {
45
60
  splits,
@@ -53,10 +68,16 @@ export function InLocalStorage(options: SplitIO.InLocalStorageOptions = {}): ISt
53
68
  uniqueKeys: new UniqueKeysCacheInMemoryCS(),
54
69
 
55
70
  validateCache() {
56
- return validateCache(options, settings, keys, splits, rbSegments, segments, largeSegments);
71
+ return validateCachePromise || (validateCachePromise = validateCache(options, storage, settings, keys, splits, rbSegments, segments, largeSegments));
57
72
  },
58
73
 
59
- destroy() { },
74
+ save() {
75
+ return storage.save && storage.save();
76
+ },
77
+
78
+ destroy() {
79
+ return storage.whenSaved && storage.whenSaved();
80
+ },
60
81
 
61
82
  // When using shared instantiation with MEMORY we reuse everything but segments (they are customer per key).
62
83
  shared(matchingKey: string) {
@@ -64,8 +85,8 @@ export function InLocalStorage(options: SplitIO.InLocalStorageOptions = {}): ISt
64
85
  return {
65
86
  splits: this.splits,
66
87
  rbSegments: this.rbSegments,
67
- segments: new MySegmentsCacheInLocal(log, new KeyBuilderCS(prefix, matchingKey)),
68
- largeSegments: new MySegmentsCacheInLocal(log, myLargeSegmentsKeyBuilder(prefix, matchingKey)),
88
+ segments: new MySegmentsCacheInLocal(log, new KeyBuilderCS(prefix, matchingKey), storage),
89
+ largeSegments: new MySegmentsCacheInLocal(log, myLargeSegmentsKeyBuilder(prefix, matchingKey), storage),
69
90
  impressions: this.impressions,
70
91
  impressionCounts: this.impressionCounts,
71
92
  events: this.events,
@@ -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.StorageWrapper): 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, initialRolloutPlan } = 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
  */
@@ -467,6 +484,7 @@ export interface IStorageBase<
467
484
  uniqueKeys: TUniqueKeysCache,
468
485
  destroy(): void | Promise<void>,
469
486
  shared?: (matchingKey: string, onReadyCb?: (error?: any) => void) => this
487
+ save?: () => void | Promise<void>,
470
488
  }
471
489
 
472
490
  export interface IStorageSync extends IStorageBase<
@@ -480,7 +498,7 @@ export interface IStorageSync extends IStorageBase<
480
498
  IUniqueKeysCacheSync
481
499
  > {
482
500
  // Defined in client-side
483
- validateCache?: () => boolean, // @TODO support async
501
+ validateCache?: () => Promise<boolean>,
484
502
  largeSegments?: ISegmentsCacheSync,
485
503
  }
486
504
 
@@ -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
@@ -51,6 +51,8 @@ export function mySegmentsUpdaterFactory(
51
51
  shouldNotifyUpdate = largeSegments!.resetSegments((segmentsData as IMembershipsResponse).ls || {}) || shouldNotifyUpdate;
52
52
  }
53
53
 
54
+ if (storage.save) storage.save();
55
+
54
56
  // Notify update if required
55
57
  if (usesSegmentsSync(storage) && (shouldNotifyUpdate || readyOnAlreadyExistentState)) {
56
58
  readyOnAlreadyExistentState = false;
@@ -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))))
@@ -89,36 +89,41 @@ export function syncManagerOnlineFactory(
89
89
  start() {
90
90
  running = true;
91
91
 
92
- if (startFirstTime) {
93
- const isCacheLoaded = storage.validateCache ? storage.validateCache() : false;
94
- if (isCacheLoaded) Promise.resolve().then(() => { readiness.splits.emit(SDK_SPLITS_CACHE_LOADED); });
95
- }
92
+ // @TODO once event, impression and telemetry storages support persistence, call when `validateCache` promise is resolved
93
+ submitterManager.start(!isConsentGranted(settings));
96
94
 
97
- // start syncing splits and segments
98
- if (pollingManager) {
95
+ return Promise.resolve(storage.validateCache ? storage.validateCache() : false).then((isCacheLoaded) => {
96
+ if (!running) return;
99
97
 
100
- // If synchronization is disabled pushManager and pollingManager should not start
101
- if (syncEnabled) {
102
- if (pushManager) {
103
- // 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 {
104
119
  if (startFirstTime) {
105
120
  pollingManager.syncAll();
106
121
  }
107
- pushManager.start();
108
- } else {
109
- pollingManager.start();
110
- }
111
- } else {
112
- if (startFirstTime) {
113
- pollingManager.syncAll();
114
122
  }
115
123
  }
116
- }
117
-
118
- // start periodic data recording (events, impressions, telemetry).
119
- submitterManager.start(!isConsentGranted(settings));
120
124
 
121
- startFirstTime = false;
125
+ startFirstTime = false;
126
+ });
122
127
  },
123
128
 
124
129
  /**
@@ -9,3 +9,23 @@ export function isLocalStorageAvailable(): boolean {
9
9
  return false;
10
10
  }
11
11
  }
12
+
13
+ export function isValidStorageWrapper(wrapper: any): boolean {
14
+ return wrapper !== null &&
15
+ typeof wrapper === 'object' &&
16
+ typeof wrapper.setItem === 'function' &&
17
+ typeof wrapper.getItem === 'function' &&
18
+ typeof wrapper.removeItem === 'function';
19
+ }
20
+
21
+ export function isWebStorage(wrapper: any): boolean {
22
+ if (typeof wrapper.length === 'number') {
23
+ try {
24
+ wrapper.key(0);
25
+ return true;
26
+ } catch (e) {
27
+ return false;
28
+ }
29
+ }
30
+ return false;
31
+ }
@@ -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;
@@ -463,6 +463,24 @@ interface IClientSideSyncSharedSettings extends IClientSideSharedSettings, ISync
463
463
  */
464
464
  declare namespace SplitIO {
465
465
 
466
+ interface StorageWrapper {
467
+ /**
468
+ * Returns the value associated with the given key, or null if the key does not exist.
469
+ * If the operation is asynchronous, returns a Promise.
470
+ */
471
+ getItem(key: string): string | null | Promise<string | null>;
472
+ /**
473
+ * Sets the value for the given key, creating a new key/value pair if key does not exist.
474
+ * If the operation is asynchronous, returns a Promise.
475
+ */
476
+ setItem(key: string, value: string): void | Promise<void>;
477
+ /**
478
+ * Removes the key/value pair for the given key, if the key exists.
479
+ * If the operation is asynchronous, returns a Promise.
480
+ */
481
+ removeItem(key: string): void | Promise<void>;
482
+ }
483
+
466
484
  /**
467
485
  * EventEmitter interface based on a subset of the Node.js EventEmitter methods.
468
486
  */
@@ -978,6 +996,12 @@ declare namespace SplitIO {
978
996
  * @defaultValue `false`
979
997
  */
980
998
  clearOnInit?: boolean;
999
+ /**
1000
+ * Optional storage wrapper to persist rollout plan related data. If not provided, the SDK will use the default localStorage Web API.
1001
+ *
1002
+ * @defaultValue `window.localStorage`
1003
+ */
1004
+ wrapper?: StorageWrapper;
981
1005
  }
982
1006
  /**
983
1007
  * Storage for asynchronous (consumer) SDK.
@@ -1339,6 +1363,12 @@ declare namespace SplitIO {
1339
1363
  * @defaultValue `false`
1340
1364
  */
1341
1365
  clearOnInit?: boolean;
1366
+ /**
1367
+ * Optional storage wrapper to persist rollout plan related data. If not provided, the SDK will use the default localStorage Web API.
1368
+ *
1369
+ * @defaultValue `window.localStorage`
1370
+ */
1371
+ wrapper?: StorageWrapper;
1342
1372
  };
1343
1373
  }
1344
1374
  /**