@splitsoftware/splitio-commons 1.12.1-rc.2 → 1.12.1-rc.3
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGES.txt +10 -9
- package/cjs/sdkFactory/index.js +1 -1
- package/cjs/sdkManager/index.js +7 -4
- package/cjs/storages/inRedis/ImpressionCountsCacheInRedis.js +5 -1
- package/cjs/storages/inRedis/RedisAdapter.js +29 -12
- package/cjs/storages/inRedis/SegmentsCacheInRedis.js +2 -2
- package/cjs/storages/inRedis/SplitsCacheInRedis.js +5 -9
- package/cjs/utils/redis/RedisMock.js +7 -1
- package/esm/sdkFactory/index.js +1 -1
- package/esm/sdkManager/index.js +7 -4
- package/esm/storages/inRedis/ImpressionCountsCacheInRedis.js +5 -1
- package/esm/storages/inRedis/RedisAdapter.js +29 -12
- package/esm/storages/inRedis/SegmentsCacheInRedis.js +2 -2
- package/esm/storages/inRedis/SplitsCacheInRedis.js +5 -9
- package/esm/utils/redis/RedisMock.js +7 -1
- package/package.json +1 -1
- package/src/sdkFactory/index.ts +1 -1
- package/src/sdkFactory/types.ts +3 -7
- package/src/sdkManager/index.ts +10 -7
- package/src/storages/inRedis/ImpressionCountsCacheInRedis.ts +5 -1
- package/src/storages/inRedis/RedisAdapter.ts +35 -17
- package/src/storages/inRedis/SegmentsCacheInRedis.ts +2 -2
- package/src/storages/inRedis/SplitsCacheInRedis.ts +5 -9
- package/src/trackers/impressionObserver/utils.ts +1 -1
- package/src/utils/redis/RedisMock.ts +9 -1
- package/types/sdkFactory/types.d.ts +3 -3
- package/types/sdkManager/index.d.ts +2 -3
- package/types/storages/inRedis/RedisAdapter.d.ts +1 -2
- package/types/storages/inRedis/SegmentsCacheInRedis.d.ts +1 -1
- package/types/storages/inRedis/SplitsCacheInRedis.d.ts +2 -7
- package/types/trackers/impressionObserver/utils.d.ts +1 -1
- package/types/utils/redis/RedisMock.d.ts +1 -0
package/CHANGES.txt
CHANGED
|
@@ -1,17 +1,18 @@
|
|
|
1
1
|
1.12.0 (December XX, 2023)
|
|
2
2
|
- Added support for Flag Sets in "consumer" and "partial consumer" modes for Pluggable and Redis storages.
|
|
3
3
|
- Updated evaluation flow to log a warning when using flag sets that don't contain cached feature flags.
|
|
4
|
-
- Updated Redis adapter to
|
|
4
|
+
- Updated Redis adapter to handle timeouts and queueing of some missing commands: 'hincrby', 'popNRaw', and 'pipeline.exec'.
|
|
5
|
+
- Bugfixing - Fixed manager methods in consumer modes to return results in a promise when the SDK is not operational (not ready or destroyed).
|
|
5
6
|
|
|
6
7
|
1.11.0 (November 3, 2023)
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
8
|
+
- Added support for Flag Sets on the SDK, which enables grouping feature flags and interacting with the group rather than individually (more details in our documentation):
|
|
9
|
+
- Added new variations of the get treatment methods to support evaluating flags in given flag set/s.
|
|
10
|
+
- getTreatmentsByFlagSet and getTreatmentsByFlagSets
|
|
11
|
+
- getTreatmentsWithConfigByFlagSets and getTreatmentsWithConfigByFlagSets
|
|
12
|
+
- Added a new optional Split Filter configuration option. This allows the SDK and Split services to only synchronize the flags in the specified flag sets, avoiding unused or unwanted flags from being synced on the SDK instance, bringing all the benefits from a reduced payload.
|
|
13
|
+
- Note: Only applicable when the SDK is in charge of the rollout data synchronization. When not applicable, the SDK will log a warning on init.
|
|
14
|
+
- Added `sets` property to the `SplitView` object returned by the `split` and `splits` methods of the SDK manager to expose flag sets on flag views.
|
|
15
|
+
- Bugfixing - Fixed SDK key validation in NodeJS to ensure the SDK_READY_TIMED_OUT event is emitted when a client-side type SDK key is provided instead of a server-side one (Related to issue https://github.com/splitio/javascript-client/issues/768).
|
|
15
16
|
|
|
16
17
|
1.10.0 (October 20, 2023)
|
|
17
18
|
- Added `defaultTreatment` property to the `SplitView` object returned by the `split` and `splits` methods of the SDK manager (Related to issue https://github.com/splitio/javascript-commons/issues/225).
|
package/cjs/sdkFactory/index.js
CHANGED
|
@@ -66,7 +66,7 @@ function sdkFactory(params) {
|
|
|
66
66
|
ctx.signalListener = signalListener;
|
|
67
67
|
// SDK client and manager
|
|
68
68
|
var clientMethod = sdkClientMethodFactory(ctx);
|
|
69
|
-
var managerInstance = sdkManagerFactory(
|
|
69
|
+
var managerInstance = sdkManagerFactory(settings, storage.splits, sdkReadinessManager);
|
|
70
70
|
syncManager && syncManager.start();
|
|
71
71
|
signalListener && signalListener.start();
|
|
72
72
|
log.info(constants_1.NEW_FACTORY);
|
package/cjs/sdkManager/index.js
CHANGED
|
@@ -5,6 +5,7 @@ var objectAssign_1 = require("../utils/lang/objectAssign");
|
|
|
5
5
|
var thenable_1 = require("../utils/promise/thenable");
|
|
6
6
|
var lang_1 = require("../utils/lang");
|
|
7
7
|
var inputValidation_1 = require("../utils/inputValidation");
|
|
8
|
+
var utils_1 = require("../trackers/impressionObserver/utils");
|
|
8
9
|
var SPLIT_FN_LABEL = 'split';
|
|
9
10
|
var SPLITS_FN_LABEL = 'splits';
|
|
10
11
|
var NAMES_FN_LABEL = 'names';
|
|
@@ -41,8 +42,10 @@ function objectsToViews(splitObjects) {
|
|
|
41
42
|
});
|
|
42
43
|
return views;
|
|
43
44
|
}
|
|
44
|
-
function sdkManagerFactory(
|
|
45
|
+
function sdkManagerFactory(settings, splits, _a) {
|
|
45
46
|
var readinessManager = _a.readinessManager, sdkStatus = _a.sdkStatus;
|
|
47
|
+
var log = settings.log;
|
|
48
|
+
var isSync = (0, utils_1.isStorageSync)(settings);
|
|
46
49
|
return (0, objectAssign_1.objectAssign)(
|
|
47
50
|
// Proto-linkage of the readiness Event Emitter
|
|
48
51
|
Object.create(sdkStatus), {
|
|
@@ -52,7 +55,7 @@ function sdkManagerFactory(log, splits, _a) {
|
|
|
52
55
|
split: function (featureFlagName) {
|
|
53
56
|
var splitName = (0, inputValidation_1.validateSplit)(log, featureFlagName, SPLIT_FN_LABEL);
|
|
54
57
|
if (!(0, inputValidation_1.validateIfNotDestroyed)(log, readinessManager, SPLIT_FN_LABEL) || !(0, inputValidation_1.validateIfOperational)(log, readinessManager, SPLIT_FN_LABEL) || !splitName) {
|
|
55
|
-
return null;
|
|
58
|
+
return isSync ? null : Promise.resolve(null);
|
|
56
59
|
}
|
|
57
60
|
var split = splits.getSplit(splitName);
|
|
58
61
|
if ((0, thenable_1.thenable)(split)) {
|
|
@@ -69,7 +72,7 @@ function sdkManagerFactory(log, splits, _a) {
|
|
|
69
72
|
*/
|
|
70
73
|
splits: function () {
|
|
71
74
|
if (!(0, inputValidation_1.validateIfNotDestroyed)(log, readinessManager, SPLITS_FN_LABEL) || !(0, inputValidation_1.validateIfOperational)(log, readinessManager, SPLITS_FN_LABEL)) {
|
|
72
|
-
return [];
|
|
75
|
+
return isSync ? [] : Promise.resolve([]);
|
|
73
76
|
}
|
|
74
77
|
var currentSplits = splits.getAll();
|
|
75
78
|
return (0, thenable_1.thenable)(currentSplits) ?
|
|
@@ -81,7 +84,7 @@ function sdkManagerFactory(log, splits, _a) {
|
|
|
81
84
|
*/
|
|
82
85
|
names: function () {
|
|
83
86
|
if (!(0, inputValidation_1.validateIfNotDestroyed)(log, readinessManager, NAMES_FN_LABEL) || !(0, inputValidation_1.validateIfOperational)(log, readinessManager, NAMES_FN_LABEL)) {
|
|
84
|
-
return [];
|
|
87
|
+
return isSync ? [] : Promise.resolve([]);
|
|
85
88
|
}
|
|
86
89
|
var splitNames = splits.getSplitNames();
|
|
87
90
|
return (0, thenable_1.thenable)(splitNames) ?
|
|
@@ -23,7 +23,11 @@ var ImpressionCountsCacheInRedis = /** @class */ (function (_super) {
|
|
|
23
23
|
var keys = Object.keys(counts);
|
|
24
24
|
if (!keys.length)
|
|
25
25
|
return Promise.resolve(false);
|
|
26
|
-
|
|
26
|
+
var pipeline = this.redis.pipeline();
|
|
27
|
+
keys.forEach(function (key) {
|
|
28
|
+
pipeline.hincrby(_this.key, key, counts[key]);
|
|
29
|
+
});
|
|
30
|
+
return pipeline.exec()
|
|
27
31
|
.then(function (data) {
|
|
28
32
|
// If this is the creation of the key on Redis, set the expiration for it in 3600 seconds.
|
|
29
33
|
if (data.length && data.length === keys.length) {
|
|
@@ -9,7 +9,8 @@ var thenable_1 = require("../../utils/promise/thenable");
|
|
|
9
9
|
var timeout_1 = require("../../utils/promise/timeout");
|
|
10
10
|
var LOG_PREFIX = 'storage:redis-adapter: ';
|
|
11
11
|
// If we ever decide to fully wrap every method, there's a Commander.getBuiltinCommands from ioredis.
|
|
12
|
-
var METHODS_TO_PROMISE_WRAP = ['set', 'exec', 'del', 'get', 'keys', 'sadd', 'srem', 'sismember', 'smembers', 'incr', 'rpush', 'expire', 'mget', 'lrange', 'ltrim', 'hset', 'hincrby', 'popNRaw'
|
|
12
|
+
var METHODS_TO_PROMISE_WRAP = ['set', 'exec', 'del', 'get', 'keys', 'sadd', 'srem', 'sismember', 'smembers', 'incr', 'rpush', 'expire', 'mget', 'lrange', 'ltrim', 'hset', 'hincrby', 'popNRaw'];
|
|
13
|
+
var METHODS_TO_PROMISE_WRAP_EXEC = ['pipeline'];
|
|
13
14
|
// Not part of the settings since it'll vary on each storage. We should be removing storage specific logic from elsewhere.
|
|
14
15
|
var DEFAULT_OPTIONS = {
|
|
15
16
|
connectionTimeout: 10000,
|
|
@@ -27,6 +28,7 @@ var DEFAULT_LIBRARY_OPTIONS = {
|
|
|
27
28
|
var RedisAdapter = /** @class */ (function (_super) {
|
|
28
29
|
(0, tslib_1.__extends)(RedisAdapter, _super);
|
|
29
30
|
function RedisAdapter(log, storageSettings) {
|
|
31
|
+
if (storageSettings === void 0) { storageSettings = {}; }
|
|
30
32
|
var _this = this;
|
|
31
33
|
var options = RedisAdapter._defineOptions(storageSettings);
|
|
32
34
|
// Call the ioredis constructor
|
|
@@ -40,9 +42,6 @@ var RedisAdapter = /** @class */ (function (_super) {
|
|
|
40
42
|
_this._setDisconnectWrapper();
|
|
41
43
|
return _this;
|
|
42
44
|
}
|
|
43
|
-
RedisAdapter.prototype.pipelineExec = function (commands) {
|
|
44
|
-
return this.pipeline(commands).exec();
|
|
45
|
-
};
|
|
46
45
|
RedisAdapter.prototype._listenToEvents = function () {
|
|
47
46
|
var _this = this;
|
|
48
47
|
this.once('ready', function () {
|
|
@@ -61,13 +60,15 @@ var RedisAdapter = /** @class */ (function (_super) {
|
|
|
61
60
|
};
|
|
62
61
|
RedisAdapter.prototype._setTimeoutWrappers = function () {
|
|
63
62
|
var instance = this;
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
instance
|
|
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.
|
|
66
|
+
return function () {
|
|
67
67
|
var params = arguments;
|
|
68
|
+
var caller = this;
|
|
68
69
|
function commandWrapper() {
|
|
69
70
|
instance.log.debug(LOG_PREFIX + "Executing " + methodName + ".");
|
|
70
|
-
var result = originalMethod.apply(
|
|
71
|
+
var result = originalMethod.apply(caller, params);
|
|
71
72
|
if ((0, thenable_1.thenable)(result)) {
|
|
72
73
|
// For handling pending commands on disconnect, add to the set and remove once finished.
|
|
73
74
|
// On sync commands there's no need, only thenables.
|
|
@@ -86,10 +87,10 @@ var RedisAdapter = /** @class */ (function (_super) {
|
|
|
86
87
|
return result;
|
|
87
88
|
}
|
|
88
89
|
if (instance._notReadyCommandsQueue) {
|
|
89
|
-
return new Promise(function (
|
|
90
|
+
return new Promise(function (resolve, reject) {
|
|
90
91
|
instance._notReadyCommandsQueue.unshift({
|
|
91
|
-
resolve:
|
|
92
|
-
reject:
|
|
92
|
+
resolve: resolve,
|
|
93
|
+
reject: reject,
|
|
93
94
|
command: commandWrapper,
|
|
94
95
|
name: methodName.toUpperCase()
|
|
95
96
|
});
|
|
@@ -99,6 +100,22 @@ var RedisAdapter = /** @class */ (function (_super) {
|
|
|
99
100
|
return commandWrapper();
|
|
100
101
|
}
|
|
101
102
|
};
|
|
103
|
+
};
|
|
104
|
+
// Wrap regular async methods to track timeouts and queue when Redis is not yet executing commands.
|
|
105
|
+
METHODS_TO_PROMISE_WRAP.forEach(function (methodName) {
|
|
106
|
+
var originalFn = instance[methodName];
|
|
107
|
+
instance[methodName] = wrapCommand(originalFn, methodName);
|
|
108
|
+
});
|
|
109
|
+
// Special handling for pipeline~like methods. We need to wrap the async trigger, which is exec, but return the Pipeline right away.
|
|
110
|
+
METHODS_TO_PROMISE_WRAP_EXEC.forEach(function (methodName) {
|
|
111
|
+
var originalFn = instance[methodName];
|
|
112
|
+
// "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);
|
|
115
|
+
var originalExec = res.exec;
|
|
116
|
+
res.exec = wrapCommand(originalExec, methodName + '.exec').bind(res);
|
|
117
|
+
return res;
|
|
118
|
+
};
|
|
102
119
|
});
|
|
103
120
|
};
|
|
104
121
|
RedisAdapter.prototype._setDisconnectWrapper = function () {
|
|
@@ -160,7 +177,7 @@ var RedisAdapter = /** @class */ (function (_super) {
|
|
|
160
177
|
* Parses the options into what we care about.
|
|
161
178
|
*/
|
|
162
179
|
RedisAdapter._defineOptions = function (_a) {
|
|
163
|
-
var
|
|
180
|
+
var connectionTimeout = _a.connectionTimeout, operationTimeout = _a.operationTimeout, url = _a.url, host = _a.host, port = _a.port, db = _a.db, pass = _a.pass, tls = _a.tls;
|
|
164
181
|
var parsedOptions = {
|
|
165
182
|
connectionTimeout: connectionTimeout,
|
|
166
183
|
operationTimeout: operationTimeout,
|
|
@@ -54,9 +54,9 @@ var SegmentsCacheInRedis = /** @class */ (function () {
|
|
|
54
54
|
SegmentsCacheInRedis.prototype.getRegisteredSegments = function () {
|
|
55
55
|
return this.redis.smembers(this.keys.buildRegisteredSegmentsKey());
|
|
56
56
|
};
|
|
57
|
-
// @TODO remove
|
|
57
|
+
// @TODO remove or implement. It is not being used.
|
|
58
58
|
SegmentsCacheInRedis.prototype.clear = function () {
|
|
59
|
-
return
|
|
59
|
+
return Promise.resolve();
|
|
60
60
|
};
|
|
61
61
|
return SegmentsCacheInRedis;
|
|
62
62
|
}());
|
|
@@ -171,7 +171,7 @@ var SplitsCacheInRedis = /** @class */ (function (_super) {
|
|
|
171
171
|
SplitsCacheInRedis.prototype.getAll = function () {
|
|
172
172
|
var _this = this;
|
|
173
173
|
return this.redis.keys(this.keys.searchPatternForSplitKeys())
|
|
174
|
-
.then(function (listOfKeys) { return _this.redis.
|
|
174
|
+
.then(function (listOfKeys) { return _this.redis.pipeline(listOfKeys.map(function (k) { return ['get', k]; })).exec(); })
|
|
175
175
|
.then(processPipelineAnswer)
|
|
176
176
|
.then(function (splitDefinitions) { return splitDefinitions.map(function (splitDefinition) {
|
|
177
177
|
return JSON.parse(splitDefinition);
|
|
@@ -189,11 +189,11 @@ var SplitsCacheInRedis = /** @class */ (function (_super) {
|
|
|
189
189
|
/**
|
|
190
190
|
* Get list of feature flag names related to a given list of flag set names.
|
|
191
191
|
* The returned promise is resolved with the list of feature flag names per flag set,
|
|
192
|
-
* or rejected if the pipelined redis operation fails.
|
|
192
|
+
* or rejected if the pipelined redis operation fails (e.g., timeout).
|
|
193
193
|
*/
|
|
194
194
|
SplitsCacheInRedis.prototype.getNamesByFlagSets = function (flagSets) {
|
|
195
195
|
var _this = this;
|
|
196
|
-
return this.redis.
|
|
196
|
+
return this.redis.pipeline(flagSets.map(function (flagSet) { return ['smembers', _this.keys.buildFlagSetKey(flagSet)]; })).exec()
|
|
197
197
|
.then(function (results) { return results.map(function (_a, index) {
|
|
198
198
|
var e = _a[0], value = _a[1];
|
|
199
199
|
if (e === null)
|
|
@@ -228,13 +228,9 @@ var SplitsCacheInRedis = /** @class */ (function (_super) {
|
|
|
228
228
|
return true;
|
|
229
229
|
});
|
|
230
230
|
};
|
|
231
|
-
|
|
232
|
-
* Delete everything in the current database.
|
|
233
|
-
*
|
|
234
|
-
* @NOTE documentation says it never fails.
|
|
235
|
-
*/
|
|
231
|
+
// @TODO remove or implement. It is not being used.
|
|
236
232
|
SplitsCacheInRedis.prototype.clear = function () {
|
|
237
|
-
return
|
|
233
|
+
return Promise.resolve();
|
|
238
234
|
};
|
|
239
235
|
/**
|
|
240
236
|
* Fetches multiple splits definitions.
|
|
@@ -9,16 +9,22 @@ function asyncFunction(data) {
|
|
|
9
9
|
return Promise.resolve(data);
|
|
10
10
|
}
|
|
11
11
|
var IDENTITY_METHODS = [];
|
|
12
|
-
var ASYNC_METHODS = ['rpush', 'hincrby'
|
|
12
|
+
var ASYNC_METHODS = ['rpush', 'hincrby'];
|
|
13
|
+
var PIPELINE_METHODS = ['rpush', 'hincrby'];
|
|
13
14
|
var RedisMock = /** @class */ (function () {
|
|
14
15
|
function RedisMock() {
|
|
15
16
|
var _this = this;
|
|
17
|
+
this.pipelineMethods = { exec: jest.fn(asyncFunction) };
|
|
16
18
|
IDENTITY_METHODS.forEach(function (method) {
|
|
17
19
|
_this[method] = jest.fn(identityFunction);
|
|
18
20
|
});
|
|
19
21
|
ASYNC_METHODS.forEach(function (method) {
|
|
20
22
|
_this[method] = jest.fn(asyncFunction);
|
|
21
23
|
});
|
|
24
|
+
PIPELINE_METHODS.forEach(function (method) {
|
|
25
|
+
_this.pipelineMethods[method] = _this[method];
|
|
26
|
+
});
|
|
27
|
+
this.pipeline = jest.fn(function () { return _this.pipelineMethods; });
|
|
22
28
|
}
|
|
23
29
|
return RedisMock;
|
|
24
30
|
}());
|
package/esm/sdkFactory/index.js
CHANGED
|
@@ -63,7 +63,7 @@ export function sdkFactory(params) {
|
|
|
63
63
|
ctx.signalListener = signalListener;
|
|
64
64
|
// SDK client and manager
|
|
65
65
|
var clientMethod = sdkClientMethodFactory(ctx);
|
|
66
|
-
var managerInstance = sdkManagerFactory(
|
|
66
|
+
var managerInstance = sdkManagerFactory(settings, storage.splits, sdkReadinessManager);
|
|
67
67
|
syncManager && syncManager.start();
|
|
68
68
|
signalListener && signalListener.start();
|
|
69
69
|
log.info(NEW_FACTORY);
|
package/esm/sdkManager/index.js
CHANGED
|
@@ -2,6 +2,7 @@ import { objectAssign } from '../utils/lang/objectAssign';
|
|
|
2
2
|
import { thenable } from '../utils/promise/thenable';
|
|
3
3
|
import { find } from '../utils/lang';
|
|
4
4
|
import { validateSplit, validateSplitExistence, validateIfNotDestroyed, validateIfOperational } from '../utils/inputValidation';
|
|
5
|
+
import { isStorageSync } from '../trackers/impressionObserver/utils';
|
|
5
6
|
var SPLIT_FN_LABEL = 'split';
|
|
6
7
|
var SPLITS_FN_LABEL = 'splits';
|
|
7
8
|
var NAMES_FN_LABEL = 'names';
|
|
@@ -38,8 +39,10 @@ function objectsToViews(splitObjects) {
|
|
|
38
39
|
});
|
|
39
40
|
return views;
|
|
40
41
|
}
|
|
41
|
-
export function sdkManagerFactory(
|
|
42
|
+
export function sdkManagerFactory(settings, splits, _a) {
|
|
42
43
|
var readinessManager = _a.readinessManager, sdkStatus = _a.sdkStatus;
|
|
44
|
+
var log = settings.log;
|
|
45
|
+
var isSync = isStorageSync(settings);
|
|
43
46
|
return objectAssign(
|
|
44
47
|
// Proto-linkage of the readiness Event Emitter
|
|
45
48
|
Object.create(sdkStatus), {
|
|
@@ -49,7 +52,7 @@ export function sdkManagerFactory(log, splits, _a) {
|
|
|
49
52
|
split: function (featureFlagName) {
|
|
50
53
|
var splitName = validateSplit(log, featureFlagName, SPLIT_FN_LABEL);
|
|
51
54
|
if (!validateIfNotDestroyed(log, readinessManager, SPLIT_FN_LABEL) || !validateIfOperational(log, readinessManager, SPLIT_FN_LABEL) || !splitName) {
|
|
52
|
-
return null;
|
|
55
|
+
return isSync ? null : Promise.resolve(null);
|
|
53
56
|
}
|
|
54
57
|
var split = splits.getSplit(splitName);
|
|
55
58
|
if (thenable(split)) {
|
|
@@ -66,7 +69,7 @@ export function sdkManagerFactory(log, splits, _a) {
|
|
|
66
69
|
*/
|
|
67
70
|
splits: function () {
|
|
68
71
|
if (!validateIfNotDestroyed(log, readinessManager, SPLITS_FN_LABEL) || !validateIfOperational(log, readinessManager, SPLITS_FN_LABEL)) {
|
|
69
|
-
return [];
|
|
72
|
+
return isSync ? [] : Promise.resolve([]);
|
|
70
73
|
}
|
|
71
74
|
var currentSplits = splits.getAll();
|
|
72
75
|
return thenable(currentSplits) ?
|
|
@@ -78,7 +81,7 @@ export function sdkManagerFactory(log, splits, _a) {
|
|
|
78
81
|
*/
|
|
79
82
|
names: function () {
|
|
80
83
|
if (!validateIfNotDestroyed(log, readinessManager, NAMES_FN_LABEL) || !validateIfOperational(log, readinessManager, NAMES_FN_LABEL)) {
|
|
81
|
-
return [];
|
|
84
|
+
return isSync ? [] : Promise.resolve([]);
|
|
82
85
|
}
|
|
83
86
|
var splitNames = splits.getSplitNames();
|
|
84
87
|
return thenable(splitNames) ?
|
|
@@ -20,7 +20,11 @@ var ImpressionCountsCacheInRedis = /** @class */ (function (_super) {
|
|
|
20
20
|
var keys = Object.keys(counts);
|
|
21
21
|
if (!keys.length)
|
|
22
22
|
return Promise.resolve(false);
|
|
23
|
-
|
|
23
|
+
var pipeline = this.redis.pipeline();
|
|
24
|
+
keys.forEach(function (key) {
|
|
25
|
+
pipeline.hincrby(_this.key, key, counts[key]);
|
|
26
|
+
});
|
|
27
|
+
return pipeline.exec()
|
|
24
28
|
.then(function (data) {
|
|
25
29
|
// If this is the creation of the key on Redis, set the expiration for it in 3600 seconds.
|
|
26
30
|
if (data.length && data.length === keys.length) {
|
|
@@ -6,7 +6,8 @@ import { thenable } from '../../utils/promise/thenable';
|
|
|
6
6
|
import { timeout } from '../../utils/promise/timeout';
|
|
7
7
|
var LOG_PREFIX = 'storage:redis-adapter: ';
|
|
8
8
|
// If we ever decide to fully wrap every method, there's a Commander.getBuiltinCommands from ioredis.
|
|
9
|
-
var METHODS_TO_PROMISE_WRAP = ['set', 'exec', 'del', 'get', 'keys', 'sadd', 'srem', 'sismember', 'smembers', 'incr', 'rpush', 'expire', 'mget', 'lrange', 'ltrim', 'hset', 'hincrby', 'popNRaw'
|
|
9
|
+
var METHODS_TO_PROMISE_WRAP = ['set', 'exec', 'del', 'get', 'keys', 'sadd', 'srem', 'sismember', 'smembers', 'incr', 'rpush', 'expire', 'mget', 'lrange', 'ltrim', 'hset', 'hincrby', 'popNRaw'];
|
|
10
|
+
var METHODS_TO_PROMISE_WRAP_EXEC = ['pipeline'];
|
|
10
11
|
// Not part of the settings since it'll vary on each storage. We should be removing storage specific logic from elsewhere.
|
|
11
12
|
var DEFAULT_OPTIONS = {
|
|
12
13
|
connectionTimeout: 10000,
|
|
@@ -24,6 +25,7 @@ var DEFAULT_LIBRARY_OPTIONS = {
|
|
|
24
25
|
var RedisAdapter = /** @class */ (function (_super) {
|
|
25
26
|
__extends(RedisAdapter, _super);
|
|
26
27
|
function RedisAdapter(log, storageSettings) {
|
|
28
|
+
if (storageSettings === void 0) { storageSettings = {}; }
|
|
27
29
|
var _this = this;
|
|
28
30
|
var options = RedisAdapter._defineOptions(storageSettings);
|
|
29
31
|
// Call the ioredis constructor
|
|
@@ -37,9 +39,6 @@ var RedisAdapter = /** @class */ (function (_super) {
|
|
|
37
39
|
_this._setDisconnectWrapper();
|
|
38
40
|
return _this;
|
|
39
41
|
}
|
|
40
|
-
RedisAdapter.prototype.pipelineExec = function (commands) {
|
|
41
|
-
return this.pipeline(commands).exec();
|
|
42
|
-
};
|
|
43
42
|
RedisAdapter.prototype._listenToEvents = function () {
|
|
44
43
|
var _this = this;
|
|
45
44
|
this.once('ready', function () {
|
|
@@ -58,13 +57,15 @@ var RedisAdapter = /** @class */ (function (_super) {
|
|
|
58
57
|
};
|
|
59
58
|
RedisAdapter.prototype._setTimeoutWrappers = function () {
|
|
60
59
|
var instance = this;
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
instance
|
|
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.
|
|
63
|
+
return function () {
|
|
64
64
|
var params = arguments;
|
|
65
|
+
var caller = this;
|
|
65
66
|
function commandWrapper() {
|
|
66
67
|
instance.log.debug(LOG_PREFIX + "Executing " + methodName + ".");
|
|
67
|
-
var result = originalMethod.apply(
|
|
68
|
+
var result = originalMethod.apply(caller, params);
|
|
68
69
|
if (thenable(result)) {
|
|
69
70
|
// For handling pending commands on disconnect, add to the set and remove once finished.
|
|
70
71
|
// On sync commands there's no need, only thenables.
|
|
@@ -83,10 +84,10 @@ var RedisAdapter = /** @class */ (function (_super) {
|
|
|
83
84
|
return result;
|
|
84
85
|
}
|
|
85
86
|
if (instance._notReadyCommandsQueue) {
|
|
86
|
-
return new Promise(function (
|
|
87
|
+
return new Promise(function (resolve, reject) {
|
|
87
88
|
instance._notReadyCommandsQueue.unshift({
|
|
88
|
-
resolve:
|
|
89
|
-
reject:
|
|
89
|
+
resolve: resolve,
|
|
90
|
+
reject: reject,
|
|
90
91
|
command: commandWrapper,
|
|
91
92
|
name: methodName.toUpperCase()
|
|
92
93
|
});
|
|
@@ -96,6 +97,22 @@ var RedisAdapter = /** @class */ (function (_super) {
|
|
|
96
97
|
return commandWrapper();
|
|
97
98
|
}
|
|
98
99
|
};
|
|
100
|
+
};
|
|
101
|
+
// Wrap regular async methods to track timeouts and queue when Redis is not yet executing commands.
|
|
102
|
+
METHODS_TO_PROMISE_WRAP.forEach(function (methodName) {
|
|
103
|
+
var originalFn = instance[methodName];
|
|
104
|
+
instance[methodName] = wrapCommand(originalFn, methodName);
|
|
105
|
+
});
|
|
106
|
+
// Special handling for pipeline~like methods. We need to wrap the async trigger, which is exec, but return the Pipeline right away.
|
|
107
|
+
METHODS_TO_PROMISE_WRAP_EXEC.forEach(function (methodName) {
|
|
108
|
+
var originalFn = instance[methodName];
|
|
109
|
+
// "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);
|
|
112
|
+
var originalExec = res.exec;
|
|
113
|
+
res.exec = wrapCommand(originalExec, methodName + '.exec').bind(res);
|
|
114
|
+
return res;
|
|
115
|
+
};
|
|
99
116
|
});
|
|
100
117
|
};
|
|
101
118
|
RedisAdapter.prototype._setDisconnectWrapper = function () {
|
|
@@ -157,7 +174,7 @@ var RedisAdapter = /** @class */ (function (_super) {
|
|
|
157
174
|
* Parses the options into what we care about.
|
|
158
175
|
*/
|
|
159
176
|
RedisAdapter._defineOptions = function (_a) {
|
|
160
|
-
var
|
|
177
|
+
var connectionTimeout = _a.connectionTimeout, operationTimeout = _a.operationTimeout, url = _a.url, host = _a.host, port = _a.port, db = _a.db, pass = _a.pass, tls = _a.tls;
|
|
161
178
|
var parsedOptions = {
|
|
162
179
|
connectionTimeout: connectionTimeout,
|
|
163
180
|
operationTimeout: operationTimeout,
|
|
@@ -51,9 +51,9 @@ var SegmentsCacheInRedis = /** @class */ (function () {
|
|
|
51
51
|
SegmentsCacheInRedis.prototype.getRegisteredSegments = function () {
|
|
52
52
|
return this.redis.smembers(this.keys.buildRegisteredSegmentsKey());
|
|
53
53
|
};
|
|
54
|
-
// @TODO remove
|
|
54
|
+
// @TODO remove or implement. It is not being used.
|
|
55
55
|
SegmentsCacheInRedis.prototype.clear = function () {
|
|
56
|
-
return
|
|
56
|
+
return Promise.resolve();
|
|
57
57
|
};
|
|
58
58
|
return SegmentsCacheInRedis;
|
|
59
59
|
}());
|
|
@@ -168,7 +168,7 @@ var SplitsCacheInRedis = /** @class */ (function (_super) {
|
|
|
168
168
|
SplitsCacheInRedis.prototype.getAll = function () {
|
|
169
169
|
var _this = this;
|
|
170
170
|
return this.redis.keys(this.keys.searchPatternForSplitKeys())
|
|
171
|
-
.then(function (listOfKeys) { return _this.redis.
|
|
171
|
+
.then(function (listOfKeys) { return _this.redis.pipeline(listOfKeys.map(function (k) { return ['get', k]; })).exec(); })
|
|
172
172
|
.then(processPipelineAnswer)
|
|
173
173
|
.then(function (splitDefinitions) { return splitDefinitions.map(function (splitDefinition) {
|
|
174
174
|
return JSON.parse(splitDefinition);
|
|
@@ -186,11 +186,11 @@ var SplitsCacheInRedis = /** @class */ (function (_super) {
|
|
|
186
186
|
/**
|
|
187
187
|
* Get list of feature flag names related to a given list of flag set names.
|
|
188
188
|
* The returned promise is resolved with the list of feature flag names per flag set,
|
|
189
|
-
* or rejected if the pipelined redis operation fails.
|
|
189
|
+
* or rejected if the pipelined redis operation fails (e.g., timeout).
|
|
190
190
|
*/
|
|
191
191
|
SplitsCacheInRedis.prototype.getNamesByFlagSets = function (flagSets) {
|
|
192
192
|
var _this = this;
|
|
193
|
-
return this.redis.
|
|
193
|
+
return this.redis.pipeline(flagSets.map(function (flagSet) { return ['smembers', _this.keys.buildFlagSetKey(flagSet)]; })).exec()
|
|
194
194
|
.then(function (results) { return results.map(function (_a, index) {
|
|
195
195
|
var e = _a[0], value = _a[1];
|
|
196
196
|
if (e === null)
|
|
@@ -225,13 +225,9 @@ var SplitsCacheInRedis = /** @class */ (function (_super) {
|
|
|
225
225
|
return true;
|
|
226
226
|
});
|
|
227
227
|
};
|
|
228
|
-
|
|
229
|
-
* Delete everything in the current database.
|
|
230
|
-
*
|
|
231
|
-
* @NOTE documentation says it never fails.
|
|
232
|
-
*/
|
|
228
|
+
// @TODO remove or implement. It is not being used.
|
|
233
229
|
SplitsCacheInRedis.prototype.clear = function () {
|
|
234
|
-
return
|
|
230
|
+
return Promise.resolve();
|
|
235
231
|
};
|
|
236
232
|
/**
|
|
237
233
|
* Fetches multiple splits definitions.
|
|
@@ -6,16 +6,22 @@ function asyncFunction(data) {
|
|
|
6
6
|
return Promise.resolve(data);
|
|
7
7
|
}
|
|
8
8
|
var IDENTITY_METHODS = [];
|
|
9
|
-
var ASYNC_METHODS = ['rpush', 'hincrby'
|
|
9
|
+
var ASYNC_METHODS = ['rpush', 'hincrby'];
|
|
10
|
+
var PIPELINE_METHODS = ['rpush', 'hincrby'];
|
|
10
11
|
var RedisMock = /** @class */ (function () {
|
|
11
12
|
function RedisMock() {
|
|
12
13
|
var _this = this;
|
|
14
|
+
this.pipelineMethods = { exec: jest.fn(asyncFunction) };
|
|
13
15
|
IDENTITY_METHODS.forEach(function (method) {
|
|
14
16
|
_this[method] = jest.fn(identityFunction);
|
|
15
17
|
});
|
|
16
18
|
ASYNC_METHODS.forEach(function (method) {
|
|
17
19
|
_this[method] = jest.fn(asyncFunction);
|
|
18
20
|
});
|
|
21
|
+
PIPELINE_METHODS.forEach(function (method) {
|
|
22
|
+
_this.pipelineMethods[method] = _this[method];
|
|
23
|
+
});
|
|
24
|
+
this.pipeline = jest.fn(function () { return _this.pipelineMethods; });
|
|
19
25
|
}
|
|
20
26
|
return RedisMock;
|
|
21
27
|
}());
|
package/package.json
CHANGED
package/src/sdkFactory/index.ts
CHANGED
|
@@ -83,7 +83,7 @@ export function sdkFactory(params: ISdkFactoryParams): SplitIO.ICsSDK | SplitIO.
|
|
|
83
83
|
|
|
84
84
|
// SDK client and manager
|
|
85
85
|
const clientMethod = sdkClientMethodFactory(ctx);
|
|
86
|
-
const managerInstance = sdkManagerFactory(
|
|
86
|
+
const managerInstance = sdkManagerFactory(settings, storage.splits, sdkReadinessManager);
|
|
87
87
|
|
|
88
88
|
syncManager && syncManager.start();
|
|
89
89
|
signalListener && signalListener.start();
|
package/src/sdkFactory/types.ts
CHANGED
|
@@ -1,9 +1,9 @@
|
|
|
1
1
|
import { IIntegrationManager, IIntegrationFactoryParams } from '../integrations/types';
|
|
2
2
|
import { ISignalListener } from '../listeners/types';
|
|
3
|
-
import { ILogger } from '../logger/types';
|
|
4
3
|
import { IReadinessManager, ISdkReadinessManager } from '../readiness/types';
|
|
4
|
+
import type { sdkManagerFactory } from '../sdkManager';
|
|
5
5
|
import { IFetch, ISplitApi, IEventSourceConstructor } from '../services/types';
|
|
6
|
-
import { IStorageAsync, IStorageSync,
|
|
6
|
+
import { IStorageAsync, IStorageSync, IStorageFactoryParams } from '../storages/types';
|
|
7
7
|
import { ISyncManager } from '../sync/types';
|
|
8
8
|
import { IImpressionObserver } from '../trackers/impressionObserver/types';
|
|
9
9
|
import { IImpressionsTracker, IEventTracker, ITelemetryTracker, IFilterAdapter, IUniqueKeysTracker } from '../trackers/types';
|
|
@@ -87,11 +87,7 @@ export interface ISdkFactoryParams {
|
|
|
87
87
|
syncManagerFactory?: (params: ISdkFactoryContextSync) => ISyncManager,
|
|
88
88
|
|
|
89
89
|
// Sdk manager factory
|
|
90
|
-
sdkManagerFactory:
|
|
91
|
-
log: ILogger,
|
|
92
|
-
splits: ISplitsCacheSync | ISplitsCacheAsync,
|
|
93
|
-
sdkReadinessManager: ISdkReadinessManager
|
|
94
|
-
) => SplitIO.IManager | SplitIO.IAsyncManager,
|
|
90
|
+
sdkManagerFactory: typeof sdkManagerFactory,
|
|
95
91
|
|
|
96
92
|
// Sdk client method factory (ISDK::client method).
|
|
97
93
|
// It Allows to distinguish SDK clients with the client-side API (`ICsSDK`) or server-side API (`ISDK` or `IAsyncSDK`).
|
package/src/sdkManager/index.ts
CHANGED
|
@@ -5,8 +5,8 @@ import { validateSplit, validateSplitExistence, validateIfNotDestroyed, validate
|
|
|
5
5
|
import { ISplitsCacheAsync, ISplitsCacheSync } from '../storages/types';
|
|
6
6
|
import { ISdkReadinessManager } from '../readiness/types';
|
|
7
7
|
import { ISplit } from '../dtos/types';
|
|
8
|
-
import { SplitIO } from '../types';
|
|
9
|
-
import {
|
|
8
|
+
import { ISettings, SplitIO } from '../types';
|
|
9
|
+
import { isStorageSync } from '../trackers/impressionObserver/utils';
|
|
10
10
|
|
|
11
11
|
const SPLIT_FN_LABEL = 'split';
|
|
12
12
|
const SPLITS_FN_LABEL = 'splits';
|
|
@@ -49,11 +49,14 @@ function objectsToViews(splitObjects: ISplit[]) {
|
|
|
49
49
|
}
|
|
50
50
|
|
|
51
51
|
export function sdkManagerFactory<TSplitCache extends ISplitsCacheSync | ISplitsCacheAsync>(
|
|
52
|
-
|
|
52
|
+
settings: Pick<ISettings, 'log' | 'mode'>,
|
|
53
53
|
splits: TSplitCache,
|
|
54
|
-
{ readinessManager, sdkStatus }: ISdkReadinessManager
|
|
54
|
+
{ readinessManager, sdkStatus }: ISdkReadinessManager,
|
|
55
55
|
): TSplitCache extends ISplitsCacheAsync ? SplitIO.IAsyncManager : SplitIO.IManager {
|
|
56
56
|
|
|
57
|
+
const log = settings.log;
|
|
58
|
+
const isSync = isStorageSync(settings);
|
|
59
|
+
|
|
57
60
|
return objectAssign(
|
|
58
61
|
// Proto-linkage of the readiness Event Emitter
|
|
59
62
|
Object.create(sdkStatus),
|
|
@@ -64,7 +67,7 @@ export function sdkManagerFactory<TSplitCache extends ISplitsCacheSync | ISplits
|
|
|
64
67
|
split(featureFlagName: string) {
|
|
65
68
|
const splitName = validateSplit(log, featureFlagName, SPLIT_FN_LABEL);
|
|
66
69
|
if (!validateIfNotDestroyed(log, readinessManager, SPLIT_FN_LABEL) || !validateIfOperational(log, readinessManager, SPLIT_FN_LABEL) || !splitName) {
|
|
67
|
-
return null;
|
|
70
|
+
return isSync ? null : Promise.resolve(null);
|
|
68
71
|
}
|
|
69
72
|
|
|
70
73
|
const split = splits.getSplit(splitName);
|
|
@@ -85,7 +88,7 @@ export function sdkManagerFactory<TSplitCache extends ISplitsCacheSync | ISplits
|
|
|
85
88
|
*/
|
|
86
89
|
splits() {
|
|
87
90
|
if (!validateIfNotDestroyed(log, readinessManager, SPLITS_FN_LABEL) || !validateIfOperational(log, readinessManager, SPLITS_FN_LABEL)) {
|
|
88
|
-
return [];
|
|
91
|
+
return isSync ? [] : Promise.resolve([]);
|
|
89
92
|
}
|
|
90
93
|
const currentSplits = splits.getAll();
|
|
91
94
|
|
|
@@ -98,7 +101,7 @@ export function sdkManagerFactory<TSplitCache extends ISplitsCacheSync | ISplits
|
|
|
98
101
|
*/
|
|
99
102
|
names() {
|
|
100
103
|
if (!validateIfNotDestroyed(log, readinessManager, NAMES_FN_LABEL) || !validateIfOperational(log, readinessManager, NAMES_FN_LABEL)) {
|
|
101
|
-
return [];
|
|
104
|
+
return isSync ? [] : Promise.resolve([]);
|
|
102
105
|
}
|
|
103
106
|
const splitNames = splits.getSplitNames();
|
|
104
107
|
|
|
@@ -27,7 +27,11 @@ export class ImpressionCountsCacheInRedis extends ImpressionCountsCacheInMemory
|
|
|
27
27
|
const keys = Object.keys(counts);
|
|
28
28
|
if (!keys.length) return Promise.resolve(false);
|
|
29
29
|
|
|
30
|
-
|
|
30
|
+
const pipeline = this.redis.pipeline();
|
|
31
|
+
keys.forEach(key => {
|
|
32
|
+
pipeline.hincrby(this.key, key, counts[key]);
|
|
33
|
+
});
|
|
34
|
+
return pipeline.exec()
|
|
31
35
|
.then(data => {
|
|
32
36
|
// If this is the creation of the key on Redis, set the expiration for it in 3600 seconds.
|
|
33
37
|
if (data.length && data.length === keys.length) {
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import ioredis from 'ioredis';
|
|
1
|
+
import ioredis, { Pipeline } from 'ioredis';
|
|
2
2
|
import { ILogger } from '../../logger/types';
|
|
3
3
|
import { merge, isString } from '../../utils/lang';
|
|
4
4
|
import { _Set, setToArray, ISet } from '../../utils/lang/sets';
|
|
@@ -8,7 +8,8 @@ import { timeout } from '../../utils/promise/timeout';
|
|
|
8
8
|
const LOG_PREFIX = 'storage:redis-adapter: ';
|
|
9
9
|
|
|
10
10
|
// If we ever decide to fully wrap every method, there's a Commander.getBuiltinCommands from ioredis.
|
|
11
|
-
const METHODS_TO_PROMISE_WRAP = ['set', 'exec', 'del', 'get', 'keys', 'sadd', 'srem', 'sismember', 'smembers', 'incr', 'rpush', 'expire', 'mget', 'lrange', 'ltrim', 'hset', 'hincrby', 'popNRaw'
|
|
11
|
+
const METHODS_TO_PROMISE_WRAP = ['set', 'exec', 'del', 'get', 'keys', 'sadd', 'srem', 'sismember', 'smembers', 'incr', 'rpush', 'expire', 'mget', 'lrange', 'ltrim', 'hset', 'hincrby', 'popNRaw'];
|
|
12
|
+
const METHODS_TO_PROMISE_WRAP_EXEC = ['pipeline'];
|
|
12
13
|
|
|
13
14
|
// Not part of the settings since it'll vary on each storage. We should be removing storage specific logic from elsewhere.
|
|
14
15
|
const DEFAULT_OPTIONS = {
|
|
@@ -38,7 +39,7 @@ export class RedisAdapter extends ioredis {
|
|
|
38
39
|
private _notReadyCommandsQueue?: IRedisCommand[];
|
|
39
40
|
private _runningCommands: ISet<Promise<any>>;
|
|
40
41
|
|
|
41
|
-
constructor(log: ILogger, storageSettings
|
|
42
|
+
constructor(log: ILogger, storageSettings: Record<string, any> = {}) {
|
|
42
43
|
const options = RedisAdapter._defineOptions(storageSettings);
|
|
43
44
|
// Call the ioredis constructor
|
|
44
45
|
super(...RedisAdapter._defineLibrarySettings(options));
|
|
@@ -52,10 +53,6 @@ export class RedisAdapter extends ioredis {
|
|
|
52
53
|
this._setDisconnectWrapper();
|
|
53
54
|
}
|
|
54
55
|
|
|
55
|
-
pipelineExec(commands?: (string | number)[][]): Promise<Array<[Error | null, any]>> {
|
|
56
|
-
return this.pipeline(commands as string[][]).exec();
|
|
57
|
-
}
|
|
58
|
-
|
|
59
56
|
_listenToEvents() {
|
|
60
57
|
this.once('ready', () => {
|
|
61
58
|
const commandsCount = this._notReadyCommandsQueue ? this._notReadyCommandsQueue.length : 0;
|
|
@@ -76,21 +73,22 @@ export class RedisAdapter extends ioredis {
|
|
|
76
73
|
_setTimeoutWrappers() {
|
|
77
74
|
const instance: Record<string, any> = this;
|
|
78
75
|
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
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) {
|
|
83
80
|
const params = arguments;
|
|
81
|
+
const caller = this;
|
|
84
82
|
|
|
85
83
|
function commandWrapper() {
|
|
86
84
|
instance.log.debug(`${LOG_PREFIX}Executing ${methodName}.`);
|
|
87
|
-
const result = originalMethod.apply(
|
|
85
|
+
const result = originalMethod.apply(caller, params);
|
|
88
86
|
|
|
89
87
|
if (thenable(result)) {
|
|
90
88
|
// For handling pending commands on disconnect, add to the set and remove once finished.
|
|
91
89
|
// On sync commands there's no need, only thenables.
|
|
92
90
|
instance._runningCommands.add(result);
|
|
93
|
-
const cleanUpRunningCommandsCb = ()
|
|
91
|
+
const cleanUpRunningCommandsCb = function () {
|
|
94
92
|
instance._runningCommands.delete(result);
|
|
95
93
|
};
|
|
96
94
|
// Both success and error remove from queue.
|
|
@@ -107,10 +105,10 @@ export class RedisAdapter extends ioredis {
|
|
|
107
105
|
}
|
|
108
106
|
|
|
109
107
|
if (instance._notReadyCommandsQueue) {
|
|
110
|
-
return new Promise((
|
|
108
|
+
return new Promise((resolve, reject) => {
|
|
111
109
|
instance._notReadyCommandsQueue.unshift({
|
|
112
|
-
resolve
|
|
113
|
-
reject
|
|
110
|
+
resolve,
|
|
111
|
+
reject,
|
|
114
112
|
command: commandWrapper,
|
|
115
113
|
name: methodName.toUpperCase()
|
|
116
114
|
});
|
|
@@ -119,6 +117,26 @@ export class RedisAdapter extends ioredis {
|
|
|
119
117
|
return commandWrapper();
|
|
120
118
|
}
|
|
121
119
|
};
|
|
120
|
+
};
|
|
121
|
+
|
|
122
|
+
// Wrap regular async methods to track timeouts and queue when Redis is not yet executing commands.
|
|
123
|
+
METHODS_TO_PROMISE_WRAP.forEach(methodName => {
|
|
124
|
+
const originalFn = instance[methodName];
|
|
125
|
+
instance[methodName] = wrapCommand(originalFn, methodName);
|
|
126
|
+
});
|
|
127
|
+
|
|
128
|
+
// Special handling for pipeline~like methods. We need to wrap the async trigger, which is exec, but return the Pipeline right away.
|
|
129
|
+
METHODS_TO_PROMISE_WRAP_EXEC.forEach(methodName => {
|
|
130
|
+
const originalFn = instance[methodName];
|
|
131
|
+
// "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);
|
|
134
|
+
const originalExec = res.exec;
|
|
135
|
+
|
|
136
|
+
res.exec = wrapCommand(originalExec, methodName + '.exec').bind(res);
|
|
137
|
+
|
|
138
|
+
return res;
|
|
139
|
+
};
|
|
122
140
|
});
|
|
123
141
|
}
|
|
124
142
|
|
|
@@ -181,7 +199,7 @@ export class RedisAdapter extends ioredis {
|
|
|
181
199
|
/**
|
|
182
200
|
* Parses the options into what we care about.
|
|
183
201
|
*/
|
|
184
|
-
static _defineOptions({ connectionTimeout, operationTimeout, url, host, port, db, pass, tls }: Record<string, any>
|
|
202
|
+
static _defineOptions({ connectionTimeout, operationTimeout, url, host, port, db, pass, tls }: Record<string, any>) {
|
|
185
203
|
const parsedOptions = {
|
|
186
204
|
connectionTimeout, operationTimeout, url, host, port, db, pass, tls
|
|
187
205
|
};
|
|
@@ -72,8 +72,8 @@ export class SegmentsCacheInRedis implements ISegmentsCacheAsync {
|
|
|
72
72
|
return this.redis.smembers(this.keys.buildRegisteredSegmentsKey());
|
|
73
73
|
}
|
|
74
74
|
|
|
75
|
-
// @TODO remove
|
|
75
|
+
// @TODO remove or implement. It is not being used.
|
|
76
76
|
clear() {
|
|
77
|
-
return
|
|
77
|
+
return Promise.resolve();
|
|
78
78
|
}
|
|
79
79
|
}
|
|
@@ -192,7 +192,7 @@ export class SplitsCacheInRedis extends AbstractSplitsCacheAsync {
|
|
|
192
192
|
*/
|
|
193
193
|
getAll(): Promise<ISplit[]> {
|
|
194
194
|
return this.redis.keys(this.keys.searchPatternForSplitKeys())
|
|
195
|
-
.then((listOfKeys) => this.redis.
|
|
195
|
+
.then((listOfKeys) => this.redis.pipeline(listOfKeys.map(k => ['get', k])).exec())
|
|
196
196
|
.then(processPipelineAnswer)
|
|
197
197
|
.then((splitDefinitions) => splitDefinitions.map((splitDefinition) => {
|
|
198
198
|
return JSON.parse(splitDefinition);
|
|
@@ -213,10 +213,10 @@ export class SplitsCacheInRedis extends AbstractSplitsCacheAsync {
|
|
|
213
213
|
/**
|
|
214
214
|
* Get list of feature flag names related to a given list of flag set names.
|
|
215
215
|
* The returned promise is resolved with the list of feature flag names per flag set,
|
|
216
|
-
* or rejected if the pipelined redis operation fails.
|
|
216
|
+
* or rejected if the pipelined redis operation fails (e.g., timeout).
|
|
217
217
|
*/
|
|
218
218
|
getNamesByFlagSets(flagSets: string[]): Promise<ISet<string>[]> {
|
|
219
|
-
return this.redis.
|
|
219
|
+
return this.redis.pipeline(flagSets.map(flagSet => ['smembers', this.keys.buildFlagSetKey(flagSet)])).exec()
|
|
220
220
|
.then((results) => results.map(([e, value], index) => {
|
|
221
221
|
if (e === null) return value;
|
|
222
222
|
|
|
@@ -252,13 +252,9 @@ export class SplitsCacheInRedis extends AbstractSplitsCacheAsync {
|
|
|
252
252
|
});
|
|
253
253
|
}
|
|
254
254
|
|
|
255
|
-
|
|
256
|
-
* Delete everything in the current database.
|
|
257
|
-
*
|
|
258
|
-
* @NOTE documentation says it never fails.
|
|
259
|
-
*/
|
|
255
|
+
// @TODO remove or implement. It is not being used.
|
|
260
256
|
clear() {
|
|
261
|
-
return
|
|
257
|
+
return Promise.resolve();
|
|
262
258
|
}
|
|
263
259
|
|
|
264
260
|
/**
|
|
@@ -4,6 +4,6 @@ import { ISettings } from '../../types';
|
|
|
4
4
|
/**
|
|
5
5
|
* Storage is async if mode is consumer or partial consumer
|
|
6
6
|
*/
|
|
7
|
-
export function isStorageSync(settings: ISettings) {
|
|
7
|
+
export function isStorageSync(settings: Pick<ISettings, 'mode'>) {
|
|
8
8
|
return [CONSUMER_MODE, CONSUMER_PARTIAL_MODE].indexOf(settings.mode) === -1 ? true : false;
|
|
9
9
|
}
|
|
@@ -8,10 +8,13 @@ function asyncFunction(data: any): Promise<any> {
|
|
|
8
8
|
}
|
|
9
9
|
|
|
10
10
|
const IDENTITY_METHODS: string[] = [];
|
|
11
|
-
const ASYNC_METHODS = ['rpush', 'hincrby'
|
|
11
|
+
const ASYNC_METHODS = ['rpush', 'hincrby'];
|
|
12
|
+
const PIPELINE_METHODS = ['rpush', 'hincrby'];
|
|
12
13
|
|
|
13
14
|
export class RedisMock {
|
|
14
15
|
|
|
16
|
+
private pipelineMethods: any = { exec: jest.fn(asyncFunction) };
|
|
17
|
+
|
|
15
18
|
constructor() {
|
|
16
19
|
IDENTITY_METHODS.forEach(method => {
|
|
17
20
|
this[method] = jest.fn(identityFunction);
|
|
@@ -19,5 +22,10 @@ export class RedisMock {
|
|
|
19
22
|
ASYNC_METHODS.forEach(method => {
|
|
20
23
|
this[method] = jest.fn(asyncFunction);
|
|
21
24
|
});
|
|
25
|
+
PIPELINE_METHODS.forEach(method => {
|
|
26
|
+
this.pipelineMethods[method] = this[method];
|
|
27
|
+
});
|
|
28
|
+
|
|
29
|
+
this.pipeline = jest.fn(() => { return this.pipelineMethods; });
|
|
22
30
|
}
|
|
23
31
|
}
|
|
@@ -1,9 +1,9 @@
|
|
|
1
1
|
import { IIntegrationManager, IIntegrationFactoryParams } from '../integrations/types';
|
|
2
2
|
import { ISignalListener } from '../listeners/types';
|
|
3
|
-
import { ILogger } from '../logger/types';
|
|
4
3
|
import { IReadinessManager, ISdkReadinessManager } from '../readiness/types';
|
|
4
|
+
import type { sdkManagerFactory } from '../sdkManager';
|
|
5
5
|
import { IFetch, ISplitApi, IEventSourceConstructor } from '../services/types';
|
|
6
|
-
import { IStorageAsync, IStorageSync,
|
|
6
|
+
import { IStorageAsync, IStorageSync, IStorageFactoryParams } from '../storages/types';
|
|
7
7
|
import { ISyncManager } from '../sync/types';
|
|
8
8
|
import { IImpressionObserver } from '../trackers/impressionObserver/types';
|
|
9
9
|
import { IImpressionsTracker, IEventTracker, ITelemetryTracker, IFilterAdapter, IUniqueKeysTracker } from '../trackers/types';
|
|
@@ -66,7 +66,7 @@ export interface ISdkFactoryParams {
|
|
|
66
66
|
storageFactory: (params: IStorageFactoryParams) => IStorageSync | IStorageAsync;
|
|
67
67
|
splitApiFactory?: (settings: ISettings, platform: IPlatform, telemetryTracker: ITelemetryTracker) => ISplitApi;
|
|
68
68
|
syncManagerFactory?: (params: ISdkFactoryContextSync) => ISyncManager;
|
|
69
|
-
sdkManagerFactory:
|
|
69
|
+
sdkManagerFactory: typeof sdkManagerFactory;
|
|
70
70
|
sdkClientMethodFactory: (params: ISdkFactoryContext) => ({
|
|
71
71
|
(): SplitIO.ICsClient;
|
|
72
72
|
(key: SplitIO.SplitKey, trafficType?: string | undefined): SplitIO.ICsClient;
|
|
@@ -1,5 +1,4 @@
|
|
|
1
1
|
import { ISplitsCacheAsync, ISplitsCacheSync } from '../storages/types';
|
|
2
2
|
import { ISdkReadinessManager } from '../readiness/types';
|
|
3
|
-
import { SplitIO } from '../types';
|
|
4
|
-
|
|
5
|
-
export declare function sdkManagerFactory<TSplitCache extends ISplitsCacheSync | ISplitsCacheAsync>(log: ILogger, splits: TSplitCache, { readinessManager, sdkStatus }: ISdkReadinessManager): TSplitCache extends ISplitsCacheAsync ? SplitIO.IAsyncManager : SplitIO.IManager;
|
|
3
|
+
import { ISettings, SplitIO } from '../types';
|
|
4
|
+
export declare function sdkManagerFactory<TSplitCache extends ISplitsCacheSync | ISplitsCacheAsync>(settings: Pick<ISettings, 'log' | 'mode'>, splits: TSplitCache, { readinessManager, sdkStatus }: ISdkReadinessManager): TSplitCache extends ISplitsCacheAsync ? SplitIO.IAsyncManager : SplitIO.IManager;
|
|
@@ -9,7 +9,6 @@ export declare class RedisAdapter extends ioredis {
|
|
|
9
9
|
private _notReadyCommandsQueue?;
|
|
10
10
|
private _runningCommands;
|
|
11
11
|
constructor(log: ILogger, storageSettings?: Record<string, any>);
|
|
12
|
-
pipelineExec(commands?: (string | number)[][]): Promise<Array<[Error | null, any]>>;
|
|
13
12
|
_listenToEvents(): void;
|
|
14
13
|
_setTimeoutWrappers(): void;
|
|
15
14
|
_setDisconnectWrapper(): void;
|
|
@@ -21,5 +20,5 @@ export declare class RedisAdapter extends ioredis {
|
|
|
21
20
|
/**
|
|
22
21
|
* Parses the options into what we care about.
|
|
23
22
|
*/
|
|
24
|
-
static _defineOptions({ connectionTimeout, operationTimeout, url, host, port, db, pass, tls }
|
|
23
|
+
static _defineOptions({ connectionTimeout, operationTimeout, url, host, port, db, pass, tls }: Record<string, any>): object;
|
|
25
24
|
}
|
|
@@ -14,5 +14,5 @@ export declare class SegmentsCacheInRedis implements ISegmentsCacheAsync {
|
|
|
14
14
|
getChangeNumber(name: string): Promise<number>;
|
|
15
15
|
registerSegments(segments: string[]): Promise<boolean>;
|
|
16
16
|
getRegisteredSegments(): Promise<string[]>;
|
|
17
|
-
clear(): Promise<
|
|
17
|
+
clear(): Promise<void>;
|
|
18
18
|
}
|
|
@@ -77,7 +77,7 @@ export declare class SplitsCacheInRedis extends AbstractSplitsCacheAsync {
|
|
|
77
77
|
/**
|
|
78
78
|
* Get list of feature flag names related to a given list of flag set names.
|
|
79
79
|
* The returned promise is resolved with the list of feature flag names per flag set,
|
|
80
|
-
* or rejected if the pipelined redis operation fails.
|
|
80
|
+
* or rejected if the pipelined redis operation fails (e.g., timeout).
|
|
81
81
|
*/
|
|
82
82
|
getNamesByFlagSets(flagSets: string[]): Promise<ISet<string>[]>;
|
|
83
83
|
/**
|
|
@@ -87,12 +87,7 @@ export declare class SplitsCacheInRedis extends AbstractSplitsCacheAsync {
|
|
|
87
87
|
* It will never be rejected.
|
|
88
88
|
*/
|
|
89
89
|
trafficTypeExists(trafficType: string): Promise<boolean>;
|
|
90
|
-
|
|
91
|
-
* Delete everything in the current database.
|
|
92
|
-
*
|
|
93
|
-
* @NOTE documentation says it never fails.
|
|
94
|
-
*/
|
|
95
|
-
clear(): Promise<boolean>;
|
|
90
|
+
clear(): Promise<void>;
|
|
96
91
|
/**
|
|
97
92
|
* Fetches multiple splits definitions.
|
|
98
93
|
* Returned promise is rejected if redis operation fails.
|