@splitsoftware/splitio-commons 1.12.1-rc.1 → 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 +11 -9
- package/cjs/sdkClient/client.js +4 -4
- package/cjs/sdkFactory/index.js +1 -1
- package/cjs/sdkManager/index.js +9 -6
- package/cjs/storages/inLocalStorage/SplitsCacheInLocal.js +1 -3
- package/cjs/storages/inRedis/RedisAdapter.js +32 -13
- package/cjs/storages/inRedis/SegmentsCacheInRedis.js +2 -2
- package/cjs/storages/inRedis/SplitsCacheInRedis.js +15 -15
- package/cjs/storages/pluggable/SplitsCachePluggable.js +5 -5
- package/cjs/utils/inputValidation/index.js +5 -5
- package/cjs/utils/inputValidation/{splitExistance.js → splitExistence.js} +3 -3
- package/cjs/utils/inputValidation/{trafficTypeExistance.js → trafficTypeExistence.js} +6 -6
- package/esm/sdkClient/client.js +4 -4
- package/esm/sdkFactory/index.js +1 -1
- package/esm/sdkManager/index.js +10 -7
- package/esm/storages/inLocalStorage/SplitsCacheInLocal.js +1 -3
- package/esm/storages/inRedis/RedisAdapter.js +32 -13
- package/esm/storages/inRedis/SegmentsCacheInRedis.js +2 -2
- package/esm/storages/inRedis/SplitsCacheInRedis.js +15 -15
- package/esm/storages/pluggable/SplitsCachePluggable.js +5 -5
- package/esm/utils/inputValidation/index.js +2 -2
- package/esm/utils/inputValidation/{splitExistance.js → splitExistence.js} +1 -1
- package/esm/utils/inputValidation/{trafficTypeExistance.js → trafficTypeExistence.js} +4 -4
- package/package.json +1 -1
- package/src/sdkClient/client.ts +4 -4
- package/src/sdkFactory/index.ts +1 -1
- package/src/sdkFactory/types.ts +3 -7
- package/src/sdkManager/index.ts +13 -10
- package/src/storages/inLocalStorage/SplitsCacheInLocal.ts +4 -5
- package/src/storages/inRedis/EventsCacheInRedis.ts +3 -3
- package/src/storages/inRedis/ImpressionCountsCacheInRedis.ts +3 -3
- package/src/storages/inRedis/ImpressionsCacheInRedis.ts +3 -3
- package/src/storages/inRedis/RedisAdapter.ts +38 -16
- package/src/storages/inRedis/SegmentsCacheInRedis.ts +5 -5
- package/src/storages/inRedis/SplitsCacheInRedis.ts +18 -19
- package/src/storages/inRedis/TelemetryCacheInRedis.ts +2 -2
- package/src/storages/inRedis/UniqueKeysCacheInRedis.ts +3 -3
- package/src/storages/pluggable/SplitsCachePluggable.ts +5 -5
- package/src/trackers/impressionObserver/utils.ts +1 -1
- package/src/utils/inputValidation/index.ts +2 -2
- package/src/utils/inputValidation/{splitExistance.ts → splitExistence.ts} +1 -1
- package/src/utils/inputValidation/{trafficTypeExistance.ts → trafficTypeExistence.ts} +4 -4
- package/src/utils/redis/RedisMock.ts +1 -3
- package/types/sdkFactory/types.d.ts +3 -3
- package/types/sdkManager/index.d.ts +2 -3
- package/types/storages/inRedis/EventsCacheInRedis.d.ts +2 -2
- package/types/storages/inRedis/ImpressionCountsCacheInRedis.d.ts +3 -2
- package/types/storages/inRedis/ImpressionsCacheInRedis.d.ts +2 -2
- package/types/storages/inRedis/RedisAdapter.d.ts +1 -1
- package/types/storages/inRedis/SegmentsCacheInRedis.d.ts +3 -3
- package/types/storages/inRedis/SplitsCacheInRedis.d.ts +6 -11
- package/types/storages/inRedis/TelemetryCacheInRedis.d.ts +2 -2
- package/types/storages/inRedis/UniqueKeysCacheInRedis.d.ts +3 -2
- package/types/storages/pluggable/SplitsCachePluggable.d.ts +4 -4
- package/types/trackers/impressionObserver/utils.d.ts +1 -1
- package/types/utils/inputValidation/index.d.ts +2 -2
- package/types/utils/inputValidation/splitExistence.d.ts +7 -0
- package/types/utils/inputValidation/trafficTypeExistence.d.ts +9 -0
|
@@ -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', '
|
|
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
|
|
@@ -55,14 +57,15 @@ var RedisAdapter = /** @class */ (function (_super) {
|
|
|
55
57
|
};
|
|
56
58
|
RedisAdapter.prototype._setTimeoutWrappers = function () {
|
|
57
59
|
var instance = this;
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
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 () {
|
|
61
64
|
var params = arguments;
|
|
65
|
+
var caller = this;
|
|
62
66
|
function commandWrapper() {
|
|
63
|
-
instance.log.debug(LOG_PREFIX +
|
|
64
|
-
|
|
65
|
-
var result = originalMethod.apply(instance, params);
|
|
67
|
+
instance.log.debug(LOG_PREFIX + "Executing " + methodName + ".");
|
|
68
|
+
var result = originalMethod.apply(caller, params);
|
|
66
69
|
if (thenable(result)) {
|
|
67
70
|
// For handling pending commands on disconnect, add to the set and remove once finished.
|
|
68
71
|
// On sync commands there's no need, only thenables.
|
|
@@ -73,7 +76,7 @@ var RedisAdapter = /** @class */ (function (_super) {
|
|
|
73
76
|
// Both success and error remove from queue.
|
|
74
77
|
result.then(cleanUpRunningCommandsCb, cleanUpRunningCommandsCb);
|
|
75
78
|
return timeout(instance._options.operationTimeout, result).catch(function (err) {
|
|
76
|
-
instance.log.error(LOG_PREFIX +
|
|
79
|
+
instance.log.error("" + LOG_PREFIX + methodName + " operation threw an error or exceeded configured timeout of " + instance._options.operationTimeout + "ms. Message: " + err);
|
|
77
80
|
// Handling is not the adapter responsibility.
|
|
78
81
|
throw err;
|
|
79
82
|
});
|
|
@@ -81,12 +84,12 @@ var RedisAdapter = /** @class */ (function (_super) {
|
|
|
81
84
|
return result;
|
|
82
85
|
}
|
|
83
86
|
if (instance._notReadyCommandsQueue) {
|
|
84
|
-
return new Promise(function (
|
|
87
|
+
return new Promise(function (resolve, reject) {
|
|
85
88
|
instance._notReadyCommandsQueue.unshift({
|
|
86
|
-
resolve:
|
|
87
|
-
reject:
|
|
89
|
+
resolve: resolve,
|
|
90
|
+
reject: reject,
|
|
88
91
|
command: commandWrapper,
|
|
89
|
-
name:
|
|
92
|
+
name: methodName.toUpperCase()
|
|
90
93
|
});
|
|
91
94
|
});
|
|
92
95
|
}
|
|
@@ -94,6 +97,22 @@ var RedisAdapter = /** @class */ (function (_super) {
|
|
|
94
97
|
return commandWrapper();
|
|
95
98
|
}
|
|
96
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
|
+
};
|
|
97
116
|
});
|
|
98
117
|
};
|
|
99
118
|
RedisAdapter.prototype._setDisconnectWrapper = function () {
|
|
@@ -104,7 +123,7 @@ var RedisAdapter = /** @class */ (function (_super) {
|
|
|
104
123
|
for (var _i = 0; _i < arguments.length; _i++) {
|
|
105
124
|
params[_i] = arguments[_i];
|
|
106
125
|
}
|
|
107
|
-
setTimeout(function
|
|
126
|
+
setTimeout(function deferredDisconnect() {
|
|
108
127
|
if (instance._runningCommands.size > 0) {
|
|
109
128
|
instance.log.info(LOG_PREFIX + ("Attempting to disconnect but there are " + instance._runningCommands.size + " commands still waiting for resolution. Defering disconnection until those finish."));
|
|
110
129
|
Promise.all(setToArray(instance._runningCommands))
|
|
@@ -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
|
}());
|
|
@@ -184,16 +184,20 @@ var SplitsCacheInRedis = /** @class */ (function (_super) {
|
|
|
184
184
|
return this.redis.keys(this.keys.searchPatternForSplitKeys()).then(function (listOfKeys) { return listOfKeys.map(_this.keys.extractKey); });
|
|
185
185
|
};
|
|
186
186
|
/**
|
|
187
|
-
* Get list of
|
|
188
|
-
* The returned promise is resolved with the list of
|
|
189
|
-
* or rejected if
|
|
187
|
+
* Get list of feature flag names related to a given list of flag set names.
|
|
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 (e.g., timeout).
|
|
190
190
|
*/
|
|
191
191
|
SplitsCacheInRedis.prototype.getNamesByFlagSets = function (flagSets) {
|
|
192
192
|
var _this = this;
|
|
193
|
-
return
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
|
|
193
|
+
return this.redis.pipeline(flagSets.map(function (flagSet) { return ['smembers', _this.keys.buildFlagSetKey(flagSet)]; })).exec()
|
|
194
|
+
.then(function (results) { return results.map(function (_a, index) {
|
|
195
|
+
var e = _a[0], value = _a[1];
|
|
196
|
+
if (e === null)
|
|
197
|
+
return value;
|
|
198
|
+
_this.log.error(LOG_PREFIX + ("Could not read result from get members of flag set " + flagSets[index] + " due to an error: " + e));
|
|
199
|
+
}); })
|
|
200
|
+
.then(function (namesByFlagSets) { return namesByFlagSets.map(function (namesByFlagSet) { return new _Set(namesByFlagSet); }); });
|
|
197
201
|
};
|
|
198
202
|
/**
|
|
199
203
|
* Check traffic type existence.
|
|
@@ -210,24 +214,20 @@ var SplitsCacheInRedis = /** @class */ (function (_super) {
|
|
|
210
214
|
return false; // if entry doesn't exist, means that TT doesn't exist
|
|
211
215
|
ttCount = parseInt(ttCount, 10);
|
|
212
216
|
if (!isFiniteNumber(ttCount) || ttCount < 0) {
|
|
213
|
-
_this.log.info(LOG_PREFIX + ("Could not validate traffic type
|
|
217
|
+
_this.log.info(LOG_PREFIX + ("Could not validate traffic type existence of " + trafficType + " due to data corruption of some sorts."));
|
|
214
218
|
return false;
|
|
215
219
|
}
|
|
216
220
|
return ttCount > 0;
|
|
217
221
|
})
|
|
218
222
|
.catch(function (e) {
|
|
219
|
-
_this.log.error(LOG_PREFIX + ("Could not validate traffic type
|
|
223
|
+
_this.log.error(LOG_PREFIX + ("Could not validate traffic type existence of " + trafficType + " due to an error: " + e + "."));
|
|
220
224
|
// If there is an error, bypass the validation so the event can get tracked.
|
|
221
225
|
return true;
|
|
222
226
|
});
|
|
223
227
|
};
|
|
224
|
-
|
|
225
|
-
* Delete everything in the current database.
|
|
226
|
-
*
|
|
227
|
-
* @NOTE documentation says it never fails.
|
|
228
|
-
*/
|
|
228
|
+
// @TODO remove or implement. It is not being used.
|
|
229
229
|
SplitsCacheInRedis.prototype.clear = function () {
|
|
230
|
-
return
|
|
230
|
+
return Promise.resolve();
|
|
231
231
|
};
|
|
232
232
|
/**
|
|
233
233
|
* Fetches multiple splits definitions.
|
|
@@ -158,15 +158,15 @@ var SplitsCachePluggable = /** @class */ (function (_super) {
|
|
|
158
158
|
return this.wrapper.getKeysByPrefix(this.keys.buildSplitKeyPrefix()).then(function (listOfKeys) { return listOfKeys.map(_this.keys.extractKey); });
|
|
159
159
|
};
|
|
160
160
|
/**
|
|
161
|
-
* Get list of
|
|
162
|
-
* The returned promise is resolved with the list of
|
|
163
|
-
*
|
|
164
|
-
|
|
161
|
+
* Get list of feature flag names related to a given list of flag set names.
|
|
162
|
+
* The returned promise is resolved with the list of feature flag names per flag set.
|
|
163
|
+
* It never rejects (If there is a wrapper error for some flag set, an empty set is returned for it).
|
|
164
|
+
*/
|
|
165
165
|
SplitsCachePluggable.prototype.getNamesByFlagSets = function (flagSets) {
|
|
166
166
|
var _this = this;
|
|
167
167
|
return Promise.all(flagSets.map(function (flagSet) {
|
|
168
168
|
var flagSetKey = _this.keys.buildFlagSetKey(flagSet);
|
|
169
|
-
return _this.wrapper.getItems(flagSetKey);
|
|
169
|
+
return _this.wrapper.getItems(flagSetKey).catch(function () { return []; });
|
|
170
170
|
})).then(function (namesByFlagSets) { return namesByFlagSets.map(function (namesByFlagSet) { return new _Set(namesByFlagSet); }); });
|
|
171
171
|
};
|
|
172
172
|
/**
|
|
@@ -8,6 +8,6 @@ export { validateSplit } from './split';
|
|
|
8
8
|
export { validateSplits } from './splits';
|
|
9
9
|
export { validateTrafficType } from './trafficType';
|
|
10
10
|
export { validateIfNotDestroyed, validateIfOperational } from './isOperational';
|
|
11
|
-
export {
|
|
12
|
-
export {
|
|
11
|
+
export { validateSplitExistence } from './splitExistence';
|
|
12
|
+
export { validateTrafficTypeExistence } from './trafficTypeExistence';
|
|
13
13
|
export { validatePreloadedData } from './preloadedData';
|
|
@@ -4,7 +4,7 @@ import { WARN_NOT_EXISTENT_SPLIT } from '../../logger/constants';
|
|
|
4
4
|
* This is defined here and in this format mostly because of the logger and the fact that it's considered a validation at product level.
|
|
5
5
|
* But it's not going to run on the input validation layer. In any case, the most compeling reason to use it as we do is to avoid going to Redis and get a split twice.
|
|
6
6
|
*/
|
|
7
|
-
export function
|
|
7
|
+
export function validateSplitExistence(log, readinessManager, splitName, labelOrSplitObj, method) {
|
|
8
8
|
if (readinessManager.isReady()) { // Only if it's ready we validate this, otherwise it may just be that the SDK is not ready yet.
|
|
9
9
|
if (labelOrSplitObj === SPLIT_NOT_FOUND || labelOrSplitObj == null) {
|
|
10
10
|
log.warn(WARN_NOT_EXISTENT_SPLIT, [method, splitName]);
|
|
@@ -1,13 +1,13 @@
|
|
|
1
1
|
import { thenable } from '../promise/thenable';
|
|
2
2
|
import { LOCALHOST_MODE } from '../constants';
|
|
3
3
|
import { WARN_NOT_EXISTENT_TT } from '../../logger/constants';
|
|
4
|
-
function
|
|
4
|
+
function logTTExistenceWarning(log, maybeTT, method) {
|
|
5
5
|
log.warn(WARN_NOT_EXISTENT_TT, [method, maybeTT]);
|
|
6
6
|
}
|
|
7
7
|
/**
|
|
8
8
|
* Separated from the previous method since on some cases it'll be async.
|
|
9
9
|
*/
|
|
10
|
-
export function
|
|
10
|
+
export function validateTrafficTypeExistence(log, readinessManager, splitsCache, mode, maybeTT, method) {
|
|
11
11
|
// If not ready or in localhost mode, we won't run the validation
|
|
12
12
|
if (!readinessManager.isReady() || mode === LOCALHOST_MODE)
|
|
13
13
|
return true;
|
|
@@ -15,13 +15,13 @@ export function validateTrafficTypeExistance(log, readinessManager, splitsCache,
|
|
|
15
15
|
if (thenable(res)) {
|
|
16
16
|
return res.then(function (isValid) {
|
|
17
17
|
if (!isValid)
|
|
18
|
-
|
|
18
|
+
logTTExistenceWarning(log, maybeTT, method);
|
|
19
19
|
return isValid; // propagate result
|
|
20
20
|
});
|
|
21
21
|
}
|
|
22
22
|
else {
|
|
23
23
|
if (!res)
|
|
24
|
-
|
|
24
|
+
logTTExistenceWarning(log, maybeTT, method);
|
|
25
25
|
return res;
|
|
26
26
|
}
|
|
27
27
|
}
|
package/package.json
CHANGED
package/src/sdkClient/client.ts
CHANGED
|
@@ -1,8 +1,8 @@
|
|
|
1
1
|
import { evaluateFeature, evaluateFeatures, evaluateFeaturesByFlagSets } from '../evaluator';
|
|
2
2
|
import { thenable } from '../utils/promise/thenable';
|
|
3
3
|
import { getMatching, getBucketing } from '../utils/key';
|
|
4
|
-
import {
|
|
5
|
-
import {
|
|
4
|
+
import { validateSplitExistence } from '../utils/inputValidation/splitExistence';
|
|
5
|
+
import { validateTrafficTypeExistence } from '../utils/inputValidation/trafficTypeExistence';
|
|
6
6
|
import { SDK_NOT_READY } from '../utils/labels';
|
|
7
7
|
import { CONTROL, TREATMENT, TREATMENTS, TREATMENT_WITH_CONFIG, TREATMENTS_WITH_CONFIG, TRACK, TREATMENTS_WITH_CONFIG_BY_FLAGSETS, TREATMENTS_BY_FLAGSETS, TREATMENTS_BY_FLAGSET, TREATMENTS_WITH_CONFIG_BY_FLAGSET } from '../utils/constants';
|
|
8
8
|
import { IEvaluationResult } from '../evaluator/types';
|
|
@@ -133,7 +133,7 @@ export function clientFactory(params: ISdkFactoryContext): SplitIO.IClient | Spl
|
|
|
133
133
|
const { treatment, label, changeNumber, config = null } = evaluation;
|
|
134
134
|
log.info(IMPRESSION, [featureFlagName, matchingKey, treatment, label]);
|
|
135
135
|
|
|
136
|
-
if (
|
|
136
|
+
if (validateSplitExistence(log, readinessManager, featureFlagName, label, invokingMethodName)) {
|
|
137
137
|
log.info(IMPRESSION_QUEUEING);
|
|
138
138
|
queue.push({
|
|
139
139
|
feature: featureFlagName,
|
|
@@ -171,7 +171,7 @@ export function clientFactory(params: ISdkFactoryContext): SplitIO.IClient | Spl
|
|
|
171
171
|
};
|
|
172
172
|
|
|
173
173
|
// This may be async but we only warn, we don't actually care if it is valid or not in terms of queueing the event.
|
|
174
|
-
|
|
174
|
+
validateTrafficTypeExistence(log, readinessManager, storage.splits, mode, trafficTypeName, 'track');
|
|
175
175
|
|
|
176
176
|
const result = eventTracker.track(eventData, size);
|
|
177
177
|
|
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
|
@@ -1,12 +1,12 @@
|
|
|
1
1
|
import { objectAssign } from '../utils/lang/objectAssign';
|
|
2
2
|
import { thenable } from '../utils/promise/thenable';
|
|
3
3
|
import { find } from '../utils/lang';
|
|
4
|
-
import { validateSplit,
|
|
4
|
+
import { validateSplit, validateSplitExistence, validateIfNotDestroyed, validateIfOperational } from '../utils/inputValidation';
|
|
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,19 +67,19 @@ 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);
|
|
71
74
|
|
|
72
75
|
if (thenable(split)) {
|
|
73
76
|
return split.catch(() => null).then(result => { // handle possible rejections when using pluggable storage
|
|
74
|
-
|
|
77
|
+
validateSplitExistence(log, readinessManager, splitName, result, SPLIT_FN_LABEL);
|
|
75
78
|
return objectToView(result);
|
|
76
79
|
});
|
|
77
80
|
}
|
|
78
81
|
|
|
79
|
-
|
|
82
|
+
validateSplitExistence(log, readinessManager, splitName, split, SPLIT_FN_LABEL);
|
|
80
83
|
|
|
81
84
|
return objectToView(split);
|
|
82
85
|
},
|
|
@@ -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
|
|
|
@@ -260,7 +260,7 @@ export class SplitsCacheInLocal extends AbstractSplitsCacheSync {
|
|
|
260
260
|
getNamesByFlagSets(flagSets: string[]): ISet<string>[] {
|
|
261
261
|
return flagSets.map(flagSet => {
|
|
262
262
|
const flagSetKey = this.keys.buildFlagSetKey(flagSet);
|
|
263
|
-
|
|
263
|
+
const flagSetFromLocalStorage = localStorage.getItem(flagSetKey);
|
|
264
264
|
|
|
265
265
|
return new _Set(flagSetFromLocalStorage ? JSON.parse(flagSetFromLocalStorage) : []);
|
|
266
266
|
});
|
|
@@ -275,10 +275,9 @@ export class SplitsCacheInLocal extends AbstractSplitsCacheSync {
|
|
|
275
275
|
|
|
276
276
|
const flagSetKey = this.keys.buildFlagSetKey(featureFlagSet);
|
|
277
277
|
|
|
278
|
-
|
|
279
|
-
if (!flagSetFromLocalStorage) flagSetFromLocalStorage = '[]';
|
|
278
|
+
const flagSetFromLocalStorage = localStorage.getItem(flagSetKey);
|
|
280
279
|
|
|
281
|
-
const flagSetCache = new _Set(JSON.parse(flagSetFromLocalStorage));
|
|
280
|
+
const flagSetCache = new _Set(flagSetFromLocalStorage ? JSON.parse(flagSetFromLocalStorage) : []);
|
|
282
281
|
flagSetCache.add(featureFlag.name);
|
|
283
282
|
|
|
284
283
|
localStorage.setItem(flagSetKey, JSON.stringify(setToArray(flagSetCache)));
|
|
@@ -296,7 +295,7 @@ export class SplitsCacheInLocal extends AbstractSplitsCacheSync {
|
|
|
296
295
|
private removeNames(flagSetName: string, featureFlagName: string) {
|
|
297
296
|
const flagSetKey = this.keys.buildFlagSetKey(flagSetName);
|
|
298
297
|
|
|
299
|
-
|
|
298
|
+
const flagSetFromLocalStorage = localStorage.getItem(flagSetKey);
|
|
300
299
|
|
|
301
300
|
if (!flagSetFromLocalStorage) return;
|
|
302
301
|
|
|
@@ -1,19 +1,19 @@
|
|
|
1
1
|
import { IEventsCacheAsync } from '../types';
|
|
2
2
|
import { IMetadata } from '../../dtos/types';
|
|
3
|
-
import { Redis } from 'ioredis';
|
|
4
3
|
import { SplitIO } from '../../types';
|
|
5
4
|
import { ILogger } from '../../logger/types';
|
|
6
5
|
import { LOG_PREFIX } from './constants';
|
|
7
6
|
import { StoredEventWithMetadata } from '../../sync/submitters/types';
|
|
7
|
+
import type { RedisAdapter } from './RedisAdapter';
|
|
8
8
|
|
|
9
9
|
export class EventsCacheInRedis implements IEventsCacheAsync {
|
|
10
10
|
|
|
11
11
|
private readonly log: ILogger;
|
|
12
12
|
private readonly key: string;
|
|
13
|
-
private readonly redis:
|
|
13
|
+
private readonly redis: RedisAdapter;
|
|
14
14
|
private readonly metadata: IMetadata;
|
|
15
15
|
|
|
16
|
-
constructor(log: ILogger, key: string, redis:
|
|
16
|
+
constructor(log: ILogger, key: string, redis: RedisAdapter, metadata: IMetadata) {
|
|
17
17
|
this.log = log;
|
|
18
18
|
this.key = key;
|
|
19
19
|
this.redis = redis;
|
|
@@ -1,19 +1,19 @@
|
|
|
1
|
-
import { Redis } from 'ioredis';
|
|
2
1
|
import { ILogger } from '../../logger/types';
|
|
3
2
|
import { ImpressionCountsPayload } from '../../sync/submitters/types';
|
|
4
3
|
import { forOwn } from '../../utils/lang';
|
|
5
4
|
import { ImpressionCountsCacheInMemory } from '../inMemory/ImpressionCountsCacheInMemory';
|
|
6
5
|
import { LOG_PREFIX, REFRESH_RATE, TTL_REFRESH } from './constants';
|
|
6
|
+
import type { RedisAdapter } from './RedisAdapter';
|
|
7
7
|
|
|
8
8
|
export class ImpressionCountsCacheInRedis extends ImpressionCountsCacheInMemory {
|
|
9
9
|
|
|
10
10
|
private readonly log: ILogger;
|
|
11
11
|
private readonly key: string;
|
|
12
|
-
private readonly redis:
|
|
12
|
+
private readonly redis: RedisAdapter;
|
|
13
13
|
private readonly refreshRate: number;
|
|
14
14
|
private intervalId: any;
|
|
15
15
|
|
|
16
|
-
constructor(log: ILogger, key: string, redis:
|
|
16
|
+
constructor(log: ILogger, key: string, redis: RedisAdapter, impressionCountsCacheSize?: number, refreshRate = REFRESH_RATE) {
|
|
17
17
|
super(impressionCountsCacheSize);
|
|
18
18
|
this.log = log;
|
|
19
19
|
this.key = key;
|
|
@@ -1,10 +1,10 @@
|
|
|
1
1
|
import { IImpressionsCacheAsync } from '../types';
|
|
2
2
|
import { IMetadata } from '../../dtos/types';
|
|
3
3
|
import { ImpressionDTO } from '../../types';
|
|
4
|
-
import { Redis } from 'ioredis';
|
|
5
4
|
import { StoredImpressionWithMetadata } from '../../sync/submitters/types';
|
|
6
5
|
import { ILogger } from '../../logger/types';
|
|
7
6
|
import { impressionsToJSON } from '../utils';
|
|
7
|
+
import type { RedisAdapter } from './RedisAdapter';
|
|
8
8
|
|
|
9
9
|
const IMPRESSIONS_TTL_REFRESH = 3600; // 1 hr
|
|
10
10
|
|
|
@@ -12,10 +12,10 @@ export class ImpressionsCacheInRedis implements IImpressionsCacheAsync {
|
|
|
12
12
|
|
|
13
13
|
private readonly log: ILogger;
|
|
14
14
|
private readonly key: string;
|
|
15
|
-
private readonly redis:
|
|
15
|
+
private readonly redis: RedisAdapter;
|
|
16
16
|
private readonly metadata: IMetadata;
|
|
17
17
|
|
|
18
|
-
constructor(log: ILogger, key: string, redis:
|
|
18
|
+
constructor(log: ILogger, key: string, redis: RedisAdapter, metadata: IMetadata) {
|
|
19
19
|
this.log = log;
|
|
20
20
|
this.key = key;
|
|
21
21
|
this.redis = redis;
|
|
@@ -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', '
|
|
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: Record<string, any>) {
|
|
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));
|
|
@@ -56,6 +57,7 @@ export class RedisAdapter extends ioredis {
|
|
|
56
57
|
this.once('ready', () => {
|
|
57
58
|
const commandsCount = this._notReadyCommandsQueue ? this._notReadyCommandsQueue.length : 0;
|
|
58
59
|
this.log.info(LOG_PREFIX + `Redis connection established. Queued commands: ${commandsCount}.`);
|
|
60
|
+
|
|
59
61
|
this._notReadyCommandsQueue && this._notReadyCommandsQueue.forEach(queued => {
|
|
60
62
|
this.log.info(LOG_PREFIX + `Executing queued ${queued.name} command.`);
|
|
61
63
|
queued.command().then(queued.resolve).catch(queued.reject);
|
|
@@ -71,16 +73,16 @@ export class RedisAdapter extends ioredis {
|
|
|
71
73
|
_setTimeoutWrappers() {
|
|
72
74
|
const instance: Record<string, any> = this;
|
|
73
75
|
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
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) {
|
|
78
80
|
const params = arguments;
|
|
81
|
+
const caller = this;
|
|
79
82
|
|
|
80
83
|
function commandWrapper() {
|
|
81
|
-
instance.log.debug(LOG_PREFIX
|
|
82
|
-
|
|
83
|
-
const result = originalMethod.apply(instance, params);
|
|
84
|
+
instance.log.debug(`${LOG_PREFIX}Executing ${methodName}.`);
|
|
85
|
+
const result = originalMethod.apply(caller, params);
|
|
84
86
|
|
|
85
87
|
if (thenable(result)) {
|
|
86
88
|
// For handling pending commands on disconnect, add to the set and remove once finished.
|
|
@@ -93,7 +95,7 @@ export class RedisAdapter extends ioredis {
|
|
|
93
95
|
result.then(cleanUpRunningCommandsCb, cleanUpRunningCommandsCb);
|
|
94
96
|
|
|
95
97
|
return timeout(instance._options.operationTimeout, result).catch(err => {
|
|
96
|
-
instance.log.error(
|
|
98
|
+
instance.log.error(`${LOG_PREFIX}${methodName} operation threw an error or exceeded configured timeout of ${instance._options.operationTimeout}ms. Message: ${err}`);
|
|
97
99
|
// Handling is not the adapter responsibility.
|
|
98
100
|
throw err;
|
|
99
101
|
});
|
|
@@ -103,18 +105,38 @@ export class RedisAdapter extends ioredis {
|
|
|
103
105
|
}
|
|
104
106
|
|
|
105
107
|
if (instance._notReadyCommandsQueue) {
|
|
106
|
-
return new Promise((
|
|
108
|
+
return new Promise((resolve, reject) => {
|
|
107
109
|
instance._notReadyCommandsQueue.unshift({
|
|
108
|
-
resolve
|
|
109
|
-
reject
|
|
110
|
+
resolve,
|
|
111
|
+
reject,
|
|
110
112
|
command: commandWrapper,
|
|
111
|
-
name:
|
|
113
|
+
name: methodName.toUpperCase()
|
|
112
114
|
});
|
|
113
115
|
});
|
|
114
116
|
} else {
|
|
115
117
|
return commandWrapper();
|
|
116
118
|
}
|
|
117
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
|
+
};
|
|
118
140
|
});
|
|
119
141
|
}
|
|
120
142
|
|
|
@@ -124,7 +146,7 @@ export class RedisAdapter extends ioredis {
|
|
|
124
146
|
|
|
125
147
|
instance.disconnect = function disconnect(...params: []) {
|
|
126
148
|
|
|
127
|
-
setTimeout(function
|
|
149
|
+
setTimeout(function deferredDisconnect() {
|
|
128
150
|
if (instance._runningCommands.size > 0) {
|
|
129
151
|
instance.log.info(LOG_PREFIX + `Attempting to disconnect but there are ${instance._runningCommands.size} commands still waiting for resolution. Defering disconnection until those finish.`);
|
|
130
152
|
|