@splitsoftware/splitio-commons 1.7.1 → 1.7.3-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 (32) hide show
  1. package/CHANGES.txt +8 -0
  2. package/cjs/listeners/browser.js +9 -3
  3. package/cjs/storages/inMemory/ImpressionCountsCacheInMemory.js +1 -1
  4. package/cjs/storages/inMemory/InMemoryStorage.js +14 -3
  5. package/cjs/storages/inMemory/InMemoryStorageCS.js +14 -3
  6. package/cjs/storages/inMemory/UniqueKeysCacheInMemory.js +3 -3
  7. package/cjs/storages/inMemory/UniqueKeysCacheInMemoryCS.js +3 -3
  8. package/cjs/storages/inRedis/UniqueKeysCacheInRedis.js +6 -8
  9. package/cjs/storages/pluggable/UniqueKeysCachePluggable.js +5 -4
  10. package/cjs/utils/settingsValidation/index.js +1 -1
  11. package/esm/listeners/browser.js +9 -3
  12. package/esm/storages/inMemory/ImpressionCountsCacheInMemory.js +1 -1
  13. package/esm/storages/inMemory/InMemoryStorage.js +15 -4
  14. package/esm/storages/inMemory/InMemoryStorageCS.js +15 -4
  15. package/esm/storages/inMemory/UniqueKeysCacheInMemory.js +3 -3
  16. package/esm/storages/inMemory/UniqueKeysCacheInMemoryCS.js +3 -3
  17. package/esm/storages/inRedis/UniqueKeysCacheInRedis.js +6 -8
  18. package/esm/storages/pluggable/UniqueKeysCachePluggable.js +5 -4
  19. package/esm/utils/settingsValidation/index.js +1 -1
  20. package/package.json +1 -1
  21. package/src/listeners/browser.ts +8 -3
  22. package/src/storages/inMemory/ImpressionCountsCacheInMemory.ts +1 -1
  23. package/src/storages/inMemory/InMemoryStorage.ts +15 -3
  24. package/src/storages/inMemory/InMemoryStorageCS.ts +15 -3
  25. package/src/storages/inMemory/UniqueKeysCacheInMemory.ts +3 -4
  26. package/src/storages/inMemory/UniqueKeysCacheInMemoryCS.ts +3 -4
  27. package/src/storages/inRedis/UniqueKeysCacheInRedis.ts +5 -7
  28. package/src/storages/pluggable/UniqueKeysCachePluggable.ts +8 -8
  29. package/src/utils/settingsValidation/index.ts +1 -1
  30. package/types/listeners/browser.d.ts +1 -0
  31. package/types/storages/pluggable/UniqueKeysCachePluggable.d.ts +2 -2
  32. package/src/logger/.DS_Store +0 -0
package/CHANGES.txt CHANGED
@@ -1,3 +1,11 @@
1
+ 1.7.3 (December 16, 2022)
2
+ - Updated events and impressions cache in localhost mode in order to avoid memory leaks (Related to issue https://github.com/splitio/javascript-commons/issues/181).
3
+ - Updated unique keys cache for Redis and Pluggable storages to optimize the usage of the underlying storage.
4
+ - Updated some transitive dependencies for vulnerability fixes.
5
+
6
+ 1.7.2 (October 14, 2022)
7
+ - Bugfixing - Handle `Navigator.sendBeacon` API exceptions in the browser, and fallback to regular Fetch/XHR transport in case of error.
8
+
1
9
  1.7.1 (October 5, 2022)
2
10
  - Updated default value of `scheduler.featuresRefreshRate` config parameter to 60 seconds.
3
11
 
@@ -100,13 +100,14 @@ var BrowserSignalListener = /** @class */ (function () {
100
100
  if (!cache.isEmpty()) {
101
101
  var dataPayload = fromCacheToPayload ? fromCacheToPayload(cache.pop()) : cache.pop();
102
102
  if (!this._sendBeacon(url, dataPayload, extraMetadata)) {
103
- postService(JSON.stringify(dataPayload)).catch(function () { }); // no-op just to catch a possible exception
103
+ postService(JSON.stringify(dataPayload)).catch(function () { }); // no-op to handle possible promise rejection
104
104
  }
105
105
  }
106
106
  };
107
107
  /**
108
108
  * _sendBeacon method.
109
109
  * Util method that check if beacon API is available, build the payload and send it.
110
+ * Returns true if beacon API was used successfully, false otherwise.
110
111
  */
111
112
  BrowserSignalListener.prototype._sendBeacon = function (url, data, extraMetadata) {
112
113
  // eslint-disable-next-line compat/compat
@@ -121,8 +122,13 @@ var BrowserSignalListener = /** @class */ (function () {
121
122
  (0, objectAssign_1.objectAssign)(json, extraMetadata);
122
123
  // Stringify the payload
123
124
  var payload = JSON.stringify(json);
124
- // eslint-disable-next-line compat/compat
125
- return navigator.sendBeacon(url, payload);
125
+ // https://xgwang.me/posts/you-may-not-know-beacon/#it-may-throw-error%2C-be-sure-to-catch
126
+ try { // eslint-disable-next-line compat/compat
127
+ return navigator.sendBeacon(url, payload);
128
+ }
129
+ catch (e) {
130
+ return false;
131
+ }
126
132
  }
127
133
  return false;
128
134
  };
@@ -27,7 +27,6 @@ var ImpressionCountsCacheInMemory = /** @class */ (function () {
27
27
  this.cacheSize = this.cacheSize + amount;
28
28
  if (this.cacheSize >= this.maxStorage) {
29
29
  this.onFullQueue();
30
- this.cacheSize = 0;
31
30
  }
32
31
  }
33
32
  };
@@ -53,6 +52,7 @@ var ImpressionCountsCacheInMemory = /** @class */ (function () {
53
52
  */
54
53
  ImpressionCountsCacheInMemory.prototype.clear = function () {
55
54
  this.cache = {};
55
+ this.cacheSize = 0;
56
56
  };
57
57
  /**
58
58
  * Check if the cache is empty.
@@ -18,7 +18,7 @@ function InMemoryStorageFactory(params) {
18
18
  var _a = params.settings, _b = _a.scheduler, impressionsQueueSize = _b.impressionsQueueSize, eventsQueueSize = _b.eventsQueueSize, impressionsMode = _a.sync.impressionsMode;
19
19
  var splits = new SplitsCacheInMemory_1.SplitsCacheInMemory();
20
20
  var segments = new SegmentsCacheInMemory_1.SegmentsCacheInMemory();
21
- return {
21
+ var storage = {
22
22
  splits: splits,
23
23
  segments: segments,
24
24
  impressions: new ImpressionsCacheInMemory_1.ImpressionsCacheInMemory(impressionsQueueSize),
@@ -28,15 +28,26 @@ function InMemoryStorageFactory(params) {
28
28
  uniqueKeys: impressionsMode === constants_1.NONE ? new UniqueKeysCacheInMemory_1.UniqueKeysCacheInMemory() : undefined,
29
29
  // When using MEMORY we should clean all the caches to leave them empty
30
30
  destroy: function () {
31
- var _a;
32
31
  this.splits.clear();
33
32
  this.segments.clear();
34
33
  this.impressions.clear();
35
34
  this.impressionCounts && this.impressionCounts.clear();
36
35
  this.events.clear();
37
- (_a = this.uniqueKeys) === null || _a === void 0 ? void 0 : _a.clear();
36
+ this.uniqueKeys && this.uniqueKeys.clear();
38
37
  }
39
38
  };
39
+ // @TODO revisit storage logic in localhost mode
40
+ // No tracking data in localhost mode to avoid memory leaks
41
+ if (params.settings.mode === constants_1.LOCALHOST_MODE) {
42
+ var noopTrack = function () { return true; };
43
+ storage.impressions.track = noopTrack;
44
+ storage.events.track = noopTrack;
45
+ if (storage.impressionCounts)
46
+ storage.impressionCounts.track = noopTrack;
47
+ if (storage.uniqueKeys)
48
+ storage.uniqueKeys.track = noopTrack;
49
+ }
50
+ return storage;
40
51
  }
41
52
  exports.InMemoryStorageFactory = InMemoryStorageFactory;
42
53
  InMemoryStorageFactory.type = constants_1.STORAGE_MEMORY;
@@ -18,7 +18,7 @@ function InMemoryStorageCSFactory(params) {
18
18
  var _a = params.settings, _b = _a.scheduler, impressionsQueueSize = _b.impressionsQueueSize, eventsQueueSize = _b.eventsQueueSize, impressionsMode = _a.sync.impressionsMode;
19
19
  var splits = new SplitsCacheInMemory_1.SplitsCacheInMemory();
20
20
  var segments = new MySegmentsCacheInMemory_1.MySegmentsCacheInMemory();
21
- return {
21
+ var storage = {
22
22
  splits: splits,
23
23
  segments: segments,
24
24
  impressions: new ImpressionsCacheInMemory_1.ImpressionsCacheInMemory(impressionsQueueSize),
@@ -28,13 +28,12 @@ function InMemoryStorageCSFactory(params) {
28
28
  uniqueKeys: impressionsMode === constants_1.NONE ? new UniqueKeysCacheInMemoryCS_1.UniqueKeysCacheInMemoryCS() : undefined,
29
29
  // When using MEMORY we should clean all the caches to leave them empty
30
30
  destroy: function () {
31
- var _a;
32
31
  this.splits.clear();
33
32
  this.segments.clear();
34
33
  this.impressions.clear();
35
34
  this.impressionCounts && this.impressionCounts.clear();
36
35
  this.events.clear();
37
- (_a = this.uniqueKeys) === null || _a === void 0 ? void 0 : _a.clear();
36
+ this.uniqueKeys && this.uniqueKeys.clear();
38
37
  },
39
38
  // When using shared instanciation with MEMORY we reuse everything but segments (they are unique per key)
40
39
  shared: function () {
@@ -53,6 +52,18 @@ function InMemoryStorageCSFactory(params) {
53
52
  };
54
53
  },
55
54
  };
55
+ // @TODO revisit storage logic in localhost mode
56
+ // No tracking data in localhost mode to avoid memory leaks
57
+ if (params.settings.mode === constants_1.LOCALHOST_MODE) {
58
+ var noopTrack = function () { return true; };
59
+ storage.impressions.track = noopTrack;
60
+ storage.events.track = noopTrack;
61
+ if (storage.impressionCounts)
62
+ storage.impressionCounts.track = noopTrack;
63
+ if (storage.uniqueKeys)
64
+ storage.uniqueKeys.track = noopTrack;
65
+ }
66
+ return storage;
56
67
  }
57
68
  exports.InMemoryStorageCSFactory = InMemoryStorageCSFactory;
58
69
  InMemoryStorageCSFactory.type = constants_1.STORAGE_MEMORY;
@@ -25,8 +25,8 @@ var UniqueKeysCacheInMemory = /** @class */ (function () {
25
25
  function UniqueKeysCacheInMemory(uniqueKeysQueueSize) {
26
26
  if (uniqueKeysQueueSize === void 0) { uniqueKeysQueueSize = constants_1.DEFAULT_CACHE_SIZE; }
27
27
  this.uniqueTrackerSize = 0;
28
- this.maxStorage = uniqueKeysQueueSize;
29
28
  this.uniqueKeysTracker = {};
29
+ this.maxStorage = uniqueKeysQueueSize;
30
30
  }
31
31
  UniqueKeysCacheInMemory.prototype.setOnFullQueueCb = function (cb) {
32
32
  this.onFullQueue = cb;
@@ -43,7 +43,6 @@ var UniqueKeysCacheInMemory = /** @class */ (function () {
43
43
  this.uniqueTrackerSize++;
44
44
  }
45
45
  if (this.uniqueTrackerSize >= this.maxStorage && this.onFullQueue) {
46
- this.uniqueTrackerSize = 0;
47
46
  this.onFullQueue();
48
47
  }
49
48
  };
@@ -51,6 +50,7 @@ var UniqueKeysCacheInMemory = /** @class */ (function () {
51
50
  * Clear the data stored on the cache.
52
51
  */
53
52
  UniqueKeysCacheInMemory.prototype.clear = function () {
53
+ this.uniqueTrackerSize = 0;
54
54
  this.uniqueKeysTracker = {};
55
55
  };
56
56
  /**
@@ -58,7 +58,7 @@ var UniqueKeysCacheInMemory = /** @class */ (function () {
58
58
  */
59
59
  UniqueKeysCacheInMemory.prototype.pop = function () {
60
60
  var data = this.uniqueKeysTracker;
61
- this.uniqueKeysTracker = {};
61
+ this.clear();
62
62
  return fromUniqueKeysCollector(data);
63
63
  };
64
64
  /**
@@ -12,8 +12,8 @@ var UniqueKeysCacheInMemoryCS = /** @class */ (function () {
12
12
  function UniqueKeysCacheInMemoryCS(uniqueKeysQueueSize) {
13
13
  if (uniqueKeysQueueSize === void 0) { uniqueKeysQueueSize = constants_1.DEFAULT_CACHE_SIZE; }
14
14
  this.uniqueTrackerSize = 0;
15
- this.maxStorage = uniqueKeysQueueSize;
16
15
  this.uniqueKeysTracker = {};
16
+ this.maxStorage = uniqueKeysQueueSize;
17
17
  }
18
18
  UniqueKeysCacheInMemoryCS.prototype.setOnFullQueueCb = function (cb) {
19
19
  this.onFullQueue = cb;
@@ -30,7 +30,6 @@ var UniqueKeysCacheInMemoryCS = /** @class */ (function () {
30
30
  this.uniqueTrackerSize++;
31
31
  }
32
32
  if (this.uniqueTrackerSize >= this.maxStorage && this.onFullQueue) {
33
- this.uniqueTrackerSize = 0;
34
33
  this.onFullQueue();
35
34
  }
36
35
  };
@@ -38,6 +37,7 @@ var UniqueKeysCacheInMemoryCS = /** @class */ (function () {
38
37
  * Clear the data stored on the cache.
39
38
  */
40
39
  UniqueKeysCacheInMemoryCS.prototype.clear = function () {
40
+ this.uniqueTrackerSize = 0;
41
41
  this.uniqueKeysTracker = {};
42
42
  };
43
43
  /**
@@ -45,7 +45,7 @@ var UniqueKeysCacheInMemoryCS = /** @class */ (function () {
45
45
  */
46
46
  UniqueKeysCacheInMemoryCS.prototype.pop = function () {
47
47
  var data = this.uniqueKeysTracker;
48
- this.uniqueKeysTracker = {};
48
+ this.clear();
49
49
  return this.fromUniqueKeysCollector(data);
50
50
  };
51
51
  /**
@@ -24,21 +24,19 @@ var UniqueKeysCacheInRedis = /** @class */ (function (_super) {
24
24
  var featureNames = Object.keys(this.uniqueKeysTracker);
25
25
  if (!featureNames.length)
26
26
  return Promise.resolve(false);
27
- var pipeline = this.redis.pipeline();
28
- for (var i = 0; i < featureNames.length; i++) {
29
- var featureName = featureNames[i];
30
- var featureKeys = (0, sets_1.setToArray)(this.uniqueKeysTracker[featureName]);
27
+ var uniqueKeysArray = featureNames.map(function (featureName) {
28
+ var featureKeys = (0, sets_1.setToArray)(_this.uniqueKeysTracker[featureName]);
31
29
  var uniqueKeysPayload = {
32
30
  f: featureName,
33
31
  ks: featureKeys
34
32
  };
35
- pipeline.rpush(this.key, JSON.stringify(uniqueKeysPayload));
36
- }
33
+ return JSON.stringify(uniqueKeysPayload);
34
+ });
37
35
  this.clear();
38
- return pipeline.exec()
36
+ return this.redis.rpush(this.key, uniqueKeysArray)
39
37
  .then(function (data) {
40
38
  // If this is the creation of the key on Redis, set the expiration for it in 3600 seconds.
41
- if (data.length && data.length === featureNames.length) {
39
+ if (data === featureNames.length) {
42
40
  return _this.redis.expire(_this.key, constants_1.TTL_REFRESH);
43
41
  }
44
42
  })
@@ -24,16 +24,17 @@ var UniqueKeysCachePluggable = /** @class */ (function (_super) {
24
24
  var featureNames = Object.keys(this.uniqueKeysTracker);
25
25
  if (!featureNames.length)
26
26
  return Promise.resolve(false);
27
- var pipeline = featureNames.reduce(function (pipeline, featureName) {
27
+ var uniqueKeysArray = featureNames.map(function (featureName) {
28
28
  var featureKeys = (0, sets_1.setToArray)(_this.uniqueKeysTracker[featureName]);
29
29
  var uniqueKeysPayload = {
30
30
  f: featureName,
31
31
  ks: featureKeys
32
32
  };
33
- return pipeline.then(function () { return _this.wrapper.pushItems(_this.key, [JSON.stringify(uniqueKeysPayload)]); });
34
- }, Promise.resolve());
33
+ return JSON.stringify(uniqueKeysPayload);
34
+ });
35
35
  this.clear();
36
- return pipeline.catch(function (err) {
36
+ return this.wrapper.pushItems(this.key, uniqueKeysArray)
37
+ .catch(function (err) {
37
38
  _this.log.error(constants_2.LOG_PREFIX + "Error in uniqueKeys pipeline: " + err + ".");
38
39
  return false;
39
40
  });
@@ -48,7 +48,7 @@ exports.base = {
48
48
  urls: {
49
49
  // CDN having all the information for your environment
50
50
  sdk: 'https://sdk.split.io/api',
51
- // Storage for your SDK events
51
+ // SDK event and impression endpoints
52
52
  events: 'https://events.split.io/api',
53
53
  // SDK Auth Server
54
54
  auth: 'https://auth.split.io/api',
@@ -97,13 +97,14 @@ var BrowserSignalListener = /** @class */ (function () {
97
97
  if (!cache.isEmpty()) {
98
98
  var dataPayload = fromCacheToPayload ? fromCacheToPayload(cache.pop()) : cache.pop();
99
99
  if (!this._sendBeacon(url, dataPayload, extraMetadata)) {
100
- postService(JSON.stringify(dataPayload)).catch(function () { }); // no-op just to catch a possible exception
100
+ postService(JSON.stringify(dataPayload)).catch(function () { }); // no-op to handle possible promise rejection
101
101
  }
102
102
  }
103
103
  };
104
104
  /**
105
105
  * _sendBeacon method.
106
106
  * Util method that check if beacon API is available, build the payload and send it.
107
+ * Returns true if beacon API was used successfully, false otherwise.
107
108
  */
108
109
  BrowserSignalListener.prototype._sendBeacon = function (url, data, extraMetadata) {
109
110
  // eslint-disable-next-line compat/compat
@@ -118,8 +119,13 @@ var BrowserSignalListener = /** @class */ (function () {
118
119
  objectAssign(json, extraMetadata);
119
120
  // Stringify the payload
120
121
  var payload = JSON.stringify(json);
121
- // eslint-disable-next-line compat/compat
122
- return navigator.sendBeacon(url, payload);
122
+ // https://xgwang.me/posts/you-may-not-know-beacon/#it-may-throw-error%2C-be-sure-to-catch
123
+ try { // eslint-disable-next-line compat/compat
124
+ return navigator.sendBeacon(url, payload);
125
+ }
126
+ catch (e) {
127
+ return false;
128
+ }
123
129
  }
124
130
  return false;
125
131
  };
@@ -24,7 +24,6 @@ var ImpressionCountsCacheInMemory = /** @class */ (function () {
24
24
  this.cacheSize = this.cacheSize + amount;
25
25
  if (this.cacheSize >= this.maxStorage) {
26
26
  this.onFullQueue();
27
- this.cacheSize = 0;
28
27
  }
29
28
  }
30
29
  };
@@ -50,6 +49,7 @@ var ImpressionCountsCacheInMemory = /** @class */ (function () {
50
49
  */
51
50
  ImpressionCountsCacheInMemory.prototype.clear = function () {
52
51
  this.cache = {};
52
+ this.cacheSize = 0;
53
53
  };
54
54
  /**
55
55
  * Check if the cache is empty.
@@ -3,7 +3,7 @@ import { SegmentsCacheInMemory } from './SegmentsCacheInMemory';
3
3
  import { ImpressionsCacheInMemory } from './ImpressionsCacheInMemory';
4
4
  import { EventsCacheInMemory } from './EventsCacheInMemory';
5
5
  import { ImpressionCountsCacheInMemory } from './ImpressionCountsCacheInMemory';
6
- import { DEBUG, NONE, STORAGE_MEMORY } from '../../utils/constants';
6
+ import { DEBUG, LOCALHOST_MODE, NONE, STORAGE_MEMORY } from '../../utils/constants';
7
7
  import { shouldRecordTelemetry, TelemetryCacheInMemory } from './TelemetryCacheInMemory';
8
8
  import { UniqueKeysCacheInMemory } from './UniqueKeysCacheInMemory';
9
9
  /**
@@ -15,7 +15,7 @@ export function InMemoryStorageFactory(params) {
15
15
  var _a = params.settings, _b = _a.scheduler, impressionsQueueSize = _b.impressionsQueueSize, eventsQueueSize = _b.eventsQueueSize, impressionsMode = _a.sync.impressionsMode;
16
16
  var splits = new SplitsCacheInMemory();
17
17
  var segments = new SegmentsCacheInMemory();
18
- return {
18
+ var storage = {
19
19
  splits: splits,
20
20
  segments: segments,
21
21
  impressions: new ImpressionsCacheInMemory(impressionsQueueSize),
@@ -25,14 +25,25 @@ export function InMemoryStorageFactory(params) {
25
25
  uniqueKeys: impressionsMode === NONE ? new UniqueKeysCacheInMemory() : undefined,
26
26
  // When using MEMORY we should clean all the caches to leave them empty
27
27
  destroy: function () {
28
- var _a;
29
28
  this.splits.clear();
30
29
  this.segments.clear();
31
30
  this.impressions.clear();
32
31
  this.impressionCounts && this.impressionCounts.clear();
33
32
  this.events.clear();
34
- (_a = this.uniqueKeys) === null || _a === void 0 ? void 0 : _a.clear();
33
+ this.uniqueKeys && this.uniqueKeys.clear();
35
34
  }
36
35
  };
36
+ // @TODO revisit storage logic in localhost mode
37
+ // No tracking data in localhost mode to avoid memory leaks
38
+ if (params.settings.mode === LOCALHOST_MODE) {
39
+ var noopTrack = function () { return true; };
40
+ storage.impressions.track = noopTrack;
41
+ storage.events.track = noopTrack;
42
+ if (storage.impressionCounts)
43
+ storage.impressionCounts.track = noopTrack;
44
+ if (storage.uniqueKeys)
45
+ storage.uniqueKeys.track = noopTrack;
46
+ }
47
+ return storage;
37
48
  }
38
49
  InMemoryStorageFactory.type = STORAGE_MEMORY;
@@ -3,7 +3,7 @@ import { MySegmentsCacheInMemory } from './MySegmentsCacheInMemory';
3
3
  import { ImpressionsCacheInMemory } from './ImpressionsCacheInMemory';
4
4
  import { EventsCacheInMemory } from './EventsCacheInMemory';
5
5
  import { ImpressionCountsCacheInMemory } from './ImpressionCountsCacheInMemory';
6
- import { DEBUG, NONE, STORAGE_MEMORY } from '../../utils/constants';
6
+ import { DEBUG, LOCALHOST_MODE, NONE, STORAGE_MEMORY } from '../../utils/constants';
7
7
  import { shouldRecordTelemetry, TelemetryCacheInMemory } from './TelemetryCacheInMemory';
8
8
  import { UniqueKeysCacheInMemoryCS } from './UniqueKeysCacheInMemoryCS';
9
9
  /**
@@ -15,7 +15,7 @@ export function InMemoryStorageCSFactory(params) {
15
15
  var _a = params.settings, _b = _a.scheduler, impressionsQueueSize = _b.impressionsQueueSize, eventsQueueSize = _b.eventsQueueSize, impressionsMode = _a.sync.impressionsMode;
16
16
  var splits = new SplitsCacheInMemory();
17
17
  var segments = new MySegmentsCacheInMemory();
18
- return {
18
+ var storage = {
19
19
  splits: splits,
20
20
  segments: segments,
21
21
  impressions: new ImpressionsCacheInMemory(impressionsQueueSize),
@@ -25,13 +25,12 @@ export function InMemoryStorageCSFactory(params) {
25
25
  uniqueKeys: impressionsMode === NONE ? new UniqueKeysCacheInMemoryCS() : undefined,
26
26
  // When using MEMORY we should clean all the caches to leave them empty
27
27
  destroy: function () {
28
- var _a;
29
28
  this.splits.clear();
30
29
  this.segments.clear();
31
30
  this.impressions.clear();
32
31
  this.impressionCounts && this.impressionCounts.clear();
33
32
  this.events.clear();
34
- (_a = this.uniqueKeys) === null || _a === void 0 ? void 0 : _a.clear();
33
+ this.uniqueKeys && this.uniqueKeys.clear();
35
34
  },
36
35
  // When using shared instanciation with MEMORY we reuse everything but segments (they are unique per key)
37
36
  shared: function () {
@@ -50,5 +49,17 @@ export function InMemoryStorageCSFactory(params) {
50
49
  };
51
50
  },
52
51
  };
52
+ // @TODO revisit storage logic in localhost mode
53
+ // No tracking data in localhost mode to avoid memory leaks
54
+ if (params.settings.mode === LOCALHOST_MODE) {
55
+ var noopTrack = function () { return true; };
56
+ storage.impressions.track = noopTrack;
57
+ storage.events.track = noopTrack;
58
+ if (storage.impressionCounts)
59
+ storage.impressionCounts.track = noopTrack;
60
+ if (storage.uniqueKeys)
61
+ storage.uniqueKeys.track = noopTrack;
62
+ }
63
+ return storage;
53
64
  }
54
65
  InMemoryStorageCSFactory.type = STORAGE_MEMORY;
@@ -21,8 +21,8 @@ var UniqueKeysCacheInMemory = /** @class */ (function () {
21
21
  function UniqueKeysCacheInMemory(uniqueKeysQueueSize) {
22
22
  if (uniqueKeysQueueSize === void 0) { uniqueKeysQueueSize = DEFAULT_CACHE_SIZE; }
23
23
  this.uniqueTrackerSize = 0;
24
- this.maxStorage = uniqueKeysQueueSize;
25
24
  this.uniqueKeysTracker = {};
25
+ this.maxStorage = uniqueKeysQueueSize;
26
26
  }
27
27
  UniqueKeysCacheInMemory.prototype.setOnFullQueueCb = function (cb) {
28
28
  this.onFullQueue = cb;
@@ -39,7 +39,6 @@ var UniqueKeysCacheInMemory = /** @class */ (function () {
39
39
  this.uniqueTrackerSize++;
40
40
  }
41
41
  if (this.uniqueTrackerSize >= this.maxStorage && this.onFullQueue) {
42
- this.uniqueTrackerSize = 0;
43
42
  this.onFullQueue();
44
43
  }
45
44
  };
@@ -47,6 +46,7 @@ var UniqueKeysCacheInMemory = /** @class */ (function () {
47
46
  * Clear the data stored on the cache.
48
47
  */
49
48
  UniqueKeysCacheInMemory.prototype.clear = function () {
49
+ this.uniqueTrackerSize = 0;
50
50
  this.uniqueKeysTracker = {};
51
51
  };
52
52
  /**
@@ -54,7 +54,7 @@ var UniqueKeysCacheInMemory = /** @class */ (function () {
54
54
  */
55
55
  UniqueKeysCacheInMemory.prototype.pop = function () {
56
56
  var data = this.uniqueKeysTracker;
57
- this.uniqueKeysTracker = {};
57
+ this.clear();
58
58
  return fromUniqueKeysCollector(data);
59
59
  };
60
60
  /**
@@ -9,8 +9,8 @@ var UniqueKeysCacheInMemoryCS = /** @class */ (function () {
9
9
  function UniqueKeysCacheInMemoryCS(uniqueKeysQueueSize) {
10
10
  if (uniqueKeysQueueSize === void 0) { uniqueKeysQueueSize = DEFAULT_CACHE_SIZE; }
11
11
  this.uniqueTrackerSize = 0;
12
- this.maxStorage = uniqueKeysQueueSize;
13
12
  this.uniqueKeysTracker = {};
13
+ this.maxStorage = uniqueKeysQueueSize;
14
14
  }
15
15
  UniqueKeysCacheInMemoryCS.prototype.setOnFullQueueCb = function (cb) {
16
16
  this.onFullQueue = cb;
@@ -27,7 +27,6 @@ var UniqueKeysCacheInMemoryCS = /** @class */ (function () {
27
27
  this.uniqueTrackerSize++;
28
28
  }
29
29
  if (this.uniqueTrackerSize >= this.maxStorage && this.onFullQueue) {
30
- this.uniqueTrackerSize = 0;
31
30
  this.onFullQueue();
32
31
  }
33
32
  };
@@ -35,6 +34,7 @@ var UniqueKeysCacheInMemoryCS = /** @class */ (function () {
35
34
  * Clear the data stored on the cache.
36
35
  */
37
36
  UniqueKeysCacheInMemoryCS.prototype.clear = function () {
37
+ this.uniqueTrackerSize = 0;
38
38
  this.uniqueKeysTracker = {};
39
39
  };
40
40
  /**
@@ -42,7 +42,7 @@ var UniqueKeysCacheInMemoryCS = /** @class */ (function () {
42
42
  */
43
43
  UniqueKeysCacheInMemoryCS.prototype.pop = function () {
44
44
  var data = this.uniqueKeysTracker;
45
- this.uniqueKeysTracker = {};
45
+ this.clear();
46
46
  return this.fromUniqueKeysCollector(data);
47
47
  };
48
48
  /**
@@ -21,21 +21,19 @@ var UniqueKeysCacheInRedis = /** @class */ (function (_super) {
21
21
  var featureNames = Object.keys(this.uniqueKeysTracker);
22
22
  if (!featureNames.length)
23
23
  return Promise.resolve(false);
24
- var pipeline = this.redis.pipeline();
25
- for (var i = 0; i < featureNames.length; i++) {
26
- var featureName = featureNames[i];
27
- var featureKeys = setToArray(this.uniqueKeysTracker[featureName]);
24
+ var uniqueKeysArray = featureNames.map(function (featureName) {
25
+ var featureKeys = setToArray(_this.uniqueKeysTracker[featureName]);
28
26
  var uniqueKeysPayload = {
29
27
  f: featureName,
30
28
  ks: featureKeys
31
29
  };
32
- pipeline.rpush(this.key, JSON.stringify(uniqueKeysPayload));
33
- }
30
+ return JSON.stringify(uniqueKeysPayload);
31
+ });
34
32
  this.clear();
35
- return pipeline.exec()
33
+ return this.redis.rpush(this.key, uniqueKeysArray)
36
34
  .then(function (data) {
37
35
  // If this is the creation of the key on Redis, set the expiration for it in 3600 seconds.
38
- if (data.length && data.length === featureNames.length) {
36
+ if (data === featureNames.length) {
39
37
  return _this.redis.expire(_this.key, TTL_REFRESH);
40
38
  }
41
39
  })
@@ -21,16 +21,17 @@ var UniqueKeysCachePluggable = /** @class */ (function (_super) {
21
21
  var featureNames = Object.keys(this.uniqueKeysTracker);
22
22
  if (!featureNames.length)
23
23
  return Promise.resolve(false);
24
- var pipeline = featureNames.reduce(function (pipeline, featureName) {
24
+ var uniqueKeysArray = featureNames.map(function (featureName) {
25
25
  var featureKeys = setToArray(_this.uniqueKeysTracker[featureName]);
26
26
  var uniqueKeysPayload = {
27
27
  f: featureName,
28
28
  ks: featureKeys
29
29
  };
30
- return pipeline.then(function () { return _this.wrapper.pushItems(_this.key, [JSON.stringify(uniqueKeysPayload)]); });
31
- }, Promise.resolve());
30
+ return JSON.stringify(uniqueKeysPayload);
31
+ });
32
32
  this.clear();
33
- return pipeline.catch(function (err) {
33
+ return this.wrapper.pushItems(this.key, uniqueKeysArray)
34
+ .catch(function (err) {
34
35
  _this.log.error(LOG_PREFIX + "Error in uniqueKeys pipeline: " + err + ".");
35
36
  return false;
36
37
  });
@@ -45,7 +45,7 @@ export var base = {
45
45
  urls: {
46
46
  // CDN having all the information for your environment
47
47
  sdk: 'https://sdk.split.io/api',
48
- // Storage for your SDK events
48
+ // SDK event and impression endpoints
49
49
  events: 'https://events.split.io/api',
50
50
  // SDK Auth Server
51
51
  auth: 'https://auth.split.io/api',
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@splitsoftware/splitio-commons",
3
- "version": "1.7.1",
3
+ "version": "1.7.3-rc.0",
4
4
  "description": "Split Javascript SDK common components",
5
5
  "main": "cjs/index.js",
6
6
  "module": "esm/index.js",
@@ -114,7 +114,7 @@ export class BrowserSignalListener implements ISignalListener {
114
114
  if (!cache.isEmpty()) {
115
115
  const dataPayload = fromCacheToPayload ? fromCacheToPayload(cache.pop()) : cache.pop();
116
116
  if (!this._sendBeacon(url, dataPayload, extraMetadata)) {
117
- postService(JSON.stringify(dataPayload)).catch(() => { }); // no-op just to catch a possible exception
117
+ postService(JSON.stringify(dataPayload)).catch(() => { }); // no-op to handle possible promise rejection
118
118
  }
119
119
  }
120
120
  }
@@ -122,6 +122,7 @@ export class BrowserSignalListener implements ISignalListener {
122
122
  /**
123
123
  * _sendBeacon method.
124
124
  * Util method that check if beacon API is available, build the payload and send it.
125
+ * Returns true if beacon API was used successfully, false otherwise.
125
126
  */
126
127
  private _sendBeacon(url: string, data: any, extraMetadata?: {}) {
127
128
  // eslint-disable-next-line compat/compat
@@ -138,8 +139,12 @@ export class BrowserSignalListener implements ISignalListener {
138
139
  // Stringify the payload
139
140
  const payload = JSON.stringify(json);
140
141
 
141
- // eslint-disable-next-line compat/compat
142
- return navigator.sendBeacon(url, payload);
142
+ // https://xgwang.me/posts/you-may-not-know-beacon/#it-may-throw-error%2C-be-sure-to-catch
143
+ try { // eslint-disable-next-line compat/compat
144
+ return navigator.sendBeacon(url, payload);
145
+ } catch (e) {
146
+ return false;
147
+ }
143
148
  }
144
149
  return false;
145
150
  }
@@ -30,7 +30,6 @@ export class ImpressionCountsCacheInMemory implements IImpressionCountsCacheSync
30
30
  this.cacheSize = this.cacheSize + amount;
31
31
  if (this.cacheSize >= this.maxStorage) {
32
32
  this.onFullQueue();
33
- this.cacheSize = 0;
34
33
  }
35
34
  }
36
35
  }
@@ -58,6 +57,7 @@ export class ImpressionCountsCacheInMemory implements IImpressionCountsCacheSync
58
57
  */
59
58
  clear() {
60
59
  this.cache = {};
60
+ this.cacheSize = 0;
61
61
  }
62
62
 
63
63
  /**
@@ -4,7 +4,7 @@ import { ImpressionsCacheInMemory } from './ImpressionsCacheInMemory';
4
4
  import { EventsCacheInMemory } from './EventsCacheInMemory';
5
5
  import { IStorageFactoryParams, IStorageSync } from '../types';
6
6
  import { ImpressionCountsCacheInMemory } from './ImpressionCountsCacheInMemory';
7
- import { DEBUG, NONE, STORAGE_MEMORY } from '../../utils/constants';
7
+ import { DEBUG, LOCALHOST_MODE, NONE, STORAGE_MEMORY } from '../../utils/constants';
8
8
  import { shouldRecordTelemetry, TelemetryCacheInMemory } from './TelemetryCacheInMemory';
9
9
  import { UniqueKeysCacheInMemory } from './UniqueKeysCacheInMemory';
10
10
 
@@ -19,7 +19,7 @@ export function InMemoryStorageFactory(params: IStorageFactoryParams): IStorageS
19
19
  const splits = new SplitsCacheInMemory();
20
20
  const segments = new SegmentsCacheInMemory();
21
21
 
22
- return {
22
+ const storage = {
23
23
  splits,
24
24
  segments,
25
25
  impressions: new ImpressionsCacheInMemory(impressionsQueueSize),
@@ -35,9 +35,21 @@ export function InMemoryStorageFactory(params: IStorageFactoryParams): IStorageS
35
35
  this.impressions.clear();
36
36
  this.impressionCounts && this.impressionCounts.clear();
37
37
  this.events.clear();
38
- this.uniqueKeys?.clear();
38
+ this.uniqueKeys && this.uniqueKeys.clear();
39
39
  }
40
40
  };
41
+
42
+ // @TODO revisit storage logic in localhost mode
43
+ // No tracking data in localhost mode to avoid memory leaks
44
+ if (params.settings.mode === LOCALHOST_MODE) {
45
+ const noopTrack = () => true;
46
+ storage.impressions.track = noopTrack;
47
+ storage.events.track = noopTrack;
48
+ if (storage.impressionCounts) storage.impressionCounts.track = noopTrack;
49
+ if (storage.uniqueKeys) storage.uniqueKeys.track = noopTrack;
50
+ }
51
+
52
+ return storage;
41
53
  }
42
54
 
43
55
  InMemoryStorageFactory.type = STORAGE_MEMORY;
@@ -4,7 +4,7 @@ import { ImpressionsCacheInMemory } from './ImpressionsCacheInMemory';
4
4
  import { EventsCacheInMemory } from './EventsCacheInMemory';
5
5
  import { IStorageSync, IStorageFactoryParams } from '../types';
6
6
  import { ImpressionCountsCacheInMemory } from './ImpressionCountsCacheInMemory';
7
- import { DEBUG, NONE, STORAGE_MEMORY } from '../../utils/constants';
7
+ import { DEBUG, LOCALHOST_MODE, NONE, STORAGE_MEMORY } from '../../utils/constants';
8
8
  import { shouldRecordTelemetry, TelemetryCacheInMemory } from './TelemetryCacheInMemory';
9
9
  import { UniqueKeysCacheInMemoryCS } from './UniqueKeysCacheInMemoryCS';
10
10
 
@@ -19,7 +19,7 @@ export function InMemoryStorageCSFactory(params: IStorageFactoryParams): IStorag
19
19
  const splits = new SplitsCacheInMemory();
20
20
  const segments = new MySegmentsCacheInMemory();
21
21
 
22
- return {
22
+ const storage = {
23
23
  splits,
24
24
  segments,
25
25
  impressions: new ImpressionsCacheInMemory(impressionsQueueSize),
@@ -35,7 +35,7 @@ export function InMemoryStorageCSFactory(params: IStorageFactoryParams): IStorag
35
35
  this.impressions.clear();
36
36
  this.impressionCounts && this.impressionCounts.clear();
37
37
  this.events.clear();
38
- this.uniqueKeys?.clear();
38
+ this.uniqueKeys && this.uniqueKeys.clear();
39
39
  },
40
40
 
41
41
  // When using shared instanciation with MEMORY we reuse everything but segments (they are unique per key)
@@ -56,6 +56,18 @@ export function InMemoryStorageCSFactory(params: IStorageFactoryParams): IStorag
56
56
  };
57
57
  },
58
58
  };
59
+
60
+ // @TODO revisit storage logic in localhost mode
61
+ // No tracking data in localhost mode to avoid memory leaks
62
+ if (params.settings.mode === LOCALHOST_MODE) {
63
+ const noopTrack = () => true;
64
+ storage.impressions.track = noopTrack;
65
+ storage.events.track = noopTrack;
66
+ if (storage.impressionCounts) storage.impressionCounts.track = noopTrack;
67
+ if (storage.uniqueKeys) storage.uniqueKeys.track = noopTrack;
68
+ }
69
+
70
+ return storage;
59
71
  }
60
72
 
61
73
  InMemoryStorageCSFactory.type = STORAGE_MEMORY;
@@ -27,11 +27,10 @@ export class UniqueKeysCacheInMemory implements IUniqueKeysCacheBase {
27
27
  protected onFullQueue?: () => void;
28
28
  private readonly maxStorage: number;
29
29
  private uniqueTrackerSize = 0;
30
- protected uniqueKeysTracker: { [featureName: string]: ISet<string> };
30
+ protected uniqueKeysTracker: { [featureName: string]: ISet<string> } = {};
31
31
 
32
32
  constructor(uniqueKeysQueueSize = DEFAULT_CACHE_SIZE) {
33
33
  this.maxStorage = uniqueKeysQueueSize;
34
- this.uniqueKeysTracker = {};
35
34
  }
36
35
 
37
36
  setOnFullQueueCb(cb: () => void) {
@@ -49,7 +48,6 @@ export class UniqueKeysCacheInMemory implements IUniqueKeysCacheBase {
49
48
  this.uniqueTrackerSize++;
50
49
  }
51
50
  if (this.uniqueTrackerSize >= this.maxStorage && this.onFullQueue) {
52
- this.uniqueTrackerSize = 0;
53
51
  this.onFullQueue();
54
52
  }
55
53
  }
@@ -58,6 +56,7 @@ export class UniqueKeysCacheInMemory implements IUniqueKeysCacheBase {
58
56
  * Clear the data stored on the cache.
59
57
  */
60
58
  clear() {
59
+ this.uniqueTrackerSize = 0;
61
60
  this.uniqueKeysTracker = {};
62
61
  }
63
62
 
@@ -66,7 +65,7 @@ export class UniqueKeysCacheInMemory implements IUniqueKeysCacheBase {
66
65
  */
67
66
  pop() {
68
67
  const data = this.uniqueKeysTracker;
69
- this.uniqueKeysTracker = {};
68
+ this.clear();
70
69
  return fromUniqueKeysCollector(data);
71
70
  }
72
71
 
@@ -8,7 +8,7 @@ export class UniqueKeysCacheInMemoryCS implements IUniqueKeysCacheBase {
8
8
  private onFullQueue?: () => void;
9
9
  private readonly maxStorage: number;
10
10
  private uniqueTrackerSize = 0;
11
- private uniqueKeysTracker: { [userKey: string]: ISet<string> };
11
+ private uniqueKeysTracker: { [userKey: string]: ISet<string> } = {};
12
12
 
13
13
  /**
14
14
  *
@@ -17,7 +17,6 @@ export class UniqueKeysCacheInMemoryCS implements IUniqueKeysCacheBase {
17
17
  */
18
18
  constructor(uniqueKeysQueueSize = DEFAULT_CACHE_SIZE) {
19
19
  this.maxStorage = uniqueKeysQueueSize;
20
- this.uniqueKeysTracker = {};
21
20
  }
22
21
 
23
22
  setOnFullQueueCb(cb: () => void) {
@@ -36,7 +35,6 @@ export class UniqueKeysCacheInMemoryCS implements IUniqueKeysCacheBase {
36
35
  this.uniqueTrackerSize++;
37
36
  }
38
37
  if (this.uniqueTrackerSize >= this.maxStorage && this.onFullQueue) {
39
- this.uniqueTrackerSize = 0;
40
38
  this.onFullQueue();
41
39
  }
42
40
  }
@@ -45,6 +43,7 @@ export class UniqueKeysCacheInMemoryCS implements IUniqueKeysCacheBase {
45
43
  * Clear the data stored on the cache.
46
44
  */
47
45
  clear() {
46
+ this.uniqueTrackerSize = 0;
48
47
  this.uniqueKeysTracker = {};
49
48
  }
50
49
 
@@ -53,7 +52,7 @@ export class UniqueKeysCacheInMemoryCS implements IUniqueKeysCacheBase {
53
52
  */
54
53
  pop() {
55
54
  const data = this.uniqueKeysTracker;
56
- this.uniqueKeysTracker = {};
55
+ this.clear();
57
56
  return this.fromUniqueKeysCollector(data);
58
57
  }
59
58
 
@@ -28,22 +28,20 @@ export class UniqueKeysCacheInRedis extends UniqueKeysCacheInMemory implements I
28
28
  const featureNames = Object.keys(this.uniqueKeysTracker);
29
29
  if (!featureNames.length) return Promise.resolve(false);
30
30
 
31
- const pipeline = this.redis.pipeline();
32
- for (let i = 0; i < featureNames.length; i++) {
33
- const featureName = featureNames[i];
31
+ const uniqueKeysArray = featureNames.map((featureName) => {
34
32
  const featureKeys = setToArray(this.uniqueKeysTracker[featureName]);
35
33
  const uniqueKeysPayload = {
36
34
  f: featureName,
37
35
  ks: featureKeys
38
36
  };
37
+ return JSON.stringify(uniqueKeysPayload);
38
+ });
39
39
 
40
- pipeline.rpush(this.key, JSON.stringify(uniqueKeysPayload));
41
- }
42
40
  this.clear();
43
- return pipeline.exec()
41
+ return this.redis.rpush(this.key, uniqueKeysArray)
44
42
  .then(data => {
45
43
  // If this is the creation of the key on Redis, set the expiration for it in 3600 seconds.
46
- if (data.length && data.length === featureNames.length) {
44
+ if (data === featureNames.length) {
47
45
  return this.redis.expire(this.key, TTL_REFRESH);
48
46
  }
49
47
  })
@@ -27,21 +27,21 @@ export class UniqueKeysCachePluggable extends UniqueKeysCacheInMemory implements
27
27
  const featureNames = Object.keys(this.uniqueKeysTracker);
28
28
  if (!featureNames.length) return Promise.resolve(false);
29
29
 
30
- const pipeline = featureNames.reduce<Promise<any>>((pipeline, featureName) => {
30
+ const uniqueKeysArray = featureNames.map((featureName) => {
31
31
  const featureKeys = setToArray(this.uniqueKeysTracker[featureName]);
32
32
  const uniqueKeysPayload = {
33
33
  f: featureName,
34
34
  ks: featureKeys
35
35
  };
36
-
37
- return pipeline.then(() => this.wrapper.pushItems(this.key, [JSON.stringify(uniqueKeysPayload)]));
38
- }, Promise.resolve());
36
+ return JSON.stringify(uniqueKeysPayload);
37
+ });
39
38
 
40
39
  this.clear();
41
- return pipeline.catch(err => {
42
- this.log.error(`${LOG_PREFIX}Error in uniqueKeys pipeline: ${err}.`);
43
- return false;
44
- });
40
+ return this.wrapper.pushItems(this.key, uniqueKeysArray)
41
+ .catch(err => {
42
+ this.log.error(`${LOG_PREFIX}Error in uniqueKeys pipeline: ${err}.`);
43
+ return false;
44
+ });
45
45
  }
46
46
 
47
47
 
@@ -51,7 +51,7 @@ export const base = {
51
51
  urls: {
52
52
  // CDN having all the information for your environment
53
53
  sdk: 'https://sdk.split.io/api',
54
- // Storage for your SDK events
54
+ // SDK event and impression endpoints
55
55
  events: 'https://events.split.io/api',
56
56
  // SDK Auth Server
57
57
  auth: 'https://auth.split.io/api',
@@ -35,6 +35,7 @@ export declare class BrowserSignalListener implements ISignalListener {
35
35
  /**
36
36
  * _sendBeacon method.
37
37
  * Util method that check if beacon API is available, build the payload and send it.
38
+ * Returns true if beacon API was used successfully, false otherwise.
38
39
  */
39
40
  private _sendBeacon;
40
41
  }
@@ -9,9 +9,9 @@ export declare class UniqueKeysCachePluggable extends UniqueKeysCacheInMemory im
9
9
  private readonly refreshRate;
10
10
  private intervalId;
11
11
  constructor(log: ILogger, key: string, wrapper: IPluggableStorageWrapper, uniqueKeysQueueSize?: number, refreshRate?: number);
12
- storeUniqueKeys(): Promise<any>;
12
+ storeUniqueKeys(): Promise<boolean | void>;
13
13
  start(): void;
14
- stop(): Promise<any>;
14
+ stop(): Promise<boolean | void>;
15
15
  /**
16
16
  * Async consumer API, used by synchronizer.
17
17
  * @param count number of items to pop from the queue. If not provided or equal 0, all items will be popped.
Binary file