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

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 (67) 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/AbstractMySegmentsCacheSync.js +31 -23
  7. package/cjs/storages/AbstractSplitsCacheSync.js +8 -3
  8. package/cjs/storages/inLocalStorage/MySegmentsCacheInLocal.js +16 -16
  9. package/cjs/storages/inLocalStorage/RBSegmentsCacheInLocal.js +20 -19
  10. package/cjs/storages/inLocalStorage/SplitsCacheInLocal.js +33 -37
  11. package/cjs/storages/inLocalStorage/index.js +28 -13
  12. package/cjs/storages/inLocalStorage/storageAdapter.js +48 -0
  13. package/cjs/storages/inLocalStorage/validateCache.js +25 -23
  14. package/cjs/sync/offline/syncTasks/fromObjectSyncTask.js +2 -3
  15. package/cjs/sync/polling/pollingManagerCS.js +5 -4
  16. package/cjs/sync/polling/updaters/mySegmentsUpdater.js +3 -2
  17. package/cjs/sync/polling/updaters/splitChangesUpdater.js +1 -1
  18. package/cjs/sync/syncManagerOnline.js +31 -26
  19. package/cjs/trackers/impressionsTracker.js +4 -4
  20. package/cjs/utils/env/isLocalStorageAvailable.js +28 -5
  21. package/cjs/utils/settingsValidation/splitFilters.js +0 -6
  22. package/cjs/utils/settingsValidation/storage/storageCS.js +1 -1
  23. package/esm/logger/messages/error.js +1 -1
  24. package/esm/sdkClient/sdkClient.js +0 -1
  25. package/esm/sdkFactory/index.js +3 -10
  26. package/esm/services/splitHttpClient.js +1 -1
  27. package/esm/storages/AbstractMySegmentsCacheSync.js +31 -23
  28. package/esm/storages/AbstractSplitsCacheSync.js +6 -2
  29. package/esm/storages/inLocalStorage/MySegmentsCacheInLocal.js +16 -16
  30. package/esm/storages/inLocalStorage/RBSegmentsCacheInLocal.js +20 -19
  31. package/esm/storages/inLocalStorage/SplitsCacheInLocal.js +33 -37
  32. package/esm/storages/inLocalStorage/index.js +29 -14
  33. package/esm/storages/inLocalStorage/storageAdapter.js +44 -0
  34. package/esm/storages/inLocalStorage/validateCache.js +25 -23
  35. package/esm/sync/offline/syncTasks/fromObjectSyncTask.js +2 -3
  36. package/esm/sync/polling/pollingManagerCS.js +5 -4
  37. package/esm/sync/polling/updaters/mySegmentsUpdater.js +3 -2
  38. package/esm/sync/polling/updaters/splitChangesUpdater.js +1 -1
  39. package/esm/sync/syncManagerOnline.js +31 -26
  40. package/esm/trackers/impressionsTracker.js +4 -4
  41. package/esm/utils/env/isLocalStorageAvailable.js +24 -3
  42. package/esm/utils/settingsValidation/splitFilters.js +0 -6
  43. package/esm/utils/settingsValidation/storage/storageCS.js +1 -1
  44. package/package.json +1 -1
  45. package/src/logger/messages/error.ts +1 -1
  46. package/src/sdkClient/sdkClient.ts +0 -1
  47. package/src/sdkFactory/index.ts +3 -13
  48. package/src/services/splitHttpClient.ts +1 -1
  49. package/src/storages/AbstractMySegmentsCacheSync.ts +26 -20
  50. package/src/storages/AbstractSplitsCacheSync.ts +8 -3
  51. package/src/storages/inLocalStorage/MySegmentsCacheInLocal.ts +18 -17
  52. package/src/storages/inLocalStorage/RBSegmentsCacheInLocal.ts +22 -20
  53. package/src/storages/inLocalStorage/SplitsCacheInLocal.ts +34 -37
  54. package/src/storages/inLocalStorage/index.ts +33 -16
  55. package/src/storages/inLocalStorage/storageAdapter.ts +50 -0
  56. package/src/storages/inLocalStorage/validateCache.ts +26 -23
  57. package/src/storages/types.ts +17 -1
  58. package/src/sync/offline/syncTasks/fromObjectSyncTask.ts +1 -2
  59. package/src/sync/polling/pollingManagerCS.ts +5 -4
  60. package/src/sync/polling/updaters/mySegmentsUpdater.ts +3 -2
  61. package/src/sync/polling/updaters/splitChangesUpdater.ts +1 -1
  62. package/src/sync/syncManagerOnline.ts +30 -24
  63. package/src/trackers/impressionsTracker.ts +3 -3
  64. package/src/utils/env/isLocalStorageAvailable.ts +24 -3
  65. package/src/utils/settingsValidation/splitFilters.ts +0 -6
  66. package/src/utils/settingsValidation/storage/storageCS.ts +1 -1
  67. package/types/splitio.d.ts +57 -16
@@ -6,29 +6,28 @@ import { ILogger } from '../../logger/types';
6
6
  import { LOG_PREFIX } from './constants';
7
7
  import { ISettings } from '../../types';
8
8
  import { setToArray } from '../../utils/lang/sets';
9
+ import { StorageAdapter } from '../types';
9
10
 
10
- /**
11
- * ISplitsCacheSync implementation that stores split definitions in browser LocalStorage.
12
- */
13
11
  export class SplitsCacheInLocal extends AbstractSplitsCacheSync {
14
12
 
15
13
  private readonly keys: KeyBuilderCS;
16
14
  private readonly log: ILogger;
17
15
  private readonly flagSetsFilter: string[];
18
16
  private hasSync?: boolean;
17
+ private readonly storage: StorageAdapter;
19
18
 
20
- constructor(settings: ISettings, keys: KeyBuilderCS) {
19
+ constructor(settings: ISettings, keys: KeyBuilderCS, storage: StorageAdapter) {
21
20
  super();
22
21
  this.keys = keys;
23
22
  this.log = settings.log;
24
23
  this.flagSetsFilter = settings.sync.__splitFiltersValidation.groupedFilters.bySet;
24
+ this.storage = storage;
25
25
  }
26
26
 
27
27
  private _decrementCount(key: string) {
28
- const count = toNumber(localStorage.getItem(key)) - 1;
29
- // @ts-expect-error
30
- if (count > 0) localStorage.setItem(key, count);
31
- else localStorage.removeItem(key);
28
+ const count = toNumber(this.storage.getItem(key)) - 1;
29
+ if (count > 0) this.storage.setItem(key, count + '');
30
+ else this.storage.removeItem(key);
32
31
  }
33
32
 
34
33
  private _decrementCounts(split: ISplit) {
@@ -48,13 +47,11 @@ export class SplitsCacheInLocal extends AbstractSplitsCacheSync {
48
47
  private _incrementCounts(split: ISplit) {
49
48
  try {
50
49
  const ttKey = this.keys.buildTrafficTypeKey(split.trafficTypeName);
51
- // @ts-expect-error
52
- localStorage.setItem(ttKey, toNumber(localStorage.getItem(ttKey)) + 1);
50
+ this.storage.setItem(ttKey, (toNumber(this.storage.getItem(ttKey)) + 1) + '');
53
51
 
54
52
  if (usesSegments(split)) {
55
53
  const segmentsCountKey = this.keys.buildSplitsWithSegmentCountKey();
56
- // @ts-expect-error
57
- localStorage.setItem(segmentsCountKey, toNumber(localStorage.getItem(segmentsCountKey)) + 1);
54
+ this.storage.setItem(segmentsCountKey, (toNumber(this.storage.getItem(segmentsCountKey)) + 1) + '');
58
55
  }
59
56
  } catch (e) {
60
57
  this.log.error(LOG_PREFIX + e);
@@ -68,15 +65,15 @@ export class SplitsCacheInLocal extends AbstractSplitsCacheSync {
68
65
  */
69
66
  clear() {
70
67
  // collect item keys
71
- const len = localStorage.length;
68
+ const len = this.storage.length;
72
69
  const accum = [];
73
70
  for (let cur = 0; cur < len; cur++) {
74
- const key = localStorage.key(cur);
71
+ const key = this.storage.key(cur);
75
72
  if (key != null && this.keys.isSplitsCacheKey(key)) accum.push(key);
76
73
  }
77
74
  // remove items
78
75
  accum.forEach(key => {
79
- localStorage.removeItem(key);
76
+ this.storage.removeItem(key);
80
77
  });
81
78
 
82
79
  this.hasSync = false;
@@ -86,15 +83,15 @@ export class SplitsCacheInLocal extends AbstractSplitsCacheSync {
86
83
  try {
87
84
  const name = split.name;
88
85
  const splitKey = this.keys.buildSplitKey(name);
89
- const splitFromLocalStorage = localStorage.getItem(splitKey);
90
- const previousSplit = splitFromLocalStorage ? JSON.parse(splitFromLocalStorage) : null;
86
+ const splitFromStorage = this.storage.getItem(splitKey);
87
+ const previousSplit = splitFromStorage ? JSON.parse(splitFromStorage) : null;
91
88
 
92
89
  if (previousSplit) {
93
90
  this._decrementCounts(previousSplit);
94
91
  this.removeFromFlagSets(previousSplit.name, previousSplit.sets);
95
92
  }
96
93
 
97
- localStorage.setItem(splitKey, JSON.stringify(split));
94
+ this.storage.setItem(splitKey, JSON.stringify(split));
98
95
 
99
96
  this._incrementCounts(split);
100
97
  this.addToFlagSets(split);
@@ -111,7 +108,7 @@ export class SplitsCacheInLocal extends AbstractSplitsCacheSync {
111
108
  const split = this.getSplit(name);
112
109
  if (!split) return false;
113
110
 
114
- localStorage.removeItem(this.keys.buildSplitKey(name));
111
+ this.storage.removeItem(this.keys.buildSplitKey(name));
115
112
 
116
113
  this._decrementCounts(split);
117
114
  this.removeFromFlagSets(split.name, split.sets);
@@ -124,15 +121,15 @@ export class SplitsCacheInLocal extends AbstractSplitsCacheSync {
124
121
  }
125
122
 
126
123
  getSplit(name: string): ISplit | null {
127
- const item = localStorage.getItem(this.keys.buildSplitKey(name));
124
+ const item = this.storage.getItem(this.keys.buildSplitKey(name));
128
125
  return item && JSON.parse(item);
129
126
  }
130
127
 
131
128
  setChangeNumber(changeNumber: number): boolean {
132
129
  try {
133
- localStorage.setItem(this.keys.buildSplitsTillKey(), changeNumber + '');
130
+ this.storage.setItem(this.keys.buildSplitsTillKey(), changeNumber + '');
134
131
  // update "last updated" timestamp with current time
135
- localStorage.setItem(this.keys.buildLastUpdatedKey(), Date.now() + '');
132
+ this.storage.setItem(this.keys.buildLastUpdatedKey(), Date.now() + '');
136
133
  this.hasSync = true;
137
134
  return true;
138
135
  } catch (e) {
@@ -143,7 +140,7 @@ export class SplitsCacheInLocal extends AbstractSplitsCacheSync {
143
140
 
144
141
  getChangeNumber(): number {
145
142
  const n = -1;
146
- let value: string | number | null = localStorage.getItem(this.keys.buildSplitsTillKey());
143
+ let value: string | number | null = this.storage.getItem(this.keys.buildSplitsTillKey());
147
144
 
148
145
  if (value !== null) {
149
146
  value = parseInt(value, 10);
@@ -155,13 +152,13 @@ export class SplitsCacheInLocal extends AbstractSplitsCacheSync {
155
152
  }
156
153
 
157
154
  getSplitNames(): string[] {
158
- const len = localStorage.length;
155
+ const len = this.storage.length;
159
156
  const accum = [];
160
157
 
161
158
  let cur = 0;
162
159
 
163
160
  while (cur < len) {
164
- const key = localStorage.key(cur);
161
+ const key = this.storage.key(cur);
165
162
 
166
163
  if (key != null && this.keys.isSplitKey(key)) accum.push(this.keys.extractKey(key));
167
164
 
@@ -172,7 +169,7 @@ export class SplitsCacheInLocal extends AbstractSplitsCacheSync {
172
169
  }
173
170
 
174
171
  trafficTypeExists(trafficType: string): boolean {
175
- const ttCount = toNumber(localStorage.getItem(this.keys.buildTrafficTypeKey(trafficType)));
172
+ const ttCount = toNumber(this.storage.getItem(this.keys.buildTrafficTypeKey(trafficType)));
176
173
  return isFiniteNumber(ttCount) && ttCount > 0;
177
174
  }
178
175
 
@@ -180,7 +177,7 @@ export class SplitsCacheInLocal extends AbstractSplitsCacheSync {
180
177
  // If cache hasn't been synchronized with the cloud, assume we need them.
181
178
  if (!this.hasSync) return true;
182
179
 
183
- const storedCount = localStorage.getItem(this.keys.buildSplitsWithSegmentCountKey());
180
+ const storedCount = this.storage.getItem(this.keys.buildSplitsWithSegmentCountKey());
184
181
  const splitsWithSegmentsCount = storedCount === null ? 0 : toNumber(storedCount);
185
182
 
186
183
  return isFiniteNumber(splitsWithSegmentsCount) ?
@@ -191,9 +188,9 @@ export class SplitsCacheInLocal extends AbstractSplitsCacheSync {
191
188
  getNamesByFlagSets(flagSets: string[]): Set<string>[] {
192
189
  return flagSets.map(flagSet => {
193
190
  const flagSetKey = this.keys.buildFlagSetKey(flagSet);
194
- const flagSetFromLocalStorage = localStorage.getItem(flagSetKey);
191
+ const flagSetFromStorage = this.storage.getItem(flagSetKey);
195
192
 
196
- return new Set(flagSetFromLocalStorage ? JSON.parse(flagSetFromLocalStorage) : []);
193
+ return new Set(flagSetFromStorage ? JSON.parse(flagSetFromStorage) : []);
197
194
  });
198
195
  }
199
196
 
@@ -206,12 +203,12 @@ export class SplitsCacheInLocal extends AbstractSplitsCacheSync {
206
203
 
207
204
  const flagSetKey = this.keys.buildFlagSetKey(featureFlagSet);
208
205
 
209
- const flagSetFromLocalStorage = localStorage.getItem(flagSetKey);
206
+ const flagSetFromStorage = this.storage.getItem(flagSetKey);
210
207
 
211
- const flagSetCache = new Set(flagSetFromLocalStorage ? JSON.parse(flagSetFromLocalStorage) : []);
208
+ const flagSetCache = new Set(flagSetFromStorage ? JSON.parse(flagSetFromStorage) : []);
212
209
  flagSetCache.add(featureFlag.name);
213
210
 
214
- localStorage.setItem(flagSetKey, JSON.stringify(setToArray(flagSetCache)));
211
+ this.storage.setItem(flagSetKey, JSON.stringify(setToArray(flagSetCache)));
215
212
  });
216
213
  }
217
214
 
@@ -226,19 +223,19 @@ export class SplitsCacheInLocal extends AbstractSplitsCacheSync {
226
223
  private removeNames(flagSetName: string, featureFlagName: string) {
227
224
  const flagSetKey = this.keys.buildFlagSetKey(flagSetName);
228
225
 
229
- const flagSetFromLocalStorage = localStorage.getItem(flagSetKey);
226
+ const flagSetFromStorage = this.storage.getItem(flagSetKey);
230
227
 
231
- if (!flagSetFromLocalStorage) return;
228
+ if (!flagSetFromStorage) return;
232
229
 
233
- const flagSetCache = new Set(JSON.parse(flagSetFromLocalStorage));
230
+ const flagSetCache = new Set(JSON.parse(flagSetFromStorage));
234
231
  flagSetCache.delete(featureFlagName);
235
232
 
236
233
  if (flagSetCache.size === 0) {
237
- localStorage.removeItem(flagSetKey);
234
+ this.storage.removeItem(flagSetKey);
238
235
  return;
239
236
  }
240
237
 
241
- localStorage.setItem(flagSetKey, JSON.stringify(setToArray(flagSetCache)));
238
+ this.storage.setItem(flagSetKey, JSON.stringify(setToArray(flagSetCache)));
242
239
  }
243
240
 
244
241
  }
@@ -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,12 @@ 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
+ destroy() {
75
+ return storage.save && storage.save();
76
+ },
60
77
 
61
78
  // When using shared instantiation with MEMORY we reuse everything but segments (they are customer per key).
62
79
  shared(matchingKey: string) {
@@ -64,8 +81,8 @@ export function InLocalStorage(options: SplitIO.InLocalStorageOptions = {}): ISt
64
81
  return {
65
82
  splits: this.splits,
66
83
  rbSegments: this.rbSegments,
67
- segments: new MySegmentsCacheInLocal(log, new KeyBuilderCS(prefix, matchingKey)),
68
- largeSegments: new MySegmentsCacheInLocal(log, myLargeSegmentsKeyBuilder(prefix, matchingKey)),
84
+ segments: new MySegmentsCacheInLocal(log, new KeyBuilderCS(prefix, matchingKey), storage),
85
+ largeSegments: new MySegmentsCacheInLocal(log, myLargeSegmentsKeyBuilder(prefix, matchingKey), storage),
69
86
  impressions: this.impressions,
70
87
  impressionCounts: this.impressionCounts,
71
88
  events: this.events,
@@ -0,0 +1,50 @@
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
+ function isTillKey(key: string) {
7
+ return key.endsWith('.till');
8
+ }
9
+
10
+ export function storageAdapter(log: ILogger, prefix: string, wrapper: SplitIO.StorageWrapper): StorageAdapter {
11
+ let cache: Record<string, string> = {};
12
+
13
+ let connectPromise: Promise<void> | undefined;
14
+ let disconnectPromise = Promise.resolve();
15
+
16
+ return {
17
+ load() {
18
+ return connectPromise || (connectPromise = Promise.resolve(wrapper.getItem(prefix)).then((storedCache) => {
19
+ cache = JSON.parse(storedCache || '{}');
20
+ }).catch((e) => {
21
+ log.error(LOG_PREFIX + 'Rejected promise calling storage getItem, with error: ' + e);
22
+ }));
23
+ },
24
+ save() {
25
+ return disconnectPromise = disconnectPromise.then(() => {
26
+ return Promise.resolve(wrapper.setItem(prefix, JSON.stringify(cache))).catch((e) => {
27
+ log.error(LOG_PREFIX + 'Rejected promise calling storage setItem, with error: ' + e);
28
+ });
29
+ });
30
+ },
31
+
32
+ get length() {
33
+ return Object.keys(cache).length;
34
+ },
35
+ getItem(key: string) {
36
+ return cache[key] || null;
37
+ },
38
+ key(index: number) {
39
+ return Object.keys(cache)[index] || null;
40
+ },
41
+ removeItem(key: string) {
42
+ delete cache[key];
43
+ if (isTillKey(key)) this.save!();
44
+ },
45
+ setItem(key: string, value: string) {
46
+ cache[key] = value;
47
+ if (isTillKey(key)) this.save!();
48
+ }
49
+ };
50
+ }
@@ -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,29 @@ 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
+ return false;
91
+ }
90
92
 
91
- // Check if ready from cache
92
- return isThereCache;
93
+ // Check if ready from cache
94
+ return isThereCache;
95
+ });
93
96
  }
@@ -4,6 +4,22 @@ 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
+ // Methods based on https://developer.mozilla.org/en-US/docs/Web/API/Storage
16
+ readonly length: number;
17
+ getItem(key: string): string | null;
18
+ key(index: number): string | null;
19
+ removeItem(key: string): void;
20
+ setItem(key: string, value: string): void;
21
+ }
22
+
7
23
  /**
8
24
  * Interface of a pluggable storage wrapper.
9
25
  */
@@ -479,7 +495,7 @@ export interface IStorageSync extends IStorageBase<
479
495
  IUniqueKeysCacheSync
480
496
  > {
481
497
  // Defined in client-side
482
- validateCache?: () => boolean, // @TODO support async
498
+ validateCache?: () => Promise<boolean>,
483
499
  largeSegments?: ISegmentsCacheSync,
484
500
  }
485
501
 
@@ -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
 
@@ -51,7 +52,7 @@ export function mySegmentsUpdaterFactory(
51
52
  }
52
53
 
53
54
  // Notify update if required
54
- if ((splits.usesSegments() || rbSegments.usesSegments()) && (shouldNotifyUpdate || readyOnAlreadyExistentState)) {
55
+ if (usesSegmentsSync(storage) && (shouldNotifyUpdate || readyOnAlreadyExistentState)) {
55
56
  readyOnAlreadyExistentState = false;
56
57
  segmentsEventEmitter.emit(SDK_SEGMENTS_ARRIVED);
57
58
  }
@@ -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
@@ -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();