@splitsoftware/splitio-commons 2.11.0 → 2.11.1-rc.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/CHANGES.txt CHANGED
@@ -1,3 +1,6 @@
1
+ 2.12.0 (February 20, 2026)
2
+ - Add support for ioredis v5
3
+
1
4
  2.11.0 (January 28, 2026)
2
5
  - Added functionality to provide metadata alongside SDK update and READY events. Read more in our docs.
3
6
 
@@ -30,7 +30,7 @@ var ImpressionCountsCacheInRedis = /** @class */ (function (_super) {
30
30
  return pipeline.exec()
31
31
  .then(function (data) {
32
32
  // If this is the creation of the key on Redis, set the expiration for it in 3600 seconds.
33
- if (data.length && data.length === keys.length) {
33
+ if (data && data.length && data.length === keys.length) {
34
34
  return _this.redis.expire(_this.key, constants_1.TTL_REFRESH);
35
35
  }
36
36
  })
@@ -1,6 +1,7 @@
1
1
  "use strict";
2
2
  Object.defineProperty(exports, "__esModule", { value: true });
3
3
  exports.ImpressionsCacheInRedis = void 0;
4
+ var tslib_1 = require("tslib");
4
5
  var utils_1 = require("../utils");
5
6
  var IMPRESSIONS_TTL_REFRESH = 3600; // 1 hr
6
7
  var ImpressionsCacheInRedis = /** @class */ (function () {
@@ -11,8 +12,9 @@ var ImpressionsCacheInRedis = /** @class */ (function () {
11
12
  this.metadata = metadata;
12
13
  }
13
14
  ImpressionsCacheInRedis.prototype.track = function (impressions) {
15
+ var _a;
14
16
  var _this = this;
15
- return this.redis.rpush(this.key, (0, utils_1.impressionsToJSON)(impressions, this.metadata)).then(function (queuedCount) {
17
+ return (_a = this.redis).rpush.apply(_a, (0, tslib_1.__spreadArray)([this.key], (0, utils_1.impressionsToJSON)(impressions, this.metadata), false)).then(function (queuedCount) {
16
18
  // If this is the creation of the key on Redis, set the expiration for it in 1hr.
17
19
  if (queuedCount === impressions.length) {
18
20
  return _this.redis.expire(_this.key, IMPRESSIONS_TTL_REFRESH);
@@ -2,7 +2,17 @@
2
2
  Object.defineProperty(exports, "__esModule", { value: true });
3
3
  exports.RedisAdapter = void 0;
4
4
  var tslib_1 = require("tslib");
5
- var ioredis_1 = (0, tslib_1.__importDefault)(require("ioredis"));
5
+ // Dynamically require ioredis to prevent strict TypeScript binding
6
+ // and handle module export differences between v4 and v5.
7
+ var RedisConstructor;
8
+ try {
9
+ var ioredisLib = require('ioredis');
10
+ RedisConstructor = ioredisLib.default || ioredisLib;
11
+ }
12
+ catch (e) {
13
+ // If we reach here, the peer dependency is missing
14
+ throw new Error('ioredis is missing. Please install ioredis v4 or v5.');
15
+ }
6
16
  var lang_1 = require("../../utils/lang");
7
17
  var thenable_1 = require("../../utils/promise/thenable");
8
18
  var timeout_1 = require("../../utils/promise/timeout");
@@ -20,31 +30,49 @@ var DEFAULT_OPTIONS = {
20
30
  var DEFAULT_LIBRARY_OPTIONS = {
21
31
  enableOfflineQueue: false,
22
32
  connectTimeout: DEFAULT_OPTIONS.connectionTimeout,
23
- lazyConnect: false
33
+ lazyConnect: false,
34
+ // CRITICAL: v5 defaults this to 0 (disabled), which breaks dynamic clusters.
35
+ // v4 defaulted to 5000. We explicitly set it here to ensure v5 works like v4.
36
+ slotsRefreshInterval: 5000,
24
37
  };
25
38
  /**
26
39
  * Redis adapter on top of the library of choice (written with ioredis) for some extra control.
40
+ * Refactored to use Composition and Proxy instead of Inheritance to support both v4 and v5.
27
41
  */
28
- var RedisAdapter = /** @class */ (function (_super) {
29
- (0, tslib_1.__extends)(RedisAdapter, _super);
42
+ var RedisAdapter = /** @class */ (function () {
30
43
  function RedisAdapter(log, storageSettings) {
31
44
  if (storageSettings === void 0) { storageSettings = {}; }
32
- var _this = this;
33
45
  var options = RedisAdapter._defineOptions(storageSettings);
34
- // Call the ioredis constructor
35
- _this = _super.apply(this, RedisAdapter._defineLibrarySettings(options)) || this;
36
- _this.log = log;
37
- _this._options = options;
38
- _this._notReadyCommandsQueue = [];
39
- _this._runningCommands = new Set();
40
- _this._listenToEvents();
41
- _this._setTimeoutWrappers();
42
- _this._setDisconnectWrapper();
43
- return _this;
46
+ this.log = log;
47
+ this._options = options;
48
+ this._notReadyCommandsQueue = [];
49
+ this._runningCommands = new Set();
50
+ // Instantiate the client using the dynamic constructor
51
+ var librarySettings = RedisAdapter._defineLibrarySettings(options);
52
+ this.client = new (RedisConstructor.bind.apply(RedisConstructor, (0, tslib_1.__spreadArray)([void 0], librarySettings, false)))();
53
+ this._listenToEvents();
54
+ this._setTimeoutWrappers();
55
+ this._setDisconnectWrapper();
56
+ // Return a Proxy. This allows the adapter to act exactly like an extended class.
57
+ // If a method/property is accessed that we didn't explicitly wrap, it forwards it to `this.client`.
58
+ return new Proxy(this, {
59
+ get: function (target, prop) {
60
+ // If the property exists on our wrapper (like wrapped 'get', 'set', or internal methods)
61
+ if (prop in target) {
62
+ return target[prop];
63
+ }
64
+ // If it doesn't exist on our wrapper but exists on the real client (like 'on', 'quit')
65
+ if (target.client && prop in target.client) {
66
+ var val = target.client[prop];
67
+ return typeof val === 'function' ? val.bind(target.client) : val;
68
+ }
69
+ return undefined;
70
+ }
71
+ });
44
72
  }
45
73
  RedisAdapter.prototype._listenToEvents = function () {
46
74
  var _this = this;
47
- this.once('ready', function () {
75
+ this.client.once('ready', function () {
48
76
  var commandsCount = _this._notReadyCommandsQueue ? _this._notReadyCommandsQueue.length : 0;
49
77
  _this.log.info(LOG_PREFIX + ("Redis connection established. Queued commands: " + commandsCount + "."));
50
78
  _this._notReadyCommandsQueue && _this._notReadyCommandsQueue.forEach(function (queued) {
@@ -54,21 +82,24 @@ var RedisAdapter = /** @class */ (function (_super) {
54
82
  // After the SDK is ready for the first time we'll stop queueing commands. This is just so we can keep handling BUR for them.
55
83
  _this._notReadyCommandsQueue = undefined;
56
84
  });
57
- this.once('close', function () {
85
+ this.client.once('close', function () {
58
86
  _this.log.info(LOG_PREFIX + 'Redis connection closed.');
59
87
  });
60
88
  };
61
89
  RedisAdapter.prototype._setTimeoutWrappers = function () {
90
+ var _this = this;
62
91
  var instance = this;
63
- var wrapCommand = function (originalMethod, methodName) {
64
- // The value of "this" in this function should be the instance actually executing the method. It might be the instance referred (the base one)
65
- // or it can be the instance of a Pipeline object.
92
+ // We pass `bindTarget` so pipeline execution is bound to the pipeline object,
93
+ // while standard commands are bound to the client.
94
+ var wrapCommand = function (originalMethod, methodName, bindTarget) {
66
95
  return function () {
67
- var params = arguments;
68
- var caller = this;
96
+ var params = [];
97
+ for (var _i = 0; _i < arguments.length; _i++) {
98
+ params[_i] = arguments[_i];
99
+ }
69
100
  function commandWrapper() {
70
101
  instance.log.debug(LOG_PREFIX + "Executing " + methodName + ".");
71
- var result = originalMethod.apply(caller, params);
102
+ var result = originalMethod.apply(bindTarget, params);
72
103
  if ((0, thenable_1.thenable)(result)) {
73
104
  // For handling pending commands on disconnect, add to the set and remove once finished.
74
105
  // On sync commands there's no need, only thenables.
@@ -103,25 +134,29 @@ var RedisAdapter = /** @class */ (function (_super) {
103
134
  };
104
135
  // Wrap regular async methods to track timeouts and queue when Redis is not yet executing commands.
105
136
  METHODS_TO_PROMISE_WRAP.forEach(function (methodName) {
106
- var originalFn = instance[methodName];
107
- instance[methodName] = wrapCommand(originalFn, methodName);
137
+ var originalFn = _this.client[methodName];
138
+ _this[methodName] = wrapCommand(originalFn, methodName, _this.client);
108
139
  });
109
140
  // Special handling for pipeline~like methods. We need to wrap the async trigger, which is exec, but return the Pipeline right away.
110
141
  METHODS_TO_PROMISE_WRAP_EXEC.forEach(function (methodName) {
111
- var originalFn = instance[methodName];
142
+ var originalFn = _this.client[methodName];
112
143
  // "First level wrapper" to handle the sync execution and wrap async, queueing later if applicable.
113
- instance[methodName] = function () {
114
- var res = originalFn.apply(instance, arguments);
144
+ _this[methodName] = function () {
145
+ var args = [];
146
+ for (var _i = 0; _i < arguments.length; _i++) {
147
+ args[_i] = arguments[_i];
148
+ }
149
+ var res = originalFn.apply(instance.client, args);
115
150
  var originalExec = res.exec;
116
- res.exec = wrapCommand(originalExec, methodName + '.exec').bind(res);
151
+ res.exec = wrapCommand(originalExec, methodName + ".exec", res);
117
152
  return res;
118
153
  };
119
154
  });
120
155
  };
121
156
  RedisAdapter.prototype._setDisconnectWrapper = function () {
122
157
  var instance = this;
123
- var originalMethod = instance.disconnect;
124
- instance.disconnect = function disconnect() {
158
+ var originalMethod = this.client.disconnect;
159
+ this.disconnect = function disconnect() {
125
160
  var params = [];
126
161
  for (var _i = 0; _i < arguments.length; _i++) {
127
162
  params[_i] = arguments[_i];
@@ -132,17 +167,17 @@ var RedisAdapter = /** @class */ (function (_super) {
132
167
  Promise.all((0, sets_1.setToArray)(instance._runningCommands))
133
168
  .then(function () {
134
169
  instance.log.debug(LOG_PREFIX + 'Pending commands finished successfully, disconnecting.');
135
- originalMethod.apply(instance, params);
170
+ originalMethod.apply(instance.client, params);
136
171
  })
137
172
  .catch(function (e) {
138
173
  instance.log.warn(LOG_PREFIX + ("Pending commands finished with error: " + e + ". Proceeding with disconnection."));
139
- originalMethod.apply(instance, params);
174
+ originalMethod.apply(instance.client, params);
140
175
  });
141
176
  }
142
177
  else {
143
178
  instance.log.debug(LOG_PREFIX + 'No commands pending execution, disconnect.');
144
179
  // Nothing pending, just proceed.
145
- originalMethod.apply(instance, params);
180
+ originalMethod.apply(instance.client, params);
146
181
  }
147
182
  }, 10);
148
183
  };
@@ -191,5 +226,5 @@ var RedisAdapter = /** @class */ (function (_super) {
191
226
  return (0, lang_1.merge)({}, DEFAULT_OPTIONS, parsedOptions);
192
227
  };
193
228
  return RedisAdapter;
194
- }(ioredis_1.default));
229
+ }());
195
230
  exports.RedisAdapter = RedisAdapter;
@@ -10,11 +10,11 @@ var sets_1 = require("../../utils/lang/sets");
10
10
  * Discard errors for an answer of multiple operations.
11
11
  */
12
12
  function processPipelineAnswer(results) {
13
- return results.reduce(function (accum, errValuePair) {
13
+ return results ? results.reduce(function (accum, errValuePair) {
14
14
  if (errValuePair[0] === null)
15
15
  accum.push(errValuePair[1]);
16
16
  return accum;
17
- }, []);
17
+ }, []) : [];
18
18
  }
19
19
  /**
20
20
  * ISplitsCacheAsync implementation that stores split definitions in Redis.
@@ -175,12 +175,12 @@ var SplitsCacheInRedis = /** @class */ (function (_super) {
175
175
  SplitsCacheInRedis.prototype.getNamesByFlagSets = function (flagSets) {
176
176
  var _this = this;
177
177
  return this.redis.pipeline(flagSets.map(function (flagSet) { return ['smembers', _this.keys.buildFlagSetKey(flagSet)]; })).exec()
178
- .then(function (results) { return results.map(function (_a, index) {
178
+ .then(function (results) { return results ? results.map(function (_a, index) {
179
179
  var e = _a[0], value = _a[1];
180
180
  if (e === null)
181
181
  return value;
182
182
  _this.log.error(constants_1.LOG_PREFIX + ("Could not read result from get members of flag set " + flagSets[index] + " due to an error: " + e));
183
- }); })
183
+ }) : []; })
184
184
  .then(function (namesByFlagSets) { return namesByFlagSets.map(function (namesByFlagSet) { return new Set(namesByFlagSet); }); });
185
185
  };
186
186
  /**
@@ -20,6 +20,7 @@ var UniqueKeysCacheInRedis = /** @class */ (function (_super) {
20
20
  return _this;
21
21
  }
22
22
  UniqueKeysCacheInRedis.prototype.postUniqueKeysInRedis = function () {
23
+ var _a;
23
24
  var _this = this;
24
25
  var featureNames = Object.keys(this.uniqueKeysTracker);
25
26
  if (!featureNames.length)
@@ -33,8 +34,7 @@ var UniqueKeysCacheInRedis = /** @class */ (function (_super) {
33
34
  return JSON.stringify(uniqueKeysPayload);
34
35
  });
35
36
  this.clear();
36
- return this.redis.rpush(this.key, uniqueKeysArray)
37
- .then(function (data) {
37
+ return (_a = this.redis).rpush.apply(_a, (0, tslib_1.__spreadArray)([this.key], uniqueKeysArray, false)).then(function (data) {
38
38
  // If this is the creation of the key on Redis, set the expiration for it in 3600 seconds.
39
39
  if (data === featureNames.length) {
40
40
  return _this.redis.expire(_this.key, constants_1.TTL_REFRESH);
@@ -27,7 +27,7 @@ var ImpressionCountsCacheInRedis = /** @class */ (function (_super) {
27
27
  return pipeline.exec()
28
28
  .then(function (data) {
29
29
  // If this is the creation of the key on Redis, set the expiration for it in 3600 seconds.
30
- if (data.length && data.length === keys.length) {
30
+ if (data && data.length && data.length === keys.length) {
31
31
  return _this.redis.expire(_this.key, TTL_REFRESH);
32
32
  }
33
33
  })
@@ -1,3 +1,4 @@
1
+ import { __spreadArray } from "tslib";
1
2
  import { impressionsToJSON } from '../utils';
2
3
  var IMPRESSIONS_TTL_REFRESH = 3600; // 1 hr
3
4
  var ImpressionsCacheInRedis = /** @class */ (function () {
@@ -8,8 +9,9 @@ var ImpressionsCacheInRedis = /** @class */ (function () {
8
9
  this.metadata = metadata;
9
10
  }
10
11
  ImpressionsCacheInRedis.prototype.track = function (impressions) {
12
+ var _a;
11
13
  var _this = this;
12
- return this.redis.rpush(this.key, impressionsToJSON(impressions, this.metadata)).then(function (queuedCount) {
14
+ return (_a = this.redis).rpush.apply(_a, __spreadArray([this.key], impressionsToJSON(impressions, this.metadata), false)).then(function (queuedCount) {
13
15
  // If this is the creation of the key on Redis, set the expiration for it in 1hr.
14
16
  if (queuedCount === impressions.length) {
15
17
  return _this.redis.expire(_this.key, IMPRESSIONS_TTL_REFRESH);
@@ -1,5 +1,15 @@
1
- import { __extends } from "tslib";
2
- import ioredis from 'ioredis';
1
+ import { __spreadArray } from "tslib";
2
+ // Dynamically require ioredis to prevent strict TypeScript binding
3
+ // and handle module export differences between v4 and v5.
4
+ var RedisConstructor;
5
+ try {
6
+ var ioredisLib = require('ioredis');
7
+ RedisConstructor = ioredisLib.default || ioredisLib;
8
+ }
9
+ catch (e) {
10
+ // If we reach here, the peer dependency is missing
11
+ throw new Error('ioredis is missing. Please install ioredis v4 or v5.');
12
+ }
3
13
  import { merge, isString } from '../../utils/lang';
4
14
  import { thenable } from '../../utils/promise/thenable';
5
15
  import { timeout } from '../../utils/promise/timeout';
@@ -17,31 +27,49 @@ var DEFAULT_OPTIONS = {
17
27
  var DEFAULT_LIBRARY_OPTIONS = {
18
28
  enableOfflineQueue: false,
19
29
  connectTimeout: DEFAULT_OPTIONS.connectionTimeout,
20
- lazyConnect: false
30
+ lazyConnect: false,
31
+ // CRITICAL: v5 defaults this to 0 (disabled), which breaks dynamic clusters.
32
+ // v4 defaulted to 5000. We explicitly set it here to ensure v5 works like v4.
33
+ slotsRefreshInterval: 5000,
21
34
  };
22
35
  /**
23
36
  * Redis adapter on top of the library of choice (written with ioredis) for some extra control.
37
+ * Refactored to use Composition and Proxy instead of Inheritance to support both v4 and v5.
24
38
  */
25
- var RedisAdapter = /** @class */ (function (_super) {
26
- __extends(RedisAdapter, _super);
39
+ var RedisAdapter = /** @class */ (function () {
27
40
  function RedisAdapter(log, storageSettings) {
28
41
  if (storageSettings === void 0) { storageSettings = {}; }
29
- var _this = this;
30
42
  var options = RedisAdapter._defineOptions(storageSettings);
31
- // Call the ioredis constructor
32
- _this = _super.apply(this, RedisAdapter._defineLibrarySettings(options)) || this;
33
- _this.log = log;
34
- _this._options = options;
35
- _this._notReadyCommandsQueue = [];
36
- _this._runningCommands = new Set();
37
- _this._listenToEvents();
38
- _this._setTimeoutWrappers();
39
- _this._setDisconnectWrapper();
40
- return _this;
43
+ this.log = log;
44
+ this._options = options;
45
+ this._notReadyCommandsQueue = [];
46
+ this._runningCommands = new Set();
47
+ // Instantiate the client using the dynamic constructor
48
+ var librarySettings = RedisAdapter._defineLibrarySettings(options);
49
+ this.client = new (RedisConstructor.bind.apply(RedisConstructor, __spreadArray([void 0], librarySettings, false)))();
50
+ this._listenToEvents();
51
+ this._setTimeoutWrappers();
52
+ this._setDisconnectWrapper();
53
+ // Return a Proxy. This allows the adapter to act exactly like an extended class.
54
+ // If a method/property is accessed that we didn't explicitly wrap, it forwards it to `this.client`.
55
+ return new Proxy(this, {
56
+ get: function (target, prop) {
57
+ // If the property exists on our wrapper (like wrapped 'get', 'set', or internal methods)
58
+ if (prop in target) {
59
+ return target[prop];
60
+ }
61
+ // If it doesn't exist on our wrapper but exists on the real client (like 'on', 'quit')
62
+ if (target.client && prop in target.client) {
63
+ var val = target.client[prop];
64
+ return typeof val === 'function' ? val.bind(target.client) : val;
65
+ }
66
+ return undefined;
67
+ }
68
+ });
41
69
  }
42
70
  RedisAdapter.prototype._listenToEvents = function () {
43
71
  var _this = this;
44
- this.once('ready', function () {
72
+ this.client.once('ready', function () {
45
73
  var commandsCount = _this._notReadyCommandsQueue ? _this._notReadyCommandsQueue.length : 0;
46
74
  _this.log.info(LOG_PREFIX + ("Redis connection established. Queued commands: " + commandsCount + "."));
47
75
  _this._notReadyCommandsQueue && _this._notReadyCommandsQueue.forEach(function (queued) {
@@ -51,21 +79,24 @@ var RedisAdapter = /** @class */ (function (_super) {
51
79
  // After the SDK is ready for the first time we'll stop queueing commands. This is just so we can keep handling BUR for them.
52
80
  _this._notReadyCommandsQueue = undefined;
53
81
  });
54
- this.once('close', function () {
82
+ this.client.once('close', function () {
55
83
  _this.log.info(LOG_PREFIX + 'Redis connection closed.');
56
84
  });
57
85
  };
58
86
  RedisAdapter.prototype._setTimeoutWrappers = function () {
87
+ var _this = this;
59
88
  var instance = this;
60
- var wrapCommand = function (originalMethod, methodName) {
61
- // The value of "this" in this function should be the instance actually executing the method. It might be the instance referred (the base one)
62
- // or it can be the instance of a Pipeline object.
89
+ // We pass `bindTarget` so pipeline execution is bound to the pipeline object,
90
+ // while standard commands are bound to the client.
91
+ var wrapCommand = function (originalMethod, methodName, bindTarget) {
63
92
  return function () {
64
- var params = arguments;
65
- var caller = this;
93
+ var params = [];
94
+ for (var _i = 0; _i < arguments.length; _i++) {
95
+ params[_i] = arguments[_i];
96
+ }
66
97
  function commandWrapper() {
67
98
  instance.log.debug(LOG_PREFIX + "Executing " + methodName + ".");
68
- var result = originalMethod.apply(caller, params);
99
+ var result = originalMethod.apply(bindTarget, params);
69
100
  if (thenable(result)) {
70
101
  // For handling pending commands on disconnect, add to the set and remove once finished.
71
102
  // On sync commands there's no need, only thenables.
@@ -100,25 +131,29 @@ var RedisAdapter = /** @class */ (function (_super) {
100
131
  };
101
132
  // Wrap regular async methods to track timeouts and queue when Redis is not yet executing commands.
102
133
  METHODS_TO_PROMISE_WRAP.forEach(function (methodName) {
103
- var originalFn = instance[methodName];
104
- instance[methodName] = wrapCommand(originalFn, methodName);
134
+ var originalFn = _this.client[methodName];
135
+ _this[methodName] = wrapCommand(originalFn, methodName, _this.client);
105
136
  });
106
137
  // Special handling for pipeline~like methods. We need to wrap the async trigger, which is exec, but return the Pipeline right away.
107
138
  METHODS_TO_PROMISE_WRAP_EXEC.forEach(function (methodName) {
108
- var originalFn = instance[methodName];
139
+ var originalFn = _this.client[methodName];
109
140
  // "First level wrapper" to handle the sync execution and wrap async, queueing later if applicable.
110
- instance[methodName] = function () {
111
- var res = originalFn.apply(instance, arguments);
141
+ _this[methodName] = function () {
142
+ var args = [];
143
+ for (var _i = 0; _i < arguments.length; _i++) {
144
+ args[_i] = arguments[_i];
145
+ }
146
+ var res = originalFn.apply(instance.client, args);
112
147
  var originalExec = res.exec;
113
- res.exec = wrapCommand(originalExec, methodName + '.exec').bind(res);
148
+ res.exec = wrapCommand(originalExec, methodName + ".exec", res);
114
149
  return res;
115
150
  };
116
151
  });
117
152
  };
118
153
  RedisAdapter.prototype._setDisconnectWrapper = function () {
119
154
  var instance = this;
120
- var originalMethod = instance.disconnect;
121
- instance.disconnect = function disconnect() {
155
+ var originalMethod = this.client.disconnect;
156
+ this.disconnect = function disconnect() {
122
157
  var params = [];
123
158
  for (var _i = 0; _i < arguments.length; _i++) {
124
159
  params[_i] = arguments[_i];
@@ -129,17 +164,17 @@ var RedisAdapter = /** @class */ (function (_super) {
129
164
  Promise.all(setToArray(instance._runningCommands))
130
165
  .then(function () {
131
166
  instance.log.debug(LOG_PREFIX + 'Pending commands finished successfully, disconnecting.');
132
- originalMethod.apply(instance, params);
167
+ originalMethod.apply(instance.client, params);
133
168
  })
134
169
  .catch(function (e) {
135
170
  instance.log.warn(LOG_PREFIX + ("Pending commands finished with error: " + e + ". Proceeding with disconnection."));
136
- originalMethod.apply(instance, params);
171
+ originalMethod.apply(instance.client, params);
137
172
  });
138
173
  }
139
174
  else {
140
175
  instance.log.debug(LOG_PREFIX + 'No commands pending execution, disconnect.');
141
176
  // Nothing pending, just proceed.
142
- originalMethod.apply(instance, params);
177
+ originalMethod.apply(instance.client, params);
143
178
  }
144
179
  }, 10);
145
180
  };
@@ -188,5 +223,5 @@ var RedisAdapter = /** @class */ (function (_super) {
188
223
  return merge({}, DEFAULT_OPTIONS, parsedOptions);
189
224
  };
190
225
  return RedisAdapter;
191
- }(ioredis));
226
+ }());
192
227
  export { RedisAdapter };
@@ -7,11 +7,11 @@ import { returnDifference } from '../../utils/lang/sets';
7
7
  * Discard errors for an answer of multiple operations.
8
8
  */
9
9
  function processPipelineAnswer(results) {
10
- return results.reduce(function (accum, errValuePair) {
10
+ return results ? results.reduce(function (accum, errValuePair) {
11
11
  if (errValuePair[0] === null)
12
12
  accum.push(errValuePair[1]);
13
13
  return accum;
14
- }, []);
14
+ }, []) : [];
15
15
  }
16
16
  /**
17
17
  * ISplitsCacheAsync implementation that stores split definitions in Redis.
@@ -172,12 +172,12 @@ var SplitsCacheInRedis = /** @class */ (function (_super) {
172
172
  SplitsCacheInRedis.prototype.getNamesByFlagSets = function (flagSets) {
173
173
  var _this = this;
174
174
  return this.redis.pipeline(flagSets.map(function (flagSet) { return ['smembers', _this.keys.buildFlagSetKey(flagSet)]; })).exec()
175
- .then(function (results) { return results.map(function (_a, index) {
175
+ .then(function (results) { return results ? results.map(function (_a, index) {
176
176
  var e = _a[0], value = _a[1];
177
177
  if (e === null)
178
178
  return value;
179
179
  _this.log.error(LOG_PREFIX + ("Could not read result from get members of flag set " + flagSets[index] + " due to an error: " + e));
180
- }); })
180
+ }) : []; })
181
181
  .then(function (namesByFlagSets) { return namesByFlagSets.map(function (namesByFlagSet) { return new Set(namesByFlagSet); }); });
182
182
  };
183
183
  /**
@@ -1,4 +1,4 @@
1
- import { __extends } from "tslib";
1
+ import { __extends, __spreadArray } from "tslib";
2
2
  import { UniqueKeysCacheInMemory } from '../inMemory/UniqueKeysCacheInMemory';
3
3
  import { DEFAULT_CACHE_SIZE, REFRESH_RATE, TTL_REFRESH } from './constants';
4
4
  import { LOG_PREFIX } from './constants';
@@ -17,6 +17,7 @@ var UniqueKeysCacheInRedis = /** @class */ (function (_super) {
17
17
  return _this;
18
18
  }
19
19
  UniqueKeysCacheInRedis.prototype.postUniqueKeysInRedis = function () {
20
+ var _a;
20
21
  var _this = this;
21
22
  var featureNames = Object.keys(this.uniqueKeysTracker);
22
23
  if (!featureNames.length)
@@ -30,8 +31,7 @@ var UniqueKeysCacheInRedis = /** @class */ (function (_super) {
30
31
  return JSON.stringify(uniqueKeysPayload);
31
32
  });
32
33
  this.clear();
33
- return this.redis.rpush(this.key, uniqueKeysArray)
34
- .then(function (data) {
34
+ return (_a = this.redis).rpush.apply(_a, __spreadArray([this.key], uniqueKeysArray, false)).then(function (data) {
35
35
  // If this is the creation of the key on Redis, set the expiration for it in 3600 seconds.
36
36
  if (data === featureNames.length) {
37
37
  return _this.redis.expire(_this.key, TTL_REFRESH);
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@splitsoftware/splitio-commons",
3
- "version": "2.11.0",
3
+ "version": "2.11.1-rc.0",
4
4
  "description": "Split JavaScript SDK common components",
5
5
  "main": "cjs/index.js",
6
6
  "module": "esm/index.js",
@@ -45,11 +45,10 @@
45
45
  "bugs": "https://github.com/splitio/javascript-commons/issues",
46
46
  "homepage": "https://github.com/splitio/javascript-commons#readme",
47
47
  "dependencies": {
48
- "@types/ioredis": "^4.28.0",
49
48
  "tslib": "^2.3.1"
50
49
  },
51
50
  "peerDependencies": {
52
- "ioredis": "^4.28.0"
51
+ "ioredis": "^4.28.0 || ^5.0.0"
53
52
  },
54
53
  "peerDependenciesMeta": {
55
54
  "ioredis": {
@@ -68,7 +67,7 @@
68
67
  "eslint-plugin-import": "^2.25.3",
69
68
  "eslint-plugin-tsdoc": "^0.3.0",
70
69
  "fetch-mock": "^9.11.0",
71
- "ioredis": "^4.28.0",
70
+ "ioredis": "^5.0.0",
72
71
  "jest": "^27.2.3",
73
72
  "jest-localstorage-mock": "^2.4.3",
74
73
  "lodash": "^4.17.21",
@@ -31,7 +31,7 @@ export class EventsCacheInRedis implements IEventsCacheAsync {
31
31
  )
32
32
  // We use boolean values to signal successful queueing
33
33
  .then(() => true)
34
- .catch(err => {
34
+ .catch((err: unknown) => {
35
35
  this.log.error(`${LOG_PREFIX}Error adding event to queue: ${err}.`);
36
36
  return false;
37
37
  });
@@ -65,9 +65,9 @@ export class EventsCacheInRedis implements IEventsCacheAsync {
65
65
  * It is the submitter responsability to handle that.
66
66
  */
67
67
  popNWithMetadata(count: number): Promise<StoredEventWithMetadata[]> {
68
- return this.redis.lrange(this.key, 0, count - 1).then(items => {
68
+ return this.redis.lrange(this.key, 0, count - 1).then((items: string[]) => {
69
69
  return this.redis.ltrim(this.key, items.length, -1).then(() => {
70
- return items.map(item => JSON.parse(item) as StoredEventWithMetadata);
70
+ return items.map((item: string) => JSON.parse(item) as StoredEventWithMetadata);
71
71
  });
72
72
  });
73
73
  }
@@ -32,13 +32,13 @@ export class ImpressionCountsCacheInRedis extends ImpressionCountsCacheInMemory
32
32
  pipeline.hincrby(this.key, key, counts[key]);
33
33
  });
34
34
  return pipeline.exec()
35
- .then(data => {
35
+ .then((data: [Error | null, unknown][] | null) => {
36
36
  // If this is the creation of the key on Redis, set the expiration for it in 3600 seconds.
37
- if (data.length && data.length === keys.length) {
37
+ if (data && data.length && data.length === keys.length) {
38
38
  return this.redis.expire(this.key, TTL_REFRESH);
39
39
  }
40
40
  })
41
- .catch(err => {
41
+ .catch((err: unknown) => {
42
42
  this.log.error(`${LOG_PREFIX}Error in impression counts pipeline: ${err}.`);
43
43
  return false;
44
44
  });
@@ -56,14 +56,14 @@ export class ImpressionCountsCacheInRedis extends ImpressionCountsCacheInMemory
56
56
  // Async consumer API, used by synchronizer
57
57
  getImpressionsCount(): Promise<ImpressionCountsPayload | undefined> {
58
58
  return this.redis.hgetall(this.key)
59
- .then(counts => {
59
+ .then((counts: Record<string, string>) => {
60
60
  if (!Object.keys(counts).length) return undefined;
61
61
 
62
62
  this.redis.del(this.key).catch(() => { /* no-op */ });
63
63
 
64
64
  const pf: ImpressionCountsPayload['pf'] = [];
65
65
 
66
- forOwn(counts, (count, key) => {
66
+ forOwn(counts, (count: string, key) => {
67
67
  const nameAndTime = key.split('::');
68
68
  if (nameAndTime.length !== 2) {
69
69
  this.log.error(`${LOG_PREFIX}Error spliting key ${key}`);
@@ -25,8 +25,8 @@ export class ImpressionsCacheInRedis implements IImpressionsCacheAsync {
25
25
  track(impressions: SplitIO.ImpressionDTO[]): Promise<void> { // @ts-ignore
26
26
  return this.redis.rpush(
27
27
  this.key,
28
- impressionsToJSON(impressions, this.metadata),
29
- ).then(queuedCount => {
28
+ ...impressionsToJSON(impressions, this.metadata),
29
+ ).then((queuedCount: number) => {
30
30
  // If this is the creation of the key on Redis, set the expiration for it in 1hr.
31
31
  if (queuedCount === impressions.length) {
32
32
  return this.redis.expire(this.key, IMPRESSIONS_TTL_REFRESH);
@@ -45,7 +45,7 @@ export class ImpressionsCacheInRedis implements IImpressionsCacheAsync {
45
45
  }
46
46
 
47
47
  popNWithMetadata(count: number): Promise<StoredImpressionWithMetadata[]> {
48
- return this.redis.lrange(this.key, 0, count - 1).then(items => {
48
+ return this.redis.lrange(this.key, 0, count - 1).then((items: string[]) => {
49
49
  return this.redis.ltrim(this.key, items.length, -1).then(() => {
50
50
  // This operation will simply do nothing if the key no longer exists (queue is empty)
51
51
  // It's only done in the "successful" exit path so that the TTL is not overriden if impressons weren't
@@ -53,7 +53,7 @@ export class ImpressionsCacheInRedis implements IImpressionsCacheAsync {
53
53
  // a huge amount of memory.
54
54
  this.redis.expire(this.key, IMPRESSIONS_TTL_REFRESH).catch(() => { }); // noop catch handler
55
55
 
56
- return items.map(item => JSON.parse(item) as StoredImpressionWithMetadata);
56
+ return items.map((item: string) => JSON.parse(item) as StoredImpressionWithMetadata);
57
57
  });
58
58
  });
59
59
  }
@@ -21,12 +21,12 @@ export class RBSegmentsCacheInRedis implements IRBSegmentsCacheAsync {
21
21
 
22
22
  get(name: string): Promise<IRBSegment | null> {
23
23
  return this.redis.get(this.keys.buildRBSegmentKey(name))
24
- .then(maybeRBSegment => maybeRBSegment && JSON.parse(maybeRBSegment));
24
+ .then((maybeRBSegment: string | null) => maybeRBSegment && JSON.parse(maybeRBSegment));
25
25
  }
26
26
 
27
27
  private getNames(): Promise<string[]> {
28
28
  return this.redis.keys(this.keys.searchPatternForRBSegmentKeys()).then(
29
- (listOfKeys) => listOfKeys.map(this.keys.extractKey)
29
+ (listOfKeys: string[]) => listOfKeys.map(this.keys.extractKey)
30
30
  );
31
31
  }
32
32
 
@@ -47,7 +47,7 @@ export class RBSegmentsCacheInRedis implements IRBSegmentsCacheAsync {
47
47
  })),
48
48
  Promise.all(toRemove.map(toRemove => {
49
49
  const key = this.keys.buildRBSegmentKey(toRemove.name);
50
- return this.redis.del(key).then(status => status === 1);
50
+ return this.redis.del(key).then((status: number) => status === 1);
51
51
  }))
52
52
  ]).then(([, added, removed]) => {
53
53
  return added.some(result => result) || removed.some(result => result);
@@ -56,7 +56,7 @@ export class RBSegmentsCacheInRedis implements IRBSegmentsCacheAsync {
56
56
 
57
57
  setChangeNumber(changeNumber: number) {
58
58
  return this.redis.set(this.keys.buildRBSegmentsTillKey(), changeNumber + '').then(
59
- status => status === 'OK'
59
+ (status: string | null) => status === 'OK'
60
60
  );
61
61
  }
62
62
 
@@ -65,7 +65,7 @@ export class RBSegmentsCacheInRedis implements IRBSegmentsCacheAsync {
65
65
  const i = parseInt(value as string, 10);
66
66
 
67
67
  return isNaNNumber(i) ? -1 : i;
68
- }).catch((e) => {
68
+ }).catch((e: unknown) => {
69
69
  this.log.error(LOG_PREFIX + 'Could not retrieve changeNumber from storage. Error: ' + e);
70
70
  return -1;
71
71
  });
@@ -1,4 +1,14 @@
1
- import ioredis, { Pipeline } from 'ioredis';
1
+ // Dynamically require ioredis to prevent strict TypeScript binding
2
+ // and handle module export differences between v4 and v5.
3
+ let RedisConstructor: any;
4
+ try {
5
+ const ioredisLib = require('ioredis');
6
+ RedisConstructor = ioredisLib.default || ioredisLib;
7
+ } catch (e) {
8
+ // If we reach here, the peer dependency is missing
9
+ throw new Error('ioredis is missing. Please install ioredis v4 or v5.');
10
+ }
11
+
2
12
  import { ILogger } from '../../logger/types';
3
13
  import { merge, isString } from '../../utils/lang';
4
14
  import { thenable } from '../../utils/promise/thenable';
@@ -20,41 +30,70 @@ const DEFAULT_OPTIONS = {
20
30
  const DEFAULT_LIBRARY_OPTIONS = {
21
31
  enableOfflineQueue: false,
22
32
  connectTimeout: DEFAULT_OPTIONS.connectionTimeout,
23
- lazyConnect: false
33
+ lazyConnect: false,
34
+ // CRITICAL: v5 defaults this to 0 (disabled), which breaks dynamic clusters.
35
+ // v4 defaulted to 5000. We explicitly set it here to ensure v5 works like v4.
36
+ slotsRefreshInterval: 5000,
24
37
  };
25
38
 
26
39
  interface IRedisCommand {
27
- resolve: () => void,
40
+ resolve: (value?: any) => void,
28
41
  reject: (err?: any) => void,
29
- command: () => Promise<void>,
30
- name: string
42
+ command: () => Promise<any>,
43
+ name: string,
31
44
  }
32
45
 
33
46
  /**
34
47
  * Redis adapter on top of the library of choice (written with ioredis) for some extra control.
48
+ * Refactored to use Composition and Proxy instead of Inheritance to support both v4 and v5.
35
49
  */
36
- export class RedisAdapter extends ioredis {
50
+ export class RedisAdapter {
51
+ // eslint-disable-next-line no-undef -- Index signature to allow proxying dynamic ioredis methods without TS errors
52
+ [key: string]: any;
37
53
  private readonly log: ILogger;
38
- private _options: object;
54
+ private _options: Record<string, any>;
39
55
  private _notReadyCommandsQueue?: IRedisCommand[];
40
56
  private _runningCommands: Set<Promise<any>>;
41
57
 
58
+ // The actual ioredis instance
59
+ public client: any;
60
+
42
61
  constructor(log: ILogger, storageSettings: Record<string, any> = {}) {
43
62
  const options = RedisAdapter._defineOptions(storageSettings);
44
- // Call the ioredis constructor
45
- super(...RedisAdapter._defineLibrarySettings(options));
46
63
 
47
64
  this.log = log;
48
65
  this._options = options;
49
66
  this._notReadyCommandsQueue = [];
50
67
  this._runningCommands = new Set();
68
+
69
+ // Instantiate the client using the dynamic constructor
70
+ const librarySettings = RedisAdapter._defineLibrarySettings(options);
71
+ this.client = new RedisConstructor(...librarySettings);
72
+
51
73
  this._listenToEvents();
52
74
  this._setTimeoutWrappers();
53
75
  this._setDisconnectWrapper();
76
+
77
+ // Return a Proxy. This allows the adapter to act exactly like an extended class.
78
+ // If a method/property is accessed that we didn't explicitly wrap, it forwards it to `this.client`.
79
+ return new Proxy(this, {
80
+ get(target: RedisAdapter, prop: string | symbol) {
81
+ // If the property exists on our wrapper (like wrapped 'get', 'set', or internal methods)
82
+ if (prop in target) {
83
+ return target[prop as keyof RedisAdapter];
84
+ }
85
+ // If it doesn't exist on our wrapper but exists on the real client (like 'on', 'quit')
86
+ if (target.client && prop in target.client) {
87
+ const val = target.client[prop];
88
+ return typeof val === 'function' ? val.bind(target.client) : val;
89
+ }
90
+ return undefined;
91
+ }
92
+ });
54
93
  }
55
94
 
56
95
  _listenToEvents() {
57
- this.once('ready', () => {
96
+ this.client.once('ready', () => {
58
97
  const commandsCount = this._notReadyCommandsQueue ? this._notReadyCommandsQueue.length : 0;
59
98
  this.log.info(LOG_PREFIX + `Redis connection established. Queued commands: ${commandsCount}.`);
60
99
 
@@ -65,24 +104,21 @@ export class RedisAdapter extends ioredis {
65
104
  // After the SDK is ready for the first time we'll stop queueing commands. This is just so we can keep handling BUR for them.
66
105
  this._notReadyCommandsQueue = undefined;
67
106
  });
68
- this.once('close', () => {
107
+ this.client.once('close', () => {
69
108
  this.log.info(LOG_PREFIX + 'Redis connection closed.');
70
109
  });
71
110
  }
72
111
 
73
112
  _setTimeoutWrappers() {
74
- const instance: Record<string, any> = this;
75
-
76
- const wrapCommand = (originalMethod: Function, methodName: string) => {
77
- // The value of "this" in this function should be the instance actually executing the method. It might be the instance referred (the base one)
78
- // or it can be the instance of a Pipeline object.
79
- return function (this: RedisAdapter | Pipeline) {
80
- const params = arguments;
81
- const caller = this;
113
+ const instance = this;
82
114
 
115
+ // We pass `bindTarget` so pipeline execution is bound to the pipeline object,
116
+ // while standard commands are bound to the client.
117
+ const wrapCommand = (originalMethod: Function, methodName: string, bindTarget: any) => {
118
+ return function (...params: any[]) {
83
119
  function commandWrapper() {
84
120
  instance.log.debug(`${LOG_PREFIX}Executing ${methodName}.`);
85
- const result = originalMethod.apply(caller, params);
121
+ const result = originalMethod.apply(bindTarget, params);
86
122
 
87
123
  if (thenable(result)) {
88
124
  // For handling pending commands on disconnect, add to the set and remove once finished.
@@ -106,7 +142,7 @@ export class RedisAdapter extends ioredis {
106
142
 
107
143
  if (instance._notReadyCommandsQueue) {
108
144
  return new Promise((resolve, reject) => {
109
- instance._notReadyCommandsQueue.unshift({
145
+ instance._notReadyCommandsQueue!.unshift({
110
146
  resolve,
111
147
  reject,
112
148
  command: commandWrapper,
@@ -121,19 +157,19 @@ export class RedisAdapter extends ioredis {
121
157
 
122
158
  // Wrap regular async methods to track timeouts and queue when Redis is not yet executing commands.
123
159
  METHODS_TO_PROMISE_WRAP.forEach(methodName => {
124
- const originalFn = instance[methodName];
125
- instance[methodName] = wrapCommand(originalFn, methodName);
160
+ const originalFn = this.client[methodName];
161
+ this[methodName] = wrapCommand(originalFn, methodName, this.client);
126
162
  });
127
163
 
128
164
  // Special handling for pipeline~like methods. We need to wrap the async trigger, which is exec, but return the Pipeline right away.
129
165
  METHODS_TO_PROMISE_WRAP_EXEC.forEach(methodName => {
130
- const originalFn = instance[methodName];
166
+ const originalFn = this.client[methodName];
131
167
  // "First level wrapper" to handle the sync execution and wrap async, queueing later if applicable.
132
- instance[methodName] = function () {
133
- const res = originalFn.apply(instance, arguments);
168
+ this[methodName] = function (...args: any[]) {
169
+ const res = originalFn.apply(instance.client, args);
134
170
  const originalExec = res.exec;
135
171
 
136
- res.exec = wrapCommand(originalExec, methodName + '.exec').bind(res);
172
+ res.exec = wrapCommand(originalExec, `${methodName}.exec`, res);
137
173
 
138
174
  return res;
139
175
  };
@@ -142,10 +178,9 @@ export class RedisAdapter extends ioredis {
142
178
 
143
179
  _setDisconnectWrapper() {
144
180
  const instance = this;
145
- const originalMethod = instance.disconnect;
146
-
147
- instance.disconnect = function disconnect(...params: []) {
181
+ const originalMethod = this.client.disconnect;
148
182
 
183
+ this.disconnect = function disconnect(...params: any[]) {
149
184
  setTimeout(function deferredDisconnect() {
150
185
  if (instance._runningCommands.size > 0) {
151
186
  instance.log.info(LOG_PREFIX + `Attempting to disconnect but there are ${instance._runningCommands.size} commands still waiting for resolution. Defering disconnection until those finish.`);
@@ -153,16 +188,16 @@ export class RedisAdapter extends ioredis {
153
188
  Promise.all(setToArray(instance._runningCommands))
154
189
  .then(() => {
155
190
  instance.log.debug(LOG_PREFIX + 'Pending commands finished successfully, disconnecting.');
156
- originalMethod.apply(instance, params);
191
+ originalMethod.apply(instance.client, params);
157
192
  })
158
193
  .catch(e => {
159
194
  instance.log.warn(LOG_PREFIX + `Pending commands finished with error: ${e}. Proceeding with disconnection.`);
160
- originalMethod.apply(instance, params);
195
+ originalMethod.apply(instance.client, params);
161
196
  });
162
197
  } else {
163
198
  instance.log.debug(LOG_PREFIX + 'No commands pending execution, disconnect.');
164
199
  // Nothing pending, just proceed.
165
- originalMethod.apply(instance, params);
200
+ originalMethod.apply(instance.client, params);
166
201
  }
167
202
  }, 10);
168
203
  };
@@ -37,7 +37,7 @@ export class SegmentsCacheInRedis implements ISegmentsCacheAsync {
37
37
  isInSegment(name: string, key: string) {
38
38
  return this.redis.sismember(
39
39
  this.keys.buildSegmentNameKey(name), key
40
- ).then(matches => matches !== 0);
40
+ ).then((matches: number) => matches !== 0);
41
41
  }
42
42
 
43
43
  getChangeNumber(name: string) {
@@ -45,7 +45,7 @@ export class SegmentsCacheInRedis implements ISegmentsCacheAsync {
45
45
  const i = parseInt(value as string, 10);
46
46
 
47
47
  return isNaNNumber(i) ? undefined : i;
48
- }).catch((e) => {
48
+ }).catch((e: unknown) => {
49
49
  this.log.error(LOG_PREFIX + 'Could not retrieve changeNumber from segments storage. Error: ' + e);
50
50
  return undefined;
51
51
  });
@@ -10,11 +10,11 @@ import type { RedisAdapter } from './RedisAdapter';
10
10
  /**
11
11
  * Discard errors for an answer of multiple operations.
12
12
  */
13
- function processPipelineAnswer(results: Array<[Error | null, string]>): string[] {
14
- return results.reduce((accum: string[], errValuePair: [Error | null, string]) => {
15
- if (errValuePair[0] === null) accum.push(errValuePair[1]);
13
+ function processPipelineAnswer(results: Array<[Error | null, unknown]> | null): string[] {
14
+ return results ? results.reduce((accum: string[], errValuePair: [Error | null, unknown]) => {
15
+ if (errValuePair[0] === null) accum.push(errValuePair[1] as string);
16
16
  return accum;
17
- }, []);
17
+ }, []) : [];
18
18
  }
19
19
 
20
20
  /**
@@ -26,7 +26,7 @@ export class SplitsCacheInRedis extends AbstractSplitsCacheAsync {
26
26
  private readonly log: ILogger;
27
27
  private readonly redis: RedisAdapter;
28
28
  private readonly keys: KeyBuilderSS;
29
- private redisError?: string;
29
+ private redisError?: Error;
30
30
  private readonly flagSetsFilter: string[];
31
31
 
32
32
  constructor(log: ILogger, keys: KeyBuilderSS, redis: RedisAdapter, splitFiltersValidation?: ISplitFiltersValidation) {
@@ -38,7 +38,7 @@ export class SplitsCacheInRedis extends AbstractSplitsCacheAsync {
38
38
 
39
39
  // There is no need to listen for redis 'error' event, because in that case ioredis calls will be rejected and handled by redis storage adapters.
40
40
  // But it is done just to avoid getting the ioredis message `Unhandled error event`.
41
- this.redis.on('error', (e) => {
41
+ this.redis.on('error', (e: Error) => {
42
42
  this.redisError = e;
43
43
  });
44
44
 
@@ -49,7 +49,7 @@ export class SplitsCacheInRedis extends AbstractSplitsCacheAsync {
49
49
 
50
50
  private _decrementCounts(split: ISplit) {
51
51
  const ttKey = this.keys.buildTrafficTypeKey(split.trafficTypeName);
52
- return this.redis.decr(ttKey).then(count => {
52
+ return this.redis.decr(ttKey).then((count: number) => {
53
53
  if (count === 0) return this.redis.del(ttKey);
54
54
  });
55
55
  }
@@ -85,7 +85,7 @@ export class SplitsCacheInRedis extends AbstractSplitsCacheAsync {
85
85
  addSplit(split: ISplit): Promise<boolean> {
86
86
  const name = split.name;
87
87
  const splitKey = this.keys.buildSplitKey(name);
88
- return this.redis.get(splitKey).then(splitFromStorage => {
88
+ return this.redis.get(splitKey).then((splitFromStorage: string | null) => {
89
89
 
90
90
  // handling parsing error
91
91
  let parsedPreviousSplit: ISplit, stringifiedNewSplit;
@@ -119,7 +119,7 @@ export class SplitsCacheInRedis extends AbstractSplitsCacheAsync {
119
119
  return this._decrementCounts(split).then(() => this._updateFlagSets(name, split.sets));
120
120
  }
121
121
  }).then(() => {
122
- return this.redis.del(this.keys.buildSplitKey(name)).then(status => status === 1);
122
+ return this.redis.del(this.keys.buildSplitKey(name)).then((status: number) => status === 1);
123
123
  });
124
124
  }
125
125
 
@@ -135,7 +135,7 @@ export class SplitsCacheInRedis extends AbstractSplitsCacheAsync {
135
135
  }
136
136
 
137
137
  return this.redis.get(this.keys.buildSplitKey(name))
138
- .then(maybeSplit => maybeSplit && JSON.parse(maybeSplit));
138
+ .then((maybeSplit: string | null) => maybeSplit && JSON.parse(maybeSplit));
139
139
  }
140
140
 
141
141
  /**
@@ -145,7 +145,7 @@ export class SplitsCacheInRedis extends AbstractSplitsCacheAsync {
145
145
  */
146
146
  setChangeNumber(changeNumber: number): Promise<boolean> {
147
147
  return this.redis.set(this.keys.buildSplitsTillKey(), changeNumber + '').then(
148
- status => status === 'OK'
148
+ (status: string | null) => status === 'OK'
149
149
  );
150
150
  }
151
151
 
@@ -159,7 +159,7 @@ export class SplitsCacheInRedis extends AbstractSplitsCacheAsync {
159
159
  const i = parseInt(value as string, 10);
160
160
 
161
161
  return isNaNNumber(i) ? -1 : i;
162
- }).catch((e) => {
162
+ }).catch((e: unknown) => {
163
163
  this.log.error(LOG_PREFIX + 'Could not retrieve changeNumber from storage. Error: ' + e);
164
164
  return -1;
165
165
  });
@@ -173,9 +173,9 @@ export class SplitsCacheInRedis extends AbstractSplitsCacheAsync {
173
173
  // @TODO we need to benchmark which is the maximun number of commands we could pipeline without kill redis performance.
174
174
  getAll(): Promise<ISplit[]> {
175
175
  return this.redis.keys(this.keys.searchPatternForSplitKeys())
176
- .then((listOfKeys) => this.redis.pipeline(listOfKeys.map(k => ['get', k])).exec())
176
+ .then((listOfKeys: string[]) => this.redis.pipeline(listOfKeys.map((k: string) => ['get', k])).exec())
177
177
  .then(processPipelineAnswer)
178
- .then((splitDefinitions) => splitDefinitions.map((splitDefinition) => {
178
+ .then((splitDefinitions: string[]) => splitDefinitions.map((splitDefinition: string) => {
179
179
  return JSON.parse(splitDefinition);
180
180
  }));
181
181
  }
@@ -187,7 +187,7 @@ export class SplitsCacheInRedis extends AbstractSplitsCacheAsync {
187
187
  */
188
188
  getSplitNames(): Promise<string[]> {
189
189
  return this.redis.keys(this.keys.searchPatternForSplitKeys()).then(
190
- (listOfKeys) => listOfKeys.map(this.keys.extractKey)
190
+ (listOfKeys: string[]) => listOfKeys.map(this.keys.extractKey)
191
191
  );
192
192
  }
193
193
 
@@ -198,12 +198,12 @@ export class SplitsCacheInRedis extends AbstractSplitsCacheAsync {
198
198
  */
199
199
  getNamesByFlagSets(flagSets: string[]): Promise<Set<string>[]> {
200
200
  return this.redis.pipeline(flagSets.map(flagSet => ['smembers', this.keys.buildFlagSetKey(flagSet)])).exec()
201
- .then((results) => results.map(([e, value], index) => {
202
- if (e === null) return value;
201
+ .then((results: [Error | null, unknown][] | null) => results ? results.map(([e, value]: [Error | null, unknown], index: number) => {
202
+ if (e === null) return value as string;
203
203
 
204
204
  this.log.error(LOG_PREFIX + `Could not read result from get members of flag set ${flagSets[index]} due to an error: ${e}`);
205
- }))
206
- .then(namesByFlagSets => namesByFlagSets.map(namesByFlagSet => new Set(namesByFlagSet)));
205
+ }) : [])
206
+ .then((namesByFlagSets: (string | undefined)[]) => namesByFlagSets.map((namesByFlagSet: string | undefined) => new Set(namesByFlagSet)));
207
207
  }
208
208
 
209
209
  /**
@@ -226,7 +226,7 @@ export class SplitsCacheInRedis extends AbstractSplitsCacheAsync {
226
226
 
227
227
  return ttCount > 0;
228
228
  })
229
- .catch(e => {
229
+ .catch((e: unknown) => {
230
230
  this.log.error(LOG_PREFIX + `Could not validate traffic type existence of ${trafficType} due to an error: ${e}.`);
231
231
  // If there is an error, bypass the validation so the event can get tracked.
232
232
  return true;
@@ -251,7 +251,7 @@ export class SplitsCacheInRedis extends AbstractSplitsCacheAsync {
251
251
 
252
252
  const keys = names.map(name => this.keys.buildSplitKey(name));
253
253
  return this.redis.mget(...keys)
254
- .then(splitDefinitions => {
254
+ .then((splitDefinitions: (string | null)[]) => {
255
255
  const splits: Record<string, ISplit | null> = {};
256
256
  names.forEach((name, idx) => {
257
257
  const split = splitDefinitions[idx];
@@ -259,7 +259,7 @@ export class SplitsCacheInRedis extends AbstractSplitsCacheAsync {
259
259
  });
260
260
  return Promise.resolve(splits);
261
261
  })
262
- .catch(e => {
262
+ .catch((e: unknown) => {
263
263
  this.log.error(LOG_PREFIX + `Could not grab feature flags due to an error: ${e}.`);
264
264
  return Promise.reject(e);
265
265
  });
@@ -43,7 +43,7 @@ export class TelemetryCacheInRedis implements ITelemetryCacheAsync {
43
43
  * The returned promise rejects if redis operations fail.
44
44
  */
45
45
  popLatencies(): Promise<MultiMethodLatencies> {
46
- return this.redis.hgetall(this.keys.latencyPrefix).then(latencies => {
46
+ return this.redis.hgetall(this.keys.latencyPrefix).then((latencies: Record<string, string>) => {
47
47
 
48
48
  const result: MultiMethodLatencies = new Map();
49
49
 
@@ -83,7 +83,7 @@ export class TelemetryCacheInRedis implements ITelemetryCacheAsync {
83
83
  * The returned promise rejects if redis operations fail.
84
84
  */
85
85
  popExceptions(): Promise<MultiMethodExceptions> {
86
- return this.redis.hgetall(this.keys.exceptionPrefix).then(exceptions => {
86
+ return this.redis.hgetall(this.keys.exceptionPrefix).then((exceptions: Record<string, string>) => {
87
87
 
88
88
  const result: MultiMethodExceptions = new Map();
89
89
 
@@ -116,7 +116,7 @@ export class TelemetryCacheInRedis implements ITelemetryCacheAsync {
116
116
  * The returned promise rejects if redis operations fail.
117
117
  */
118
118
  popConfigs(): Promise<MultiConfigs> {
119
- return this.redis.hgetall(this.keys.initPrefix).then(configs => {
119
+ return this.redis.hgetall(this.keys.initPrefix).then((configs: Record<string, string>) => {
120
120
 
121
121
  const result: MultiConfigs = new Map();
122
122
 
@@ -38,14 +38,14 @@ export class UniqueKeysCacheInRedis extends UniqueKeysCacheInMemory implements I
38
38
  });
39
39
 
40
40
  this.clear();
41
- return this.redis.rpush(this.key, uniqueKeysArray)
42
- .then(data => {
41
+ return this.redis.rpush(this.key, ...uniqueKeysArray)
42
+ .then((data: number) => {
43
43
  // If this is the creation of the key on Redis, set the expiration for it in 3600 seconds.
44
44
  if (data === featureNames.length) {
45
45
  return this.redis.expire(this.key, TTL_REFRESH);
46
46
  }
47
47
  })
48
- .catch(err => {
48
+ .catch((err: unknown) => {
49
49
  this.log.error(`${LOG_PREFIX}Error in uniqueKeys pipeline: ${err}.`);
50
50
  return false;
51
51
  });
@@ -66,9 +66,9 @@ export class UniqueKeysCacheInRedis extends UniqueKeysCacheInMemory implements I
66
66
  * @param count - number of items to pop from the queue. If not provided or equal 0, all items will be popped.
67
67
  */
68
68
  popNRaw(count = 0): Promise<UniqueKeysItemSs[]> {
69
- return this.redis.lrange(this.key, 0, count - 1).then(uniqueKeyItems => {
69
+ return this.redis.lrange(this.key, 0, count - 1).then((uniqueKeyItems: string[]) => {
70
70
  return this.redis.ltrim(this.key, uniqueKeyItems.length, -1)
71
- .then(() => uniqueKeyItems.map(uniqueKeyItem => JSON.parse(uniqueKeyItem)));
71
+ .then(() => uniqueKeyItems.map((uniqueKeyItem: string) => JSON.parse(uniqueKeyItem)));
72
72
  });
73
73
  }
74
74