@splitsoftware/splitio-commons 2.4.2-rc.2 → 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 (30) hide show
  1. package/cjs/storages/AbstractMySegmentsCacheSync.js +23 -31
  2. package/cjs/storages/AbstractSplitsCacheSync.js +2 -3
  3. package/cjs/storages/inLocalStorage/MySegmentsCacheInLocal.js +1 -1
  4. package/cjs/storages/inLocalStorage/RBSegmentsCacheInLocal.js +2 -3
  5. package/cjs/storages/inLocalStorage/index.js +4 -1
  6. package/cjs/storages/inLocalStorage/storageAdapter.js +23 -17
  7. package/cjs/storages/inLocalStorage/validateCache.js +3 -0
  8. package/cjs/sync/polling/updaters/mySegmentsUpdater.js +2 -0
  9. package/cjs/sync/polling/updaters/splitChangesUpdater.js +2 -0
  10. package/esm/storages/AbstractMySegmentsCacheSync.js +23 -31
  11. package/esm/storages/AbstractSplitsCacheSync.js +2 -3
  12. package/esm/storages/inLocalStorage/MySegmentsCacheInLocal.js +1 -1
  13. package/esm/storages/inLocalStorage/RBSegmentsCacheInLocal.js +2 -3
  14. package/esm/storages/inLocalStorage/index.js +4 -1
  15. package/esm/storages/inLocalStorage/storageAdapter.js +23 -17
  16. package/esm/storages/inLocalStorage/validateCache.js +3 -0
  17. package/esm/sync/polling/updaters/mySegmentsUpdater.js +2 -0
  18. package/esm/sync/polling/updaters/splitChangesUpdater.js +2 -0
  19. package/package.json +1 -1
  20. package/src/storages/AbstractMySegmentsCacheSync.ts +20 -26
  21. package/src/storages/AbstractSplitsCacheSync.ts +2 -3
  22. package/src/storages/inLocalStorage/MySegmentsCacheInLocal.ts +1 -1
  23. package/src/storages/inLocalStorage/RBSegmentsCacheInLocal.ts +2 -3
  24. package/src/storages/inLocalStorage/index.ts +6 -2
  25. package/src/storages/inLocalStorage/storageAdapter.ts +28 -16
  26. package/src/storages/inLocalStorage/validateCache.ts +3 -0
  27. package/src/storages/types.ts +3 -1
  28. package/src/sync/polling/updaters/mySegmentsUpdater.ts +2 -0
  29. package/src/sync/polling/updaters/splitChangesUpdater.ts +3 -1
  30. package/types/splitio.d.ts +24 -9
@@ -23,45 +23,37 @@ var AbstractMySegmentsCacheSync = /** @class */ (function () {
23
23
  */
24
24
  AbstractMySegmentsCacheSync.prototype.resetSegments = function (segmentsData) {
25
25
  var _this = this;
26
+ this.setChangeNumber(segmentsData.cn);
26
27
  var _a = segmentsData, added = _a.added, removed = _a.removed;
27
- var isDiff = false;
28
28
  if (added && removed) {
29
+ var isDiff_1 = false;
29
30
  added.forEach(function (segment) {
30
- isDiff = _this.addSegment(segment) || isDiff;
31
+ isDiff_1 = _this.addSegment(segment) || isDiff_1;
31
32
  });
32
33
  removed.forEach(function (segment) {
33
- isDiff = _this.removeSegment(segment) || isDiff;
34
+ isDiff_1 = _this.removeSegment(segment) || isDiff_1;
34
35
  });
36
+ return isDiff_1;
35
37
  }
36
- else {
37
- var names = (segmentsData.k || []).map(function (s) { return s.n; }).sort();
38
- var storedSegmentKeys = this.getRegisteredSegments().sort();
39
- // Extreme fast => everything is empty
40
- if (!names.length && !storedSegmentKeys.length) {
41
- isDiff = false;
42
- }
43
- else {
44
- var index = 0;
45
- while (index < names.length && index < storedSegmentKeys.length && names[index] === storedSegmentKeys[index])
46
- index++;
47
- // Quick path => no changes
48
- if (index === names.length && index === storedSegmentKeys.length) {
49
- isDiff = false;
50
- }
51
- else {
52
- // Slowest path => add and/or remove segments
53
- for (var removeIndex = index; removeIndex < storedSegmentKeys.length; removeIndex++) {
54
- this.removeSegment(storedSegmentKeys[removeIndex]);
55
- }
56
- for (var addIndex = index; addIndex < names.length; addIndex++) {
57
- this.addSegment(names[addIndex]);
58
- }
59
- isDiff = true;
60
- }
61
- }
38
+ var names = (segmentsData.k || []).map(function (s) { return s.n; }).sort();
39
+ var storedSegmentKeys = this.getRegisteredSegments().sort();
40
+ // Extreme fast => everything is empty
41
+ if (!names.length && !storedSegmentKeys.length)
42
+ return false;
43
+ var index = 0;
44
+ while (index < names.length && index < storedSegmentKeys.length && names[index] === storedSegmentKeys[index])
45
+ index++;
46
+ // Quick path => no changes
47
+ if (index === names.length && index === storedSegmentKeys.length)
48
+ return false;
49
+ // Slowest path => add and/or remove segments
50
+ for (var removeIndex = index; removeIndex < storedSegmentKeys.length; removeIndex++) {
51
+ this.removeSegment(storedSegmentKeys[removeIndex]);
62
52
  }
63
- this.setChangeNumber(segmentsData.cn);
64
- return isDiff;
53
+ for (var addIndex = index; addIndex < names.length; addIndex++) {
54
+ this.addSegment(names[addIndex]);
55
+ }
56
+ return true;
65
57
  };
66
58
  return AbstractMySegmentsCacheSync;
67
59
  }());
@@ -12,10 +12,9 @@ var AbstractSplitsCacheSync = /** @class */ (function () {
12
12
  }
13
13
  AbstractSplitsCacheSync.prototype.update = function (toAdd, toRemove, changeNumber) {
14
14
  var _this = this;
15
- var updated = toAdd.map(function (addedFF) { return _this.addSplit(addedFF); }).some(function (result) { return result; });
16
- updated = toRemove.map(function (removedFF) { return _this.removeSplit(removedFF.name); }).some(function (result) { return result; }) || updated;
17
15
  this.setChangeNumber(changeNumber);
18
- return updated;
16
+ var updated = toAdd.map(function (addedFF) { return _this.addSplit(addedFF); }).some(function (result) { return result; });
17
+ return toRemove.map(function (removedFF) { return _this.removeSplit(removedFF.name); }).some(function (result) { return result; }) || updated;
19
18
  };
20
19
  AbstractSplitsCacheSync.prototype.getSplits = function (names) {
21
20
  var _this = this;
@@ -46,7 +46,7 @@ var MySegmentsCacheInLocal = /** @class */ (function (_super) {
46
46
  };
47
47
  MySegmentsCacheInLocal.prototype.getRegisteredSegments = function () {
48
48
  var registeredSegments = [];
49
- for (var i = 0; i < this.storage.length; i++) {
49
+ for (var i = 0, len = this.storage.length; i < len; i++) {
50
50
  var segmentName = this.keys.extractSegmentName(this.storage.key(i));
51
51
  if (segmentName)
52
52
  registeredSegments.push(segmentName);
@@ -18,10 +18,9 @@ var RBSegmentsCacheInLocal = /** @class */ (function () {
18
18
  };
19
19
  RBSegmentsCacheInLocal.prototype.update = function (toAdd, toRemove, changeNumber) {
20
20
  var _this = this;
21
- var updated = toAdd.map(function (toAdd) { return _this.add(toAdd); }).some(function (result) { return result; });
22
- updated = toRemove.map(function (toRemove) { return _this.remove(toRemove.name); }).some(function (result) { return result; }) || updated;
23
21
  this.setChangeNumber(changeNumber);
24
- return updated;
22
+ var updated = toAdd.map(function (toAdd) { return _this.add(toAdd); }).some(function (result) { return result; });
23
+ return toRemove.map(function (toRemove) { return _this.remove(toRemove.name); }).some(function (result) { return result; }) || updated;
25
24
  };
26
25
  RBSegmentsCacheInLocal.prototype.setChangeNumber = function (changeNumber) {
27
26
  try {
@@ -62,9 +62,12 @@ function InLocalStorage(options) {
62
62
  validateCache: function () {
63
63
  return validateCachePromise || (validateCachePromise = (0, validateCache_1.validateCache)(options, storage, settings, keys, splits, rbSegments, segments, largeSegments));
64
64
  },
65
- destroy: function () {
65
+ save: function () {
66
66
  return storage.save && storage.save();
67
67
  },
68
+ destroy: function () {
69
+ return storage.whenSaved && storage.whenSaved();
70
+ },
68
71
  // When using shared instantiation with MEMORY we reuse everything but segments (they are customer per key).
69
72
  shared: function (matchingKey) {
70
73
  return {
@@ -2,46 +2,52 @@
2
2
  Object.defineProperty(exports, "__esModule", { value: true });
3
3
  exports.storageAdapter = void 0;
4
4
  var constants_1 = require("./constants");
5
- function isTillKey(key) {
6
- return key.endsWith('.till');
7
- }
8
5
  function storageAdapter(log, prefix, wrapper) {
6
+ var keys = [];
9
7
  var cache = {};
10
- var connectPromise;
11
- var disconnectPromise = Promise.resolve();
8
+ var loadPromise;
9
+ var savePromise = Promise.resolve();
12
10
  return {
13
11
  load: function () {
14
- return connectPromise || (connectPromise = Promise.resolve(wrapper.getItem(prefix)).then(function (storedCache) {
12
+ return loadPromise || (loadPromise = Promise.resolve().then(function () {
13
+ return wrapper.getItem(prefix);
14
+ }).then(function (storedCache) {
15
15
  cache = JSON.parse(storedCache || '{}');
16
+ keys = Object.keys(cache);
16
17
  }).catch(function (e) {
17
- log.error(constants_1.LOG_PREFIX + 'Rejected promise calling storage getItem, with error: ' + e);
18
+ log.error(constants_1.LOG_PREFIX + 'Rejected promise calling wrapper `getItem` method, with error: ' + e);
18
19
  }));
19
20
  },
20
21
  save: function () {
21
- return disconnectPromise = disconnectPromise.then(function () {
22
- return Promise.resolve(wrapper.setItem(prefix, JSON.stringify(cache))).catch(function (e) {
23
- log.error(constants_1.LOG_PREFIX + 'Rejected promise calling storage setItem, with error: ' + e);
24
- });
22
+ return savePromise = savePromise.then(function () {
23
+ return Promise.resolve(wrapper.setItem(prefix, JSON.stringify(cache)));
24
+ }).catch(function (e) {
25
+ log.error(constants_1.LOG_PREFIX + 'Rejected promise calling wrapper `setItem` method, with error: ' + e);
25
26
  });
26
27
  },
28
+ whenSaved: function () {
29
+ return savePromise;
30
+ },
27
31
  get length() {
28
- return Object.keys(cache).length;
32
+ return keys.length;
29
33
  },
30
34
  getItem: function (key) {
31
35
  return cache[key] || null;
32
36
  },
33
37
  key: function (index) {
34
- return Object.keys(cache)[index] || null;
38
+ return keys[index] || null;
35
39
  },
36
40
  removeItem: function (key) {
41
+ var index = keys.indexOf(key);
42
+ if (index === -1)
43
+ return;
44
+ keys.splice(index, 1);
37
45
  delete cache[key];
38
- if (isTillKey(key))
39
- this.save();
40
46
  },
41
47
  setItem: function (key, value) {
48
+ if (keys.indexOf(key) === -1)
49
+ keys.push(key);
42
50
  cache[key] = value;
43
- if (isTillKey(key))
44
- this.save();
45
51
  }
46
52
  };
47
53
  }
@@ -73,6 +73,9 @@ function validateCache(options, storage, settings, keys, splits, rbSegments, seg
73
73
  catch (e) {
74
74
  settings.log.error(constants_1.LOG_PREFIX + e);
75
75
  }
76
+ // Persist clear
77
+ if (storage.save)
78
+ storage.save();
76
79
  return false;
77
80
  }
78
81
  // Check if ready from cache
@@ -34,6 +34,8 @@ function mySegmentsUpdaterFactory(log, mySegmentsFetcher, storage, segmentsEvent
34
34
  shouldNotifyUpdate = segments.resetSegments(segmentsData.ms || {});
35
35
  shouldNotifyUpdate = largeSegments.resetSegments(segmentsData.ls || {}) || shouldNotifyUpdate;
36
36
  }
37
+ if (storage.save)
38
+ storage.save();
37
39
  // Notify update if required
38
40
  if ((0, AbstractSplitsCacheSync_1.usesSegmentsSync)(storage) && (shouldNotifyUpdate || readyOnAlreadyExistentState)) {
39
41
  readyOnAlreadyExistentState = false;
@@ -154,6 +154,8 @@ function splitChangesUpdaterFactory(log, splitChangesFetcher, storage, splitFilt
154
154
  segments.registerSegments((0, sets_1.setToArray)(usedSegments))
155
155
  ]).then(function (_a) {
156
156
  var ffChanged = _a[0], rbsChanged = _a[1];
157
+ if (storage.save)
158
+ storage.save();
157
159
  if (splitsEventEmitter) {
158
160
  // To emit SDK_SPLITS_ARRIVED for server-side SDK, we must check that all registered segments have been fetched
159
161
  return Promise.resolve(!splitsEventEmitter.splitsArrived || ((ffChanged || rbsChanged) && (isClientSide || checkAllSegmentsExist(segments))))
@@ -20,45 +20,37 @@ var AbstractMySegmentsCacheSync = /** @class */ (function () {
20
20
  */
21
21
  AbstractMySegmentsCacheSync.prototype.resetSegments = function (segmentsData) {
22
22
  var _this = this;
23
+ this.setChangeNumber(segmentsData.cn);
23
24
  var _a = segmentsData, added = _a.added, removed = _a.removed;
24
- var isDiff = false;
25
25
  if (added && removed) {
26
+ var isDiff_1 = false;
26
27
  added.forEach(function (segment) {
27
- isDiff = _this.addSegment(segment) || isDiff;
28
+ isDiff_1 = _this.addSegment(segment) || isDiff_1;
28
29
  });
29
30
  removed.forEach(function (segment) {
30
- isDiff = _this.removeSegment(segment) || isDiff;
31
+ isDiff_1 = _this.removeSegment(segment) || isDiff_1;
31
32
  });
33
+ return isDiff_1;
32
34
  }
33
- else {
34
- var names = (segmentsData.k || []).map(function (s) { return s.n; }).sort();
35
- var storedSegmentKeys = this.getRegisteredSegments().sort();
36
- // Extreme fast => everything is empty
37
- if (!names.length && !storedSegmentKeys.length) {
38
- isDiff = false;
39
- }
40
- else {
41
- var index = 0;
42
- while (index < names.length && index < storedSegmentKeys.length && names[index] === storedSegmentKeys[index])
43
- index++;
44
- // Quick path => no changes
45
- if (index === names.length && index === storedSegmentKeys.length) {
46
- isDiff = false;
47
- }
48
- else {
49
- // Slowest path => add and/or remove segments
50
- for (var removeIndex = index; removeIndex < storedSegmentKeys.length; removeIndex++) {
51
- this.removeSegment(storedSegmentKeys[removeIndex]);
52
- }
53
- for (var addIndex = index; addIndex < names.length; addIndex++) {
54
- this.addSegment(names[addIndex]);
55
- }
56
- isDiff = true;
57
- }
58
- }
35
+ var names = (segmentsData.k || []).map(function (s) { return s.n; }).sort();
36
+ var storedSegmentKeys = this.getRegisteredSegments().sort();
37
+ // Extreme fast => everything is empty
38
+ if (!names.length && !storedSegmentKeys.length)
39
+ return false;
40
+ var index = 0;
41
+ while (index < names.length && index < storedSegmentKeys.length && names[index] === storedSegmentKeys[index])
42
+ index++;
43
+ // Quick path => no changes
44
+ if (index === names.length && index === storedSegmentKeys.length)
45
+ return false;
46
+ // Slowest path => add and/or remove segments
47
+ for (var removeIndex = index; removeIndex < storedSegmentKeys.length; removeIndex++) {
48
+ this.removeSegment(storedSegmentKeys[removeIndex]);
59
49
  }
60
- this.setChangeNumber(segmentsData.cn);
61
- return isDiff;
50
+ for (var addIndex = index; addIndex < names.length; addIndex++) {
51
+ this.addSegment(names[addIndex]);
52
+ }
53
+ return true;
62
54
  };
63
55
  return AbstractMySegmentsCacheSync;
64
56
  }());
@@ -9,10 +9,9 @@ var AbstractSplitsCacheSync = /** @class */ (function () {
9
9
  }
10
10
  AbstractSplitsCacheSync.prototype.update = function (toAdd, toRemove, changeNumber) {
11
11
  var _this = this;
12
- var updated = toAdd.map(function (addedFF) { return _this.addSplit(addedFF); }).some(function (result) { return result; });
13
- updated = toRemove.map(function (removedFF) { return _this.removeSplit(removedFF.name); }).some(function (result) { return result; }) || updated;
14
12
  this.setChangeNumber(changeNumber);
15
- return updated;
13
+ var updated = toAdd.map(function (addedFF) { return _this.addSplit(addedFF); }).some(function (result) { return result; });
14
+ return toRemove.map(function (removedFF) { return _this.removeSplit(removedFF.name); }).some(function (result) { return result; }) || updated;
16
15
  };
17
16
  AbstractSplitsCacheSync.prototype.getSplits = function (names) {
18
17
  var _this = this;
@@ -43,7 +43,7 @@ var MySegmentsCacheInLocal = /** @class */ (function (_super) {
43
43
  };
44
44
  MySegmentsCacheInLocal.prototype.getRegisteredSegments = function () {
45
45
  var registeredSegments = [];
46
- for (var i = 0; i < this.storage.length; i++) {
46
+ for (var i = 0, len = this.storage.length; i < len; i++) {
47
47
  var segmentName = this.keys.extractSegmentName(this.storage.key(i));
48
48
  if (segmentName)
49
49
  registeredSegments.push(segmentName);
@@ -15,10 +15,9 @@ var RBSegmentsCacheInLocal = /** @class */ (function () {
15
15
  };
16
16
  RBSegmentsCacheInLocal.prototype.update = function (toAdd, toRemove, changeNumber) {
17
17
  var _this = this;
18
- var updated = toAdd.map(function (toAdd) { return _this.add(toAdd); }).some(function (result) { return result; });
19
- updated = toRemove.map(function (toRemove) { return _this.remove(toRemove.name); }).some(function (result) { return result; }) || updated;
20
18
  this.setChangeNumber(changeNumber);
21
- return updated;
19
+ var updated = toAdd.map(function (toAdd) { return _this.add(toAdd); }).some(function (result) { return result; });
20
+ return toRemove.map(function (toRemove) { return _this.remove(toRemove.name); }).some(function (result) { return result; }) || updated;
22
21
  };
23
22
  RBSegmentsCacheInLocal.prototype.setChangeNumber = function (changeNumber) {
24
23
  try {
@@ -59,9 +59,12 @@ export function InLocalStorage(options) {
59
59
  validateCache: function () {
60
60
  return validateCachePromise || (validateCachePromise = validateCache(options, storage, settings, keys, splits, rbSegments, segments, largeSegments));
61
61
  },
62
- destroy: function () {
62
+ save: function () {
63
63
  return storage.save && storage.save();
64
64
  },
65
+ destroy: function () {
66
+ return storage.whenSaved && storage.whenSaved();
67
+ },
65
68
  // When using shared instantiation with MEMORY we reuse everything but segments (they are customer per key).
66
69
  shared: function (matchingKey) {
67
70
  return {
@@ -1,44 +1,50 @@
1
1
  import { LOG_PREFIX } from './constants';
2
- function isTillKey(key) {
3
- return key.endsWith('.till');
4
- }
5
2
  export function storageAdapter(log, prefix, wrapper) {
3
+ var keys = [];
6
4
  var cache = {};
7
- var connectPromise;
8
- var disconnectPromise = Promise.resolve();
5
+ var loadPromise;
6
+ var savePromise = Promise.resolve();
9
7
  return {
10
8
  load: function () {
11
- return connectPromise || (connectPromise = Promise.resolve(wrapper.getItem(prefix)).then(function (storedCache) {
9
+ return loadPromise || (loadPromise = Promise.resolve().then(function () {
10
+ return wrapper.getItem(prefix);
11
+ }).then(function (storedCache) {
12
12
  cache = JSON.parse(storedCache || '{}');
13
+ keys = Object.keys(cache);
13
14
  }).catch(function (e) {
14
- log.error(LOG_PREFIX + 'Rejected promise calling storage getItem, with error: ' + e);
15
+ log.error(LOG_PREFIX + 'Rejected promise calling wrapper `getItem` method, with error: ' + e);
15
16
  }));
16
17
  },
17
18
  save: function () {
18
- return disconnectPromise = disconnectPromise.then(function () {
19
- return Promise.resolve(wrapper.setItem(prefix, JSON.stringify(cache))).catch(function (e) {
20
- log.error(LOG_PREFIX + 'Rejected promise calling storage setItem, with error: ' + e);
21
- });
19
+ return savePromise = savePromise.then(function () {
20
+ return Promise.resolve(wrapper.setItem(prefix, JSON.stringify(cache)));
21
+ }).catch(function (e) {
22
+ log.error(LOG_PREFIX + 'Rejected promise calling wrapper `setItem` method, with error: ' + e);
22
23
  });
23
24
  },
25
+ whenSaved: function () {
26
+ return savePromise;
27
+ },
24
28
  get length() {
25
- return Object.keys(cache).length;
29
+ return keys.length;
26
30
  },
27
31
  getItem: function (key) {
28
32
  return cache[key] || null;
29
33
  },
30
34
  key: function (index) {
31
- return Object.keys(cache)[index] || null;
35
+ return keys[index] || null;
32
36
  },
33
37
  removeItem: function (key) {
38
+ var index = keys.indexOf(key);
39
+ if (index === -1)
40
+ return;
41
+ keys.splice(index, 1);
34
42
  delete cache[key];
35
- if (isTillKey(key))
36
- this.save();
37
43
  },
38
44
  setItem: function (key, value) {
45
+ if (keys.indexOf(key) === -1)
46
+ keys.push(key);
39
47
  cache[key] = value;
40
- if (isTillKey(key))
41
- this.save();
42
48
  }
43
49
  };
44
50
  }
@@ -70,6 +70,9 @@ export function validateCache(options, storage, settings, keys, splits, rbSegmen
70
70
  catch (e) {
71
71
  settings.log.error(LOG_PREFIX + e);
72
72
  }
73
+ // Persist clear
74
+ if (storage.save)
75
+ storage.save();
73
76
  return false;
74
77
  }
75
78
  // Check if ready from cache
@@ -31,6 +31,8 @@ export function mySegmentsUpdaterFactory(log, mySegmentsFetcher, storage, segmen
31
31
  shouldNotifyUpdate = segments.resetSegments(segmentsData.ms || {});
32
32
  shouldNotifyUpdate = largeSegments.resetSegments(segmentsData.ls || {}) || shouldNotifyUpdate;
33
33
  }
34
+ if (storage.save)
35
+ storage.save();
34
36
  // Notify update if required
35
37
  if (usesSegmentsSync(storage) && (shouldNotifyUpdate || readyOnAlreadyExistentState)) {
36
38
  readyOnAlreadyExistentState = false;
@@ -149,6 +149,8 @@ export function splitChangesUpdaterFactory(log, splitChangesFetcher, storage, sp
149
149
  segments.registerSegments(setToArray(usedSegments))
150
150
  ]).then(function (_a) {
151
151
  var ffChanged = _a[0], rbsChanged = _a[1];
152
+ if (storage.save)
153
+ storage.save();
152
154
  if (splitsEventEmitter) {
153
155
  // To emit SDK_SPLITS_ARRIVED for server-side SDK, we must check that all registered segments have been fetched
154
156
  return Promise.resolve(!splitsEventEmitter.splitsArrived || ((ffChanged || rbsChanged) && (isClientSide || checkAllSegmentsExist(segments))))
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.4.2-rc.3",
4
4
  "description": "Split JavaScript SDK common components",
5
5
  "main": "cjs/index.js",
6
6
  "module": "esm/index.js",
@@ -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
@@ -51,7 +51,7 @@ export class MySegmentsCacheInLocal extends AbstractMySegmentsCacheSync {
51
51
 
52
52
  getRegisteredSegments(): string[] {
53
53
  const registeredSegments: string[] = [];
54
- for (let i = 0; i < this.storage.length; i++) {
54
+ for (let i = 0, len = this.storage.length; i < len; i++) {
55
55
  const segmentName = this.keys.extractSegmentName(this.storage.key(i)!);
56
56
  if (segmentName) registeredSegments.push(segmentName);
57
57
  }
@@ -26,10 +26,9 @@ export class RBSegmentsCacheInLocal implements IRBSegmentsCacheSync {
26
26
  }
27
27
 
28
28
  update(toAdd: IRBSegment[], toRemove: IRBSegment[], changeNumber: number): boolean {
29
- let updated = toAdd.map(toAdd => this.add(toAdd)).some(result => result);
30
- updated = toRemove.map(toRemove => this.remove(toRemove.name)).some(result => result) || updated;
31
29
  this.setChangeNumber(changeNumber);
32
- return updated;
30
+ const updated = toAdd.map(toAdd => this.add(toAdd)).some(result => result);
31
+ return toRemove.map(toRemove => this.remove(toRemove.name)).some(result => result) || updated;
33
32
  }
34
33
 
35
34
  private setChangeNumber(changeNumber: number) {
@@ -19,7 +19,7 @@ import { ILogger } from '../../logger/types';
19
19
  import SplitIO from '../../../types/splitio';
20
20
  import { storageAdapter } from './storageAdapter';
21
21
 
22
- function validateStorage(log: ILogger, prefix: string, wrapper?: SplitIO.StorageWrapper): StorageAdapter | undefined {
22
+ function validateStorage(log: ILogger, prefix: string, wrapper?: SplitIO.SyncStorageWrapper | SplitIO.AsyncStorageWrapper): StorageAdapter | undefined {
23
23
  if (wrapper) {
24
24
  if (isValidStorageWrapper(wrapper)) {
25
25
  return isWebStorage(wrapper) ?
@@ -71,10 +71,14 @@ export function InLocalStorage(options: SplitIO.InLocalStorageOptions = {}): ISt
71
71
  return validateCachePromise || (validateCachePromise = validateCache(options, storage, settings, keys, splits, rbSegments, segments, largeSegments));
72
72
  },
73
73
 
74
- destroy() {
74
+ save() {
75
75
  return storage.save && storage.save();
76
76
  },
77
77
 
78
+ destroy() {
79
+ return storage.whenSaved && storage.whenSaved();
80
+ },
81
+
78
82
  // When using shared instantiation with MEMORY we reuse everything but segments (they are customer per key).
79
83
  shared(matchingKey: string) {
80
84
 
@@ -3,48 +3,60 @@ import SplitIO from '../../../types/splitio';
3
3
  import { LOG_PREFIX } from './constants';
4
4
  import { StorageAdapter } from '../types';
5
5
 
6
- function isTillKey(key: string) {
7
- return key.endsWith('.till');
8
- }
9
6
 
10
- export function storageAdapter(log: ILogger, prefix: string, wrapper: SplitIO.StorageWrapper): StorageAdapter {
7
+ export function storageAdapter(log: ILogger, prefix: string, wrapper: SplitIO.SyncStorageWrapper | SplitIO.AsyncStorageWrapper): Required<StorageAdapter> {
8
+ let keys: string[] = [];
11
9
  let cache: Record<string, string> = {};
12
10
 
13
- let connectPromise: Promise<void> | undefined;
14
- let disconnectPromise = Promise.resolve();
11
+ let loadPromise: Promise<void> | undefined;
12
+ let savePromise = Promise.resolve();
15
13
 
16
14
  return {
17
15
  load() {
18
- return connectPromise || (connectPromise = Promise.resolve(wrapper.getItem(prefix)).then((storedCache) => {
16
+ return loadPromise || (loadPromise = Promise.resolve().then(() => {
17
+ return wrapper.getItem(prefix);
18
+ }).then((storedCache) => {
19
19
  cache = JSON.parse(storedCache || '{}');
20
+ keys = Object.keys(cache);
20
21
  }).catch((e) => {
21
- log.error(LOG_PREFIX + 'Rejected promise calling storage getItem, with error: ' + e);
22
+ log.error(LOG_PREFIX + 'Rejected promise calling wrapper `getItem` method, with error: ' + e);
22
23
  }));
23
24
  },
25
+
24
26
  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
- });
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);
29
31
  });
30
32
  },
31
33
 
34
+ whenSaved() {
35
+ return savePromise;
36
+ },
37
+
32
38
  get length() {
33
- return Object.keys(cache).length;
39
+ return keys.length;
34
40
  },
41
+
35
42
  getItem(key: string) {
36
43
  return cache[key] || null;
37
44
  },
45
+
38
46
  key(index: number) {
39
- return Object.keys(cache)[index] || null;
47
+ return keys[index] || null;
40
48
  },
49
+
41
50
  removeItem(key: string) {
51
+ const index = keys.indexOf(key);
52
+ if (index === -1) return;
53
+ keys.splice(index, 1);
42
54
  delete cache[key];
43
- if (isTillKey(key)) this.save!();
44
55
  },
56
+
45
57
  setItem(key: string, value: string) {
58
+ if (keys.indexOf(key) === -1) keys.push(key);
46
59
  cache[key] = value;
47
- if (isTillKey(key)) this.save!();
48
60
  }
49
61
  };
50
62
  }
@@ -87,6 +87,9 @@ export function validateCache(options: SplitIO.InLocalStorageOptions, storage: S
87
87
  settings.log.error(LOG_PREFIX + e);
88
88
  }
89
89
 
90
+ // Persist clear
91
+ if (storage.save) storage.save();
92
+
90
93
  return false;
91
94
  }
92
95
 
@@ -12,10 +12,11 @@ export interface StorageAdapter {
12
12
  // Methods to support async storages
13
13
  load?: () => Promise<void>;
14
14
  save?: () => Promise<void>;
15
+ whenSaved?: () => Promise<void>;
15
16
  // Methods based on https://developer.mozilla.org/en-US/docs/Web/API/Storage
16
17
  readonly length: number;
17
- getItem(key: string): string | null;
18
18
  key(index: number): string | null;
19
+ getItem(key: string): string | null;
19
20
  removeItem(key: string): void;
20
21
  setItem(key: string, value: string): void;
21
22
  }
@@ -482,6 +483,7 @@ export interface IStorageBase<
482
483
  uniqueKeys: TUniqueKeysCache,
483
484
  destroy(): void | Promise<void>,
484
485
  shared?: (matchingKey: string, onReadyCb: (error?: any) => void) => this
486
+ save?: () => void | Promise<void>,
485
487
  }
486
488
 
487
489
  export interface IStorageSync extends IStorageBase<
@@ -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))))
@@ -458,19 +458,34 @@ interface IClientSideSyncSharedSettings extends IClientSideSharedSettings, ISync
458
458
  */
459
459
  declare namespace SplitIO {
460
460
 
461
- interface StorageWrapper {
461
+ interface SyncStorageWrapper {
462
462
  /**
463
- * Returns a promise that resolves to the current value associated with the given key, or null if the given key does not exist.
463
+ * Returns the value associated with the given key, or null if the key does not exist.
464
464
  */
465
- getItem(key: string): Promise<string | null> | string | null;
465
+ getItem(key: string): string | null;
466
466
  /**
467
- * 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 none existed for key previously.
467
+ * Sets the value for the given key, creating a new key/value pair if key does not exist.
468
468
  */
469
- setItem(key: string, value: string): Promise<void> | void;
469
+ setItem(key: string, value: string): void;
470
470
  /**
471
- * Returns a promise that resolves when the key/value pair with the given key is removed, if a key/value pair with the given key exists.
471
+ * Removes the key/value pair for the given key, if the key exists.
472
472
  */
473
- removeItem(key: string): Promise<void> | void;
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>;
474
489
  }
475
490
 
476
491
  /**
@@ -992,7 +1007,7 @@ declare namespace SplitIO {
992
1007
  *
993
1008
  * @defaultValue `window.localStorage`
994
1009
  */
995
- wrapper?: StorageWrapper;
1010
+ wrapper?: SyncStorageWrapper | AsyncStorageWrapper;
996
1011
  }
997
1012
  /**
998
1013
  * Storage for asynchronous (consumer) SDK.
@@ -1338,7 +1353,7 @@ declare namespace SplitIO {
1338
1353
  *
1339
1354
  * @defaultValue `window.localStorage`
1340
1355
  */
1341
- wrapper?: StorageWrapper;
1356
+ wrapper?: SyncStorageWrapper | AsyncStorageWrapper;
1342
1357
  };
1343
1358
  }
1344
1359
  /**