@splitsoftware/splitio-commons 2.4.2-rc.2 → 2.5.0-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 (51) hide show
  1. package/CHANGES.txt +2 -10
  2. package/cjs/storages/AbstractMySegmentsCacheSync.js +23 -31
  3. package/cjs/storages/AbstractSplitsCacheSync.js +2 -3
  4. package/cjs/storages/dataLoader.js +102 -43
  5. package/cjs/storages/inLocalStorage/MySegmentsCacheInLocal.js +16 -16
  6. package/cjs/storages/inLocalStorage/RBSegmentsCacheInLocal.js +23 -20
  7. package/cjs/storages/inLocalStorage/SplitsCacheInLocal.js +37 -33
  8. package/cjs/storages/inLocalStorage/index.js +13 -28
  9. package/cjs/storages/inLocalStorage/validateCache.js +23 -25
  10. package/cjs/storages/inMemory/InMemoryStorageCS.js +32 -14
  11. package/cjs/storages/inMemory/RBSegmentsCacheInMemory.js +4 -0
  12. package/cjs/sync/offline/syncTasks/fromObjectSyncTask.js +3 -2
  13. package/cjs/sync/syncManagerOnline.js +24 -28
  14. package/cjs/utils/env/isLocalStorageAvailable.js +5 -28
  15. package/cjs/utils/settingsValidation/storage/storageCS.js +1 -1
  16. package/esm/storages/AbstractMySegmentsCacheSync.js +23 -31
  17. package/esm/storages/AbstractSplitsCacheSync.js +2 -3
  18. package/esm/storages/dataLoader.js +99 -41
  19. package/esm/storages/inLocalStorage/MySegmentsCacheInLocal.js +16 -16
  20. package/esm/storages/inLocalStorage/RBSegmentsCacheInLocal.js +23 -20
  21. package/esm/storages/inLocalStorage/SplitsCacheInLocal.js +37 -33
  22. package/esm/storages/inLocalStorage/index.js +14 -29
  23. package/esm/storages/inLocalStorage/validateCache.js +23 -25
  24. package/esm/storages/inMemory/InMemoryStorageCS.js +32 -14
  25. package/esm/storages/inMemory/RBSegmentsCacheInMemory.js +4 -0
  26. package/esm/sync/offline/syncTasks/fromObjectSyncTask.js +3 -2
  27. package/esm/sync/syncManagerOnline.js +24 -28
  28. package/esm/utils/env/isLocalStorageAvailable.js +3 -24
  29. package/esm/utils/settingsValidation/storage/storageCS.js +1 -1
  30. package/package.json +1 -1
  31. package/src/sdkFactory/index.ts +3 -2
  32. package/src/storages/AbstractMySegmentsCacheSync.ts +20 -26
  33. package/src/storages/AbstractSplitsCacheSync.ts +2 -3
  34. package/src/storages/dataLoader.ts +107 -49
  35. package/src/storages/inLocalStorage/MySegmentsCacheInLocal.ts +17 -18
  36. package/src/storages/inLocalStorage/RBSegmentsCacheInLocal.ts +24 -22
  37. package/src/storages/inLocalStorage/SplitsCacheInLocal.ts +37 -34
  38. package/src/storages/inLocalStorage/index.ts +16 -33
  39. package/src/storages/inLocalStorage/validateCache.ts +23 -26
  40. package/src/storages/inMemory/InMemoryStorageCS.ts +37 -14
  41. package/src/storages/inMemory/RBSegmentsCacheInMemory.ts +4 -0
  42. package/src/storages/types.ts +6 -20
  43. package/src/sync/offline/syncTasks/fromObjectSyncTask.ts +2 -1
  44. package/src/sync/syncManagerOnline.ts +22 -27
  45. package/src/types.ts +0 -35
  46. package/src/utils/env/isLocalStorageAvailable.ts +3 -24
  47. package/src/utils/settingsValidation/storage/storageCS.ts +1 -1
  48. package/types/splitio.d.ts +46 -27
  49. package/cjs/storages/inLocalStorage/storageAdapter.js +0 -48
  50. package/esm/storages/inLocalStorage/storageAdapter.js +0 -44
  51. package/src/storages/inLocalStorage/storageAdapter.ts +0 -50
@@ -8,10 +8,10 @@ var MILLIS_IN_A_DAY = 86400000;
8
8
  *
9
9
  * @returns `true` if cache should be cleared, `false` otherwise
10
10
  */
11
- function validateExpiration(options, storage, settings, keys, currentTimestamp, isThereCache) {
11
+ function validateExpiration(options, settings, keys, currentTimestamp, isThereCache) {
12
12
  var log = settings.log;
13
13
  // Check expiration
14
- var lastUpdatedTimestamp = parseInt(storage.getItem(keys.buildLastUpdatedKey()), 10);
14
+ var lastUpdatedTimestamp = parseInt(localStorage.getItem(keys.buildLastUpdatedKey()), 10);
15
15
  if (!isNaNNumber(lastUpdatedTimestamp)) {
16
16
  var cacheExpirationInDays = isFiniteNumber(options.expirationDays) && options.expirationDays >= 1 ? options.expirationDays : DEFAULT_CACHE_EXPIRATION_IN_DAYS;
17
17
  var expirationTimestamp = currentTimestamp - MILLIS_IN_A_DAY * cacheExpirationInDays;
@@ -22,11 +22,11 @@ function validateExpiration(options, storage, settings, keys, currentTimestamp,
22
22
  }
23
23
  // Check hash
24
24
  var storageHashKey = keys.buildHashKey();
25
- var storageHash = storage.getItem(storageHashKey);
25
+ var storageHash = localStorage.getItem(storageHashKey);
26
26
  var currentStorageHash = getStorageHash(settings);
27
27
  if (storageHash !== currentStorageHash) {
28
28
  try {
29
- storage.setItem(storageHashKey, currentStorageHash);
29
+ localStorage.setItem(storageHashKey, currentStorageHash);
30
30
  }
31
31
  catch (e) {
32
32
  log.error(LOG_PREFIX + e);
@@ -39,7 +39,7 @@ function validateExpiration(options, storage, settings, keys, currentTimestamp,
39
39
  }
40
40
  // Clear on init
41
41
  if (options.clearOnInit) {
42
- var lastClearTimestamp = parseInt(storage.getItem(keys.buildLastClear()), 10);
42
+ var lastClearTimestamp = parseInt(localStorage.getItem(keys.buildLastClear()), 10);
43
43
  if (isNaNNumber(lastClearTimestamp) || lastClearTimestamp < currentTimestamp - MILLIS_IN_A_DAY) {
44
44
  log.info(LOG_PREFIX + 'clearOnInit was set and cache was not cleared in the last 24 hours. Cleaning up cache');
45
45
  return true;
@@ -54,25 +54,23 @@ function validateExpiration(options, storage, settings, keys, currentTimestamp,
54
54
  *
55
55
  * @returns `true` if cache is ready to be used, `false` otherwise (cache was cleared or there is no cache)
56
56
  */
57
- export function validateCache(options, storage, settings, keys, splits, rbSegments, segments, largeSegments) {
58
- return Promise.resolve(storage.load && storage.load()).then(function () {
59
- var currentTimestamp = Date.now();
60
- var isThereCache = splits.getChangeNumber() > -1;
61
- if (validateExpiration(options, storage, settings, keys, currentTimestamp, isThereCache)) {
62
- splits.clear();
63
- rbSegments.clear();
64
- segments.clear();
65
- largeSegments.clear();
66
- // Update last clear timestamp
67
- try {
68
- storage.setItem(keys.buildLastClear(), currentTimestamp + '');
69
- }
70
- catch (e) {
71
- settings.log.error(LOG_PREFIX + e);
72
- }
73
- return false;
57
+ export function validateCache(options, settings, keys, splits, rbSegments, segments, largeSegments) {
58
+ var currentTimestamp = Date.now();
59
+ var isThereCache = splits.getChangeNumber() > -1;
60
+ if (validateExpiration(options, settings, keys, currentTimestamp, isThereCache)) {
61
+ splits.clear();
62
+ rbSegments.clear();
63
+ segments.clear();
64
+ largeSegments.clear();
65
+ // Update last clear timestamp
66
+ try {
67
+ localStorage.setItem(keys.buildLastClear(), currentTimestamp + '');
68
+ }
69
+ catch (e) {
70
+ settings.log.error(LOG_PREFIX + e);
74
71
  }
75
- // Check if ready from cache
76
- return isThereCache;
77
- });
72
+ return false;
73
+ }
74
+ // Check if ready from cache
75
+ return isThereCache;
78
76
  }
@@ -6,6 +6,8 @@ import { ImpressionCountsCacheInMemory } from './ImpressionCountsCacheInMemory';
6
6
  import { LOCALHOST_MODE, STORAGE_MEMORY } from '../../utils/constants';
7
7
  import { shouldRecordTelemetry, TelemetryCacheInMemory } from './TelemetryCacheInMemory';
8
8
  import { UniqueKeysCacheInMemoryCS } from './UniqueKeysCacheInMemoryCS';
9
+ import { getMatching } from '../../utils/key';
10
+ import { setCache } from '../dataLoader';
9
11
  import { RBSegmentsCacheInMemory } from './RBSegmentsCacheInMemory';
10
12
  /**
11
13
  * InMemory storage factory for standalone client-side SplitFactory
@@ -13,7 +15,8 @@ import { RBSegmentsCacheInMemory } from './RBSegmentsCacheInMemory';
13
15
  * @param params - parameters required by EventsCacheSync
14
16
  */
15
17
  export function InMemoryStorageCSFactory(params) {
16
- var _a = params.settings, _b = _a.scheduler, impressionsQueueSize = _b.impressionsQueueSize, eventsQueueSize = _b.eventsQueueSize, __splitFiltersValidation = _a.sync.__splitFiltersValidation;
18
+ var _a = params.settings, log = _a.log, _b = _a.scheduler, impressionsQueueSize = _b.impressionsQueueSize, eventsQueueSize = _b.eventsQueueSize, __splitFiltersValidation = _a.sync.__splitFiltersValidation, preloadedData = _a.preloadedData, onReadyFromCacheCb = params.onReadyFromCacheCb;
19
+ var storages = {};
17
20
  var splits = new SplitsCacheInMemory(__splitFiltersValidation);
18
21
  var rbSegments = new RBSegmentsCacheInMemory();
19
22
  var segments = new MySegmentsCacheInMemory();
@@ -30,19 +33,27 @@ export function InMemoryStorageCSFactory(params) {
30
33
  uniqueKeys: new UniqueKeysCacheInMemoryCS(),
31
34
  destroy: function () { },
32
35
  // When using shared instantiation with MEMORY we reuse everything but segments (they are unique per key)
33
- shared: function () {
34
- return {
35
- splits: this.splits,
36
- rbSegments: this.rbSegments,
37
- segments: new MySegmentsCacheInMemory(),
38
- largeSegments: new MySegmentsCacheInMemory(),
39
- impressions: this.impressions,
40
- impressionCounts: this.impressionCounts,
41
- events: this.events,
42
- telemetry: this.telemetry,
43
- uniqueKeys: this.uniqueKeys,
44
- destroy: function () { }
45
- };
36
+ shared: function (matchingKey) {
37
+ if (!storages[matchingKey]) {
38
+ var segments_1 = new MySegmentsCacheInMemory();
39
+ var largeSegments_1 = new MySegmentsCacheInMemory();
40
+ if (preloadedData) {
41
+ setCache(log, preloadedData, { segments: segments_1, largeSegments: largeSegments_1 }, matchingKey);
42
+ }
43
+ storages[matchingKey] = {
44
+ splits: this.splits,
45
+ rbSegments: this.rbSegments,
46
+ segments: segments_1,
47
+ largeSegments: largeSegments_1,
48
+ impressions: this.impressions,
49
+ impressionCounts: this.impressionCounts,
50
+ events: this.events,
51
+ telemetry: this.telemetry,
52
+ uniqueKeys: this.uniqueKeys,
53
+ destroy: function () { }
54
+ };
55
+ }
56
+ return storages[matchingKey];
46
57
  },
47
58
  };
48
59
  // @TODO revisit storage logic in localhost mode
@@ -54,6 +65,13 @@ export function InMemoryStorageCSFactory(params) {
54
65
  storage.impressionCounts.track = noopTrack;
55
66
  storage.uniqueKeys.track = noopTrack;
56
67
  }
68
+ var matchingKey = getMatching(params.settings.core.key);
69
+ storages[matchingKey] = storage;
70
+ if (preloadedData) {
71
+ setCache(log, preloadedData, storage, matchingKey);
72
+ if (splits.getChangeNumber() > -1)
73
+ onReadyFromCacheCb();
74
+ }
57
75
  return storage;
58
76
  }
59
77
  InMemoryStorageCSFactory.type = STORAGE_MEMORY;
@@ -42,6 +42,10 @@ var RBSegmentsCacheInMemory = /** @class */ (function () {
42
42
  RBSegmentsCacheInMemory.prototype.get = function (name) {
43
43
  return this.cache[name] || null;
44
44
  };
45
+ RBSegmentsCacheInMemory.prototype.getAll = function () {
46
+ var _this = this;
47
+ return this.getNames().map(function (key) { return _this.get(key); });
48
+ };
45
49
  RBSegmentsCacheInMemory.prototype.contains = function (names) {
46
50
  var namesArray = setToArray(names);
47
51
  var namesInStorage = this.getNames();
@@ -42,9 +42,10 @@ export function fromObjectUpdaterFactory(splitsParser, storage, readiness, setti
42
42
  readiness.splits.emit(SDK_SPLITS_ARRIVED);
43
43
  if (startingUp) {
44
44
  startingUp = false;
45
- Promise.resolve(storage.validateCache ? storage.validateCache() : false).then(function (isCacheLoaded) {
45
+ var isCacheLoaded_1 = storage.validateCache ? storage.validateCache() : false;
46
+ Promise.resolve().then(function () {
46
47
  // Emits SDK_READY_FROM_CACHE
47
- if (isCacheLoaded)
48
+ if (isCacheLoaded_1)
48
49
  readiness.splits.emit(SDK_SPLITS_CACHE_LOADED);
49
50
  // Emits SDK_READY
50
51
  readiness.segments.emit(SDK_SEGMENTS_ARRIVED);
@@ -66,39 +66,35 @@ export function syncManagerOnlineFactory(pollingManagerFactory, pushManagerFacto
66
66
  */
67
67
  start: function () {
68
68
  running = true;
69
- // @TODO once event, impression and telemetry storages support persistence, call when `validateCache` promise is resolved
70
- submitterManager.start(!isConsentGranted(settings));
71
- return Promise.resolve(storage.validateCache ? storage.validateCache() : false).then(function (isCacheLoaded) {
72
- if (!running)
73
- return;
74
- if (startFirstTime) {
75
- // Emits SDK_READY_FROM_CACHE
76
- if (isCacheLoaded)
77
- readiness.splits.emit(SDK_SPLITS_CACHE_LOADED);
78
- }
79
- // start syncing splits and segments
80
- if (pollingManager) {
81
- // If synchronization is disabled pushManager and pollingManager should not start
82
- if (syncEnabled) {
83
- if (pushManager) {
84
- // Doesn't call `syncAll` when the syncManager is resuming
85
- if (startFirstTime) {
86
- pollingManager.syncAll();
87
- }
88
- pushManager.start();
89
- }
90
- else {
91
- pollingManager.start();
92
- }
93
- }
94
- else {
69
+ if (startFirstTime) {
70
+ var isCacheLoaded = storage.validateCache ? storage.validateCache() : false;
71
+ if (isCacheLoaded)
72
+ Promise.resolve().then(function () { readiness.splits.emit(SDK_SPLITS_CACHE_LOADED); });
73
+ }
74
+ // start syncing splits and segments
75
+ if (pollingManager) {
76
+ // If synchronization is disabled pushManager and pollingManager should not start
77
+ if (syncEnabled) {
78
+ if (pushManager) {
79
+ // Doesn't call `syncAll` when the syncManager is resuming
95
80
  if (startFirstTime) {
96
81
  pollingManager.syncAll();
97
82
  }
83
+ pushManager.start();
84
+ }
85
+ else {
86
+ pollingManager.start();
87
+ }
88
+ }
89
+ else {
90
+ if (startFirstTime) {
91
+ pollingManager.syncAll();
98
92
  }
99
93
  }
100
- startFirstTime = false;
101
- });
94
+ }
95
+ // start periodic data recording (events, impressions, telemetry).
96
+ submitterManager.start(!isConsentGranted(settings));
97
+ startFirstTime = false;
102
98
  },
103
99
  /**
104
100
  * Method used to stop/pause the syncManager.
@@ -1,33 +1,12 @@
1
+ /* eslint-disable no-undef */
1
2
  export function isLocalStorageAvailable() {
2
- try {
3
- // eslint-disable-next-line no-undef
4
- return isValidStorageWrapper(localStorage);
5
- }
6
- catch (e) {
7
- return false;
8
- }
9
- }
10
- export function isValidStorageWrapper(wrapper) {
11
3
  var mod = '__SPLITSOFTWARE__';
12
4
  try {
13
- wrapper.setItem(mod, mod);
14
- wrapper.getItem(mod);
15
- wrapper.removeItem(mod);
5
+ localStorage.setItem(mod, mod);
6
+ localStorage.removeItem(mod);
16
7
  return true;
17
8
  }
18
9
  catch (e) {
19
10
  return false;
20
11
  }
21
12
  }
22
- export function isWebStorage(wrapper) {
23
- if (typeof wrapper.length === 'number') {
24
- try {
25
- wrapper.key(0);
26
- return true;
27
- }
28
- catch (e) {
29
- return false;
30
- }
31
- }
32
- return false;
33
- }
@@ -3,7 +3,7 @@ import { ERROR_STORAGE_INVALID } from '../../../logger/constants';
3
3
  import { LOCALHOST_MODE, STANDALONE_MODE, STORAGE_PLUGGABLE, STORAGE_LOCALSTORAGE, STORAGE_MEMORY } from '../../../utils/constants';
4
4
  export function __InLocalStorageMockFactory(params) {
5
5
  var result = InMemoryStorageCSFactory(params);
6
- result.validateCache = function () { return Promise.resolve(true); }; // to emit SDK_READY_FROM_CACHE
6
+ result.validateCache = function () { return true; }; // to emit SDK_READY_FROM_CACHE
7
7
  return result;
8
8
  }
9
9
  __InLocalStorageMockFactory.type = STORAGE_MEMORY;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@splitsoftware/splitio-commons",
3
- "version": "2.4.2-rc.2",
3
+ "version": "2.5.0-rc.0",
4
4
  "description": "Split JavaScript SDK common components",
5
5
  "main": "cjs/index.js",
6
6
  "module": "esm/index.js",
@@ -43,7 +43,7 @@ export function sdkFactory(params: ISdkFactoryParams): SplitIO.ISDK | SplitIO.IA
43
43
 
44
44
  const storage = storageFactory({
45
45
  settings,
46
- onReadyCb: (error) => {
46
+ onReadyCb(error) {
47
47
  if (error) {
48
48
  // If storage fails to connect, SDK_READY_TIMED_OUT event is emitted immediately. Review when timeout and non-recoverable errors are reworked
49
49
  readiness.timeout();
@@ -52,10 +52,11 @@ export function sdkFactory(params: ISdkFactoryParams): SplitIO.ISDK | SplitIO.IA
52
52
  readiness.splits.emit(SDK_SPLITS_ARRIVED);
53
53
  readiness.segments.emit(SDK_SEGMENTS_ARRIVED);
54
54
  },
55
- onReadyFromCacheCb: () => {
55
+ onReadyFromCacheCb() {
56
56
  readiness.splits.emit(SDK_SPLITS_CACHE_LOADED);
57
57
  }
58
58
  });
59
+
59
60
  // @TODO add support for dataloader: `if (params.dataLoader) params.dataLoader(storage);`
60
61
  const clients: Record<string, SplitIO.IBasicClient> = {};
61
62
  const telemetryTracker = telemetryTrackerFactory(storage.telemetry, platform.now);
@@ -49,10 +49,12 @@ export abstract class AbstractMySegmentsCacheSync implements ISegmentsCacheSync
49
49
  * For client-side synchronizer: it resets or updates the cache.
50
50
  */
51
51
  resetSegments(segmentsData: MySegmentsData | IMySegmentsResponse): boolean {
52
+ this.setChangeNumber(segmentsData.cn);
53
+
52
54
  const { added, removed } = segmentsData as MySegmentsData;
53
- let isDiff = false;
54
55
 
55
56
  if (added && removed) {
57
+ let isDiff = false;
56
58
 
57
59
  added.forEach(segment => {
58
60
  isDiff = this.addSegment(segment) || isDiff;
@@ -61,40 +63,32 @@ export abstract class AbstractMySegmentsCacheSync implements ISegmentsCacheSync
61
63
  removed.forEach(segment => {
62
64
  isDiff = this.removeSegment(segment) || isDiff;
63
65
  });
64
- } else {
65
66
 
66
- const names = ((segmentsData as IMySegmentsResponse).k || []).map(s => s.n).sort();
67
- const storedSegmentKeys = this.getRegisteredSegments().sort();
67
+ return isDiff;
68
+ }
68
69
 
69
- // Extreme fast => everything is empty
70
- if (!names.length && !storedSegmentKeys.length) {
71
- isDiff = false;
72
- } else {
70
+ const names = ((segmentsData as IMySegmentsResponse).k || []).map(s => s.n).sort();
71
+ const storedSegmentKeys = this.getRegisteredSegments().sort();
73
72
 
74
- let index = 0;
73
+ // Extreme fast => everything is empty
74
+ if (!names.length && !storedSegmentKeys.length) return false;
75
75
 
76
- while (index < names.length && index < storedSegmentKeys.length && names[index] === storedSegmentKeys[index]) index++;
76
+ let index = 0;
77
77
 
78
- // Quick path => no changes
79
- if (index === names.length && index === storedSegmentKeys.length) {
80
- isDiff = false;
81
- } else {
78
+ while (index < names.length && index < storedSegmentKeys.length && names[index] === storedSegmentKeys[index]) index++;
82
79
 
83
- // Slowest path => add and/or remove segments
84
- for (let removeIndex = index; removeIndex < storedSegmentKeys.length; removeIndex++) {
85
- this.removeSegment(storedSegmentKeys[removeIndex]);
86
- }
80
+ // Quick path => no changes
81
+ if (index === names.length && index === storedSegmentKeys.length) return false;
87
82
 
88
- for (let addIndex = index; addIndex < names.length; addIndex++) {
89
- this.addSegment(names[addIndex]);
90
- }
83
+ // Slowest path => add and/or remove segments
84
+ for (let removeIndex = index; removeIndex < storedSegmentKeys.length; removeIndex++) {
85
+ this.removeSegment(storedSegmentKeys[removeIndex]);
86
+ }
91
87
 
92
- isDiff = true;
93
- }
94
- }
88
+ for (let addIndex = index; addIndex < names.length; addIndex++) {
89
+ this.addSegment(names[addIndex]);
95
90
  }
96
91
 
97
- this.setChangeNumber(segmentsData.cn);
98
- return isDiff;
92
+ return true;
99
93
  }
100
94
  }
@@ -14,10 +14,9 @@ export abstract class AbstractSplitsCacheSync implements ISplitsCacheSync {
14
14
  protected abstract setChangeNumber(changeNumber: number): boolean | void
15
15
 
16
16
  update(toAdd: ISplit[], toRemove: ISplit[], changeNumber: number): boolean {
17
- let updated = toAdd.map(addedFF => this.addSplit(addedFF)).some(result => result);
18
- updated = toRemove.map(removedFF => this.removeSplit(removedFF.name)).some(result => result) || updated;
19
17
  this.setChangeNumber(changeNumber);
20
- return updated;
18
+ const updated = toAdd.map(addedFF => this.addSplit(addedFF)).some(result => result);
19
+ return toRemove.map(removedFF => this.removeSplit(removedFF.name)).some(result => result) || updated;
21
20
  }
22
21
 
23
22
  abstract getSplit(name: string): ISplit | null
@@ -1,55 +1,113 @@
1
- import { PreloadedData } from '../types';
2
- import { DataLoader, ISegmentsCacheSync, ISplitsCacheSync } from './types';
3
-
4
- // This value might be eventually set via a config parameter
5
- const DEFAULT_CACHE_EXPIRATION_IN_MILLIS = 864000000; // 10 days
1
+ import SplitIO from '../../types/splitio';
2
+ import { IRBSegmentsCacheSync, ISegmentsCacheSync, ISplitsCacheSync, IStorageSync } from './types';
3
+ import { setToArray } from '../utils/lang/sets';
4
+ import { getMatching } from '../utils/key';
5
+ import { IMembershipsResponse, IMySegmentsResponse, IRBSegment, ISplit } from '../dtos/types';
6
+ import { ILogger } from '../logger/types';
6
7
 
7
8
  /**
8
- * Factory of client-side storage loader
9
- *
10
- * @param preloadedData - validated data following the format proposed in https://github.com/godaddy/split-javascript-data-loader
11
- * and extended with a `mySegmentsData` property.
12
- * @returns function to preload the storage
9
+ * Sets the given synchronous storage with the provided preloaded data snapshot.
10
+ * If `matchingKey` is provided, the storage is handled as a client-side storage (segments and largeSegments are instances of MySegmentsCache).
11
+ * Otherwise, the storage is handled as a server-side storage (segments is an instance of SegmentsCache).
13
12
  */
14
- export function dataLoaderFactory(preloadedData: PreloadedData): DataLoader {
15
-
16
- /**
17
- * Storage-agnostic adaptation of `loadDataIntoLocalStorage` function
18
- * (https://github.com/godaddy/split-javascript-data-loader/blob/master/src/load-data.js)
19
- *
20
- * @param storage - object containing `splits` and `segments` cache (client-side variant)
21
- * @param userId - user key string of the provided MySegmentsCache
22
- */
23
- // @TODO extend to support SegmentsCache (server-side variant) by making `userId` optional and adding the corresponding logic.
24
- // @TODO extend to load data on shared mySegments storages. Be specific when emitting SDK_READY_FROM_CACHE on shared clients. Maybe the serializer should provide the `useSegments` flag.
25
- return function loadData(storage: { splits: ISplitsCacheSync, segments: ISegmentsCacheSync }, userId: string) {
26
- // Do not load data if current preloadedData is empty
27
- if (Object.keys(preloadedData).length === 0) return;
28
-
29
- const { lastUpdated = -1, segmentsData = {}, since = -1, splitsData = {} } = preloadedData;
30
-
31
- const storedSince = storage.splits.getChangeNumber();
32
- const expirationTimestamp = Date.now() - DEFAULT_CACHE_EXPIRATION_IN_MILLIS;
33
-
34
- // Do not load data if current localStorage data is more recent,
35
- // or if its `lastUpdated` timestamp is older than the given `expirationTimestamp`,
36
- if (storedSince > since || lastUpdated < expirationTimestamp) return;
37
-
38
- // cleaning up the localStorage data, since some cached splits might need be part of the preloaded data
39
- storage.splits.clear();
40
-
41
- // splitsData in an object where the property is the split name and the pertaining value is a stringified json of its data
42
- storage.splits.update(Object.keys(splitsData).map(splitName => JSON.parse(splitsData[splitName])), [], since);
43
-
44
- // add mySegments data
45
- let mySegmentsData = preloadedData.mySegmentsData && preloadedData.mySegmentsData[userId];
46
- if (!mySegmentsData) {
47
- // segmentsData in an object where the property is the segment name and the pertaining value is a stringified object that contains the `added` array of userIds
48
- mySegmentsData = Object.keys(segmentsData).filter(segmentName => {
49
- const userIds = JSON.parse(segmentsData[segmentName]).added;
50
- return Array.isArray(userIds) && userIds.indexOf(userId) > -1;
51
- });
13
+ export function setCache(log: ILogger, preloadedData: SplitIO.PreloadedData, storage: { splits?: ISplitsCacheSync, rbSegments?: IRBSegmentsCacheSync, segments: ISegmentsCacheSync, largeSegments?: ISegmentsCacheSync }, matchingKey?: string) {
14
+ // Do not load data if current preloadedData is empty
15
+ if (Object.keys(preloadedData).length === 0) return;
16
+
17
+ const { splits, rbSegments, segments, largeSegments } = storage;
18
+
19
+ log.debug(`set cache${matchingKey ? ` for key ${matchingKey}` : ''}`);
20
+
21
+ if (splits) {
22
+ splits.clear();
23
+ splits.update(preloadedData.flags as ISplit[] || [], [], preloadedData.since || -1);
24
+ }
25
+
26
+ if (rbSegments) {
27
+ rbSegments.clear();
28
+ rbSegments.update(preloadedData.rbSegments as IRBSegment[] || [], [], preloadedData.rbSince || -1);
29
+ }
30
+
31
+ const segmentsData = preloadedData.segments || {};
32
+ if (matchingKey) { // add memberships data (client-side)
33
+ let memberships = preloadedData.memberships && preloadedData.memberships[matchingKey];
34
+ if (!memberships && segmentsData) {
35
+ memberships = {
36
+ ms: {
37
+ k: Object.keys(segmentsData).filter(segmentName => {
38
+ const segmentKeys = segmentsData[segmentName];
39
+ return segmentKeys.indexOf(matchingKey) > -1;
40
+ }).map(segmentName => ({ n: segmentName }))
41
+ }
42
+ };
43
+ }
44
+
45
+ if (memberships) {
46
+ if ((memberships as IMembershipsResponse).ms) segments.resetSegments((memberships as IMembershipsResponse).ms!);
47
+ if ((memberships as IMembershipsResponse).ls && largeSegments) largeSegments.resetSegments((memberships as IMembershipsResponse).ls!);
52
48
  }
53
- storage.segments.resetSegments({ k: mySegmentsData.map(s => ({ n: s })) });
49
+ } else { // add segments data (server-side)
50
+ Object.keys(segmentsData).forEach(segmentName => {
51
+ const segmentKeys = segmentsData[segmentName];
52
+ segments.update(segmentName, segmentKeys, [], -1);
53
+ });
54
+ }
55
+ }
56
+
57
+ /**
58
+ * Gets the preloaded data snapshot from the given synchronous storage.
59
+ * If `keys` are provided, the memberships for those keys is returned, to protect segments data.
60
+ * Otherwise, the segments data is returned.
61
+ */
62
+ export function getCache(log: ILogger, storage: IStorageSync, keys?: SplitIO.SplitKey[]): SplitIO.PreloadedData {
63
+
64
+ log.debug(`get cache${keys ? ` for keys ${keys}` : ''}`);
65
+
66
+ return {
67
+ since: storage.splits.getChangeNumber(),
68
+ flags: storage.splits.getAll(),
69
+ rbSince: storage.rbSegments.getChangeNumber(),
70
+ rbSegments: storage.rbSegments.getAll(),
71
+ segments: keys ?
72
+ undefined : // @ts-ignore accessing private prop
73
+ Object.keys(storage.segments.segmentCache).reduce((prev, cur) => { // @ts-ignore accessing private prop
74
+ prev[cur] = setToArray(storage.segments.segmentCache[cur] as Set<string>);
75
+ return prev;
76
+ }, {}),
77
+ memberships: keys ?
78
+ keys.reduce<Record<string, IMembershipsResponse>>((prev, key) => {
79
+ if (storage.shared) {
80
+ // Client-side segments
81
+ // @ts-ignore accessing private prop
82
+ const sharedStorage = storage.shared(key);
83
+ prev[getMatching(key)] = {
84
+ ms: {
85
+ // @ts-ignore accessing private prop
86
+ k: Object.keys(sharedStorage.segments.segmentCache).map(segmentName => ({ n: segmentName })),
87
+ },
88
+ ls: sharedStorage.largeSegments ? {
89
+ // @ts-ignore accessing private prop
90
+ k: Object.keys(sharedStorage.largeSegments.segmentCache).map(segmentName => ({ n: segmentName })),
91
+ } : undefined
92
+ };
93
+ } else {
94
+ prev[getMatching(key)] = {
95
+ ms: {
96
+ // Server-side segments
97
+ // @ts-ignore accessing private prop
98
+ k: Object.keys(storage.segments.segmentCache).reduce<IMySegmentsResponse['k']>((prev, segmentName) => { // @ts-ignore accessing private prop
99
+ return storage.segments.segmentCache[segmentName].has(key) ?
100
+ prev!.concat({ n: segmentName }) :
101
+ prev;
102
+ }, [])
103
+ },
104
+ ls: {
105
+ k: []
106
+ }
107
+ };
108
+ }
109
+ return prev;
110
+ }, {}) :
111
+ undefined
54
112
  };
55
113
  }