@splitsoftware/splitio-commons 2.10.2-rc.7 → 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 +5 -3
- package/cjs/sdkFactory/index.js +1 -1
- package/cjs/storages/inRedis/ImpressionCountsCacheInRedis.js +1 -1
- package/cjs/storages/inRedis/ImpressionsCacheInRedis.js +3 -1
- package/cjs/storages/inRedis/RedisAdapter.js +70 -35
- package/cjs/storages/inRedis/SplitsCacheInRedis.js +4 -4
- package/cjs/storages/inRedis/UniqueKeysCacheInRedis.js +2 -2
- package/esm/sdkFactory/index.js +1 -1
- package/esm/storages/inRedis/ImpressionCountsCacheInRedis.js +1 -1
- package/esm/storages/inRedis/ImpressionsCacheInRedis.js +3 -1
- package/esm/storages/inRedis/RedisAdapter.js +71 -36
- package/esm/storages/inRedis/SplitsCacheInRedis.js +4 -4
- package/esm/storages/inRedis/UniqueKeysCacheInRedis.js +3 -3
- package/package.json +3 -4
- package/src/sdkFactory/index.ts +1 -1
- package/src/storages/inRedis/EventsCacheInRedis.ts +3 -3
- package/src/storages/inRedis/ImpressionCountsCacheInRedis.ts +5 -5
- package/src/storages/inRedis/ImpressionsCacheInRedis.ts +4 -4
- package/src/storages/inRedis/RBSegmentsCacheInRedis.ts +5 -5
- package/src/storages/inRedis/RedisAdapter.ts +68 -33
- package/src/storages/inRedis/SegmentsCacheInRedis.ts +2 -2
- package/src/storages/inRedis/SplitsCacheInRedis.ts +22 -22
- package/src/storages/inRedis/TelemetryCacheInRedis.ts +3 -3
- package/src/storages/inRedis/UniqueKeysCacheInRedis.ts +5 -5
package/CHANGES.txt
CHANGED
|
@@ -1,6 +1,8 @@
|
|
|
1
|
-
2.
|
|
2
|
-
-
|
|
3
|
-
|
|
1
|
+
2.12.0 (February 20, 2026)
|
|
2
|
+
- Add support for ioredis v5
|
|
3
|
+
|
|
4
|
+
2.11.0 (January 28, 2026)
|
|
5
|
+
- Added functionality to provide metadata alongside SDK update and READY events. Read more in our docs.
|
|
4
6
|
|
|
5
7
|
2.10.1 (December 18, 2025)
|
|
6
8
|
- Bugfix - Handle `null` prerequisites properly.
|
package/cjs/sdkFactory/index.js
CHANGED
|
@@ -49,7 +49,7 @@ function sdkFactory(params) {
|
|
|
49
49
|
readiness.segments.emit(constants_2.SDK_SEGMENTS_ARRIVED);
|
|
50
50
|
},
|
|
51
51
|
onReadyFromCacheCb: function () {
|
|
52
|
-
readiness.splits.emit(constants_2.SDK_SPLITS_CACHE_LOADED
|
|
52
|
+
readiness.splits.emit(constants_2.SDK_SPLITS_CACHE_LOADED);
|
|
53
53
|
}
|
|
54
54
|
});
|
|
55
55
|
var fallbackTreatmentsCalculator = new fallbackTreatmentsCalculator_1.FallbackTreatmentsCalculator(settings.fallbackTreatments);
|
|
@@ -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
|
-
|
|
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 (
|
|
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
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
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
|
-
|
|
64
|
-
|
|
65
|
-
|
|
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 =
|
|
68
|
-
var
|
|
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(
|
|
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 =
|
|
107
|
-
|
|
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 =
|
|
142
|
+
var originalFn = _this.client[methodName];
|
|
112
143
|
// "First level wrapper" to handle the sync execution and wrap async, queueing later if applicable.
|
|
113
|
-
|
|
114
|
-
var
|
|
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 +
|
|
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 =
|
|
124
|
-
|
|
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
|
-
}(
|
|
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);
|
package/esm/sdkFactory/index.js
CHANGED
|
@@ -46,7 +46,7 @@ export function sdkFactory(params) {
|
|
|
46
46
|
readiness.segments.emit(SDK_SEGMENTS_ARRIVED);
|
|
47
47
|
},
|
|
48
48
|
onReadyFromCacheCb: function () {
|
|
49
|
-
readiness.splits.emit(SDK_SPLITS_CACHE_LOADED
|
|
49
|
+
readiness.splits.emit(SDK_SPLITS_CACHE_LOADED);
|
|
50
50
|
}
|
|
51
51
|
});
|
|
52
52
|
var fallbackTreatmentsCalculator = new FallbackTreatmentsCalculator(settings.fallbackTreatments);
|
|
@@ -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 {
|
|
2
|
-
|
|
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 (
|
|
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
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
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
|
-
|
|
61
|
-
|
|
62
|
-
|
|
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 =
|
|
65
|
-
var
|
|
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(
|
|
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 =
|
|
104
|
-
|
|
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 =
|
|
139
|
+
var originalFn = _this.client[methodName];
|
|
109
140
|
// "First level wrapper" to handle the sync execution and wrap async, queueing later if applicable.
|
|
110
|
-
|
|
111
|
-
var
|
|
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 +
|
|
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 =
|
|
121
|
-
|
|
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
|
-
}(
|
|
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.
|
|
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": "^
|
|
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",
|
package/src/sdkFactory/index.ts
CHANGED
|
@@ -57,7 +57,7 @@ export function sdkFactory(params: ISdkFactoryParams): SplitIO.ISDK | SplitIO.IA
|
|
|
57
57
|
readiness.segments.emit(SDK_SEGMENTS_ARRIVED);
|
|
58
58
|
},
|
|
59
59
|
onReadyFromCacheCb() {
|
|
60
|
-
readiness.splits.emit(SDK_SPLITS_CACHE_LOADED
|
|
60
|
+
readiness.splits.emit(SDK_SPLITS_CACHE_LOADED);
|
|
61
61
|
}
|
|
62
62
|
});
|
|
63
63
|
|
|
@@ -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
|
-
|
|
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<
|
|
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
|
|
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:
|
|
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
|
|
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(
|
|
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
|
|
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 =
|
|
125
|
-
|
|
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 =
|
|
166
|
+
const originalFn = this.client[methodName];
|
|
131
167
|
// "First level wrapper" to handle the sync execution and wrap async, queueing later if applicable.
|
|
132
|
-
|
|
133
|
-
const res = originalFn.apply(instance,
|
|
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
|
|
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 =
|
|
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,
|
|
14
|
-
return results.reduce((accum: string[], errValuePair: [Error | null,
|
|
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?:
|
|
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
|
|