@splitsoftware/splitio-commons 1.12.1-rc.1 → 1.12.1-rc.2
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 +2 -1
- package/cjs/sdkClient/client.js +4 -4
- package/cjs/sdkManager/index.js +2 -2
- package/cjs/storages/inLocalStorage/SplitsCacheInLocal.js +1 -3
- package/cjs/storages/inRedis/ImpressionCountsCacheInRedis.js +1 -5
- package/cjs/storages/inRedis/RedisAdapter.js +12 -10
- package/cjs/storages/inRedis/SplitsCacheInRedis.js +14 -10
- 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/cjs/utils/redis/RedisMock.js +1 -7
- package/esm/sdkClient/client.js +4 -4
- package/esm/sdkManager/index.js +3 -3
- package/esm/storages/inLocalStorage/SplitsCacheInLocal.js +1 -3
- package/esm/storages/inRedis/ImpressionCountsCacheInRedis.js +1 -5
- package/esm/storages/inRedis/RedisAdapter.js +12 -10
- package/esm/storages/inRedis/SplitsCacheInRedis.js +14 -10
- 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/esm/utils/redis/RedisMock.js +1 -7
- package/package.json +1 -1
- package/src/sdkClient/client.ts +4 -4
- package/src/sdkManager/index.ts +3 -3
- package/src/storages/inLocalStorage/SplitsCacheInLocal.ts +4 -5
- package/src/storages/inRedis/EventsCacheInRedis.ts +3 -3
- package/src/storages/inRedis/ImpressionCountsCacheInRedis.ts +4 -8
- package/src/storages/inRedis/ImpressionsCacheInRedis.ts +3 -3
- package/src/storages/inRedis/RedisAdapter.ts +16 -12
- package/src/storages/inRedis/SegmentsCacheInRedis.ts +3 -3
- package/src/storages/inRedis/SplitsCacheInRedis.ts +17 -14
- 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/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 -11
- 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 +3 -2
- package/types/storages/inRedis/SegmentsCacheInRedis.d.ts +2 -2
- package/types/storages/inRedis/SplitsCacheInRedis.d.ts +5 -5
- 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/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
- package/types/utils/redis/RedisMock.d.ts +0 -1
|
@@ -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
|
}
|
|
@@ -6,22 +6,16 @@ function asyncFunction(data) {
|
|
|
6
6
|
return Promise.resolve(data);
|
|
7
7
|
}
|
|
8
8
|
var IDENTITY_METHODS = [];
|
|
9
|
-
var ASYNC_METHODS = ['rpush', 'hincrby'];
|
|
10
|
-
var PIPELINE_METHODS = ['rpush', 'hincrby'];
|
|
9
|
+
var ASYNC_METHODS = ['rpush', 'hincrby', 'pipelineExec'];
|
|
11
10
|
var RedisMock = /** @class */ (function () {
|
|
12
11
|
function RedisMock() {
|
|
13
12
|
var _this = this;
|
|
14
|
-
this.pipelineMethods = { exec: jest.fn(asyncFunction) };
|
|
15
13
|
IDENTITY_METHODS.forEach(function (method) {
|
|
16
14
|
_this[method] = jest.fn(identityFunction);
|
|
17
15
|
});
|
|
18
16
|
ASYNC_METHODS.forEach(function (method) {
|
|
19
17
|
_this[method] = jest.fn(asyncFunction);
|
|
20
18
|
});
|
|
21
|
-
PIPELINE_METHODS.forEach(function (method) {
|
|
22
|
-
_this.pipelineMethods[method] = _this[method];
|
|
23
|
-
});
|
|
24
|
-
this.pipeline = jest.fn(function () { return _this.pipelineMethods; });
|
|
25
19
|
}
|
|
26
20
|
return RedisMock;
|
|
27
21
|
}());
|
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/sdkManager/index.ts
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
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';
|
|
@@ -71,12 +71,12 @@ export function sdkManagerFactory<TSplitCache extends ISplitsCacheSync | ISplits
|
|
|
71
71
|
|
|
72
72
|
if (thenable(split)) {
|
|
73
73
|
return split.catch(() => null).then(result => { // handle possible rejections when using pluggable storage
|
|
74
|
-
|
|
74
|
+
validateSplitExistence(log, readinessManager, splitName, result, SPLIT_FN_LABEL);
|
|
75
75
|
return objectToView(result);
|
|
76
76
|
});
|
|
77
77
|
}
|
|
78
78
|
|
|
79
|
-
|
|
79
|
+
validateSplitExistence(log, readinessManager, splitName, split, SPLIT_FN_LABEL);
|
|
80
80
|
|
|
81
81
|
return objectToView(split);
|
|
82
82
|
},
|
|
@@ -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;
|
|
@@ -27,11 +27,7 @@ export class ImpressionCountsCacheInRedis extends ImpressionCountsCacheInMemory
|
|
|
27
27
|
const keys = Object.keys(counts);
|
|
28
28
|
if (!keys.length) return Promise.resolve(false);
|
|
29
29
|
|
|
30
|
-
|
|
31
|
-
keys.forEach(key => {
|
|
32
|
-
pipeline.hincrby(this.key, key, counts[key]);
|
|
33
|
-
});
|
|
34
|
-
return pipeline.exec()
|
|
30
|
+
return this.redis.pipelineExec(keys.map(key => ['hincrby', this.key, key, counts[key]]))
|
|
35
31
|
.then(data => {
|
|
36
32
|
// If this is the creation of the key on Redis, set the expiration for it in 3600 seconds.
|
|
37
33
|
if (data.length && data.length === keys.length) {
|
|
@@ -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;
|
|
@@ -8,7 +8,7 @@ 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', 'flushdb', 'pipelineExec'];
|
|
12
12
|
|
|
13
13
|
// Not part of the settings since it'll vary on each storage. We should be removing storage specific logic from elsewhere.
|
|
14
14
|
const DEFAULT_OPTIONS = {
|
|
@@ -38,7 +38,7 @@ export class RedisAdapter extends ioredis {
|
|
|
38
38
|
private _notReadyCommandsQueue?: IRedisCommand[];
|
|
39
39
|
private _runningCommands: ISet<Promise<any>>;
|
|
40
40
|
|
|
41
|
-
constructor(log: ILogger, storageSettings
|
|
41
|
+
constructor(log: ILogger, storageSettings?: Record<string, any>) {
|
|
42
42
|
const options = RedisAdapter._defineOptions(storageSettings);
|
|
43
43
|
// Call the ioredis constructor
|
|
44
44
|
super(...RedisAdapter._defineLibrarySettings(options));
|
|
@@ -52,10 +52,15 @@ export class RedisAdapter extends ioredis {
|
|
|
52
52
|
this._setDisconnectWrapper();
|
|
53
53
|
}
|
|
54
54
|
|
|
55
|
+
pipelineExec(commands?: (string | number)[][]): Promise<Array<[Error | null, any]>> {
|
|
56
|
+
return this.pipeline(commands as string[][]).exec();
|
|
57
|
+
}
|
|
58
|
+
|
|
55
59
|
_listenToEvents() {
|
|
56
60
|
this.once('ready', () => {
|
|
57
61
|
const commandsCount = this._notReadyCommandsQueue ? this._notReadyCommandsQueue.length : 0;
|
|
58
62
|
this.log.info(LOG_PREFIX + `Redis connection established. Queued commands: ${commandsCount}.`);
|
|
63
|
+
|
|
59
64
|
this._notReadyCommandsQueue && this._notReadyCommandsQueue.forEach(queued => {
|
|
60
65
|
this.log.info(LOG_PREFIX + `Executing queued ${queued.name} command.`);
|
|
61
66
|
queued.command().then(queued.resolve).catch(queued.reject);
|
|
@@ -71,29 +76,28 @@ export class RedisAdapter extends ioredis {
|
|
|
71
76
|
_setTimeoutWrappers() {
|
|
72
77
|
const instance: Record<string, any> = this;
|
|
73
78
|
|
|
74
|
-
METHODS_TO_PROMISE_WRAP.forEach(
|
|
75
|
-
const originalMethod = instance[
|
|
79
|
+
METHODS_TO_PROMISE_WRAP.forEach(methodName => {
|
|
80
|
+
const originalMethod = instance[methodName];
|
|
76
81
|
|
|
77
|
-
instance[
|
|
82
|
+
instance[methodName] = function () {
|
|
78
83
|
const params = arguments;
|
|
79
84
|
|
|
80
85
|
function commandWrapper() {
|
|
81
|
-
instance.log.debug(LOG_PREFIX
|
|
82
|
-
// Return original method
|
|
86
|
+
instance.log.debug(`${LOG_PREFIX}Executing ${methodName}.`);
|
|
83
87
|
const result = originalMethod.apply(instance, params);
|
|
84
88
|
|
|
85
89
|
if (thenable(result)) {
|
|
86
90
|
// For handling pending commands on disconnect, add to the set and remove once finished.
|
|
87
91
|
// On sync commands there's no need, only thenables.
|
|
88
92
|
instance._runningCommands.add(result);
|
|
89
|
-
const cleanUpRunningCommandsCb =
|
|
93
|
+
const cleanUpRunningCommandsCb = () => {
|
|
90
94
|
instance._runningCommands.delete(result);
|
|
91
95
|
};
|
|
92
96
|
// Both success and error remove from queue.
|
|
93
97
|
result.then(cleanUpRunningCommandsCb, cleanUpRunningCommandsCb);
|
|
94
98
|
|
|
95
99
|
return timeout(instance._options.operationTimeout, result).catch(err => {
|
|
96
|
-
instance.log.error(
|
|
100
|
+
instance.log.error(`${LOG_PREFIX}${methodName} operation threw an error or exceeded configured timeout of ${instance._options.operationTimeout}ms. Message: ${err}`);
|
|
97
101
|
// Handling is not the adapter responsibility.
|
|
98
102
|
throw err;
|
|
99
103
|
});
|
|
@@ -108,7 +112,7 @@ export class RedisAdapter extends ioredis {
|
|
|
108
112
|
resolve: res,
|
|
109
113
|
reject: rej,
|
|
110
114
|
command: commandWrapper,
|
|
111
|
-
name:
|
|
115
|
+
name: methodName.toUpperCase()
|
|
112
116
|
});
|
|
113
117
|
});
|
|
114
118
|
} else {
|
|
@@ -124,7 +128,7 @@ export class RedisAdapter extends ioredis {
|
|
|
124
128
|
|
|
125
129
|
instance.disconnect = function disconnect(...params: []) {
|
|
126
130
|
|
|
127
|
-
setTimeout(function
|
|
131
|
+
setTimeout(function deferredDisconnect() {
|
|
128
132
|
if (instance._runningCommands.size > 0) {
|
|
129
133
|
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
134
|
|
|
@@ -177,7 +181,7 @@ export class RedisAdapter extends ioredis {
|
|
|
177
181
|
/**
|
|
178
182
|
* Parses the options into what we care about.
|
|
179
183
|
*/
|
|
180
|
-
static _defineOptions({ connectionTimeout, operationTimeout, url, host, port, db, pass, tls }: Record<string, any>) {
|
|
184
|
+
static _defineOptions({ connectionTimeout, operationTimeout, url, host, port, db, pass, tls }: Record<string, any> = {}) {
|
|
181
185
|
const parsedOptions = {
|
|
182
186
|
connectionTimeout, operationTimeout, url, host, port, db, pass, tls
|
|
183
187
|
};
|
|
@@ -1,17 +1,17 @@
|
|
|
1
|
-
import { Redis } from 'ioredis';
|
|
2
1
|
import { ILogger } from '../../logger/types';
|
|
3
2
|
import { isNaNNumber } from '../../utils/lang';
|
|
4
3
|
import { LOG_PREFIX } from '../inLocalStorage/constants';
|
|
5
4
|
import { KeyBuilderSS } from '../KeyBuilderSS';
|
|
6
5
|
import { ISegmentsCacheAsync } from '../types';
|
|
6
|
+
import type { RedisAdapter } from './RedisAdapter';
|
|
7
7
|
|
|
8
8
|
export class SegmentsCacheInRedis implements ISegmentsCacheAsync {
|
|
9
9
|
|
|
10
10
|
private readonly log: ILogger;
|
|
11
|
-
private readonly redis:
|
|
11
|
+
private readonly redis: RedisAdapter;
|
|
12
12
|
private readonly keys: KeyBuilderSS;
|
|
13
13
|
|
|
14
|
-
constructor(log: ILogger, keys: KeyBuilderSS, redis:
|
|
14
|
+
constructor(log: ILogger, keys: KeyBuilderSS, redis: RedisAdapter) {
|
|
15
15
|
this.log = log;
|
|
16
16
|
this.redis = redis;
|
|
17
17
|
this.keys = keys;
|
|
@@ -1,11 +1,11 @@
|
|
|
1
1
|
import { isFiniteNumber, isNaNNumber } from '../../utils/lang';
|
|
2
2
|
import { KeyBuilderSS } from '../KeyBuilderSS';
|
|
3
|
-
import { Redis } from 'ioredis';
|
|
4
3
|
import { ILogger } from '../../logger/types';
|
|
5
4
|
import { LOG_PREFIX } from './constants';
|
|
6
5
|
import { ISplit, ISplitFiltersValidation } from '../../dtos/types';
|
|
7
6
|
import { AbstractSplitsCacheAsync } from '../AbstractSplitsCacheAsync';
|
|
8
7
|
import { ISet, _Set, returnListDifference } from '../../utils/lang/sets';
|
|
8
|
+
import type { RedisAdapter } from './RedisAdapter';
|
|
9
9
|
|
|
10
10
|
/**
|
|
11
11
|
* Discard errors for an answer of multiple operations.
|
|
@@ -24,12 +24,12 @@ function processPipelineAnswer(results: Array<[Error | null, string]>): string[]
|
|
|
24
24
|
export class SplitsCacheInRedis extends AbstractSplitsCacheAsync {
|
|
25
25
|
|
|
26
26
|
private readonly log: ILogger;
|
|
27
|
-
private readonly redis:
|
|
27
|
+
private readonly redis: RedisAdapter;
|
|
28
28
|
private readonly keys: KeyBuilderSS;
|
|
29
29
|
private redisError?: string;
|
|
30
30
|
private readonly flagSetsFilter: string[];
|
|
31
31
|
|
|
32
|
-
constructor(log: ILogger, keys: KeyBuilderSS, redis:
|
|
32
|
+
constructor(log: ILogger, keys: KeyBuilderSS, redis: RedisAdapter, splitFiltersValidation?: ISplitFiltersValidation) {
|
|
33
33
|
super();
|
|
34
34
|
this.log = log;
|
|
35
35
|
this.redis = redis;
|
|
@@ -192,10 +192,10 @@ 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.pipelineExec(listOfKeys.map(k => ['get', k])))
|
|
196
196
|
.then(processPipelineAnswer)
|
|
197
197
|
.then((splitDefinitions) => splitDefinitions.map((splitDefinition) => {
|
|
198
|
-
return JSON.parse(splitDefinition
|
|
198
|
+
return JSON.parse(splitDefinition);
|
|
199
199
|
}));
|
|
200
200
|
}
|
|
201
201
|
|
|
@@ -211,15 +211,18 @@ export class SplitsCacheInRedis extends AbstractSplitsCacheAsync {
|
|
|
211
211
|
}
|
|
212
212
|
|
|
213
213
|
/**
|
|
214
|
-
* Get list of
|
|
215
|
-
* The returned promise is resolved with the list of
|
|
216
|
-
* or rejected if
|
|
214
|
+
* Get list of feature flag names related to a given list of flag set names.
|
|
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.
|
|
217
217
|
*/
|
|
218
218
|
getNamesByFlagSets(flagSets: string[]): Promise<ISet<string>[]> {
|
|
219
|
-
return
|
|
220
|
-
|
|
221
|
-
|
|
222
|
-
|
|
219
|
+
return this.redis.pipelineExec(flagSets.map(flagSet => ['smembers', this.keys.buildFlagSetKey(flagSet)]))
|
|
220
|
+
.then((results) => results.map(([e, value], index) => {
|
|
221
|
+
if (e === null) return value;
|
|
222
|
+
|
|
223
|
+
this.log.error(LOG_PREFIX + `Could not read result from get members of flag set ${flagSets[index]} due to an error: ${e}`);
|
|
224
|
+
}))
|
|
225
|
+
.then(namesByFlagSets => namesByFlagSets.map(namesByFlagSet => new _Set(namesByFlagSet)));
|
|
223
226
|
}
|
|
224
227
|
|
|
225
228
|
/**
|
|
@@ -236,14 +239,14 @@ export class SplitsCacheInRedis extends AbstractSplitsCacheAsync {
|
|
|
236
239
|
|
|
237
240
|
ttCount = parseInt(ttCount as string, 10);
|
|
238
241
|
if (!isFiniteNumber(ttCount) || ttCount < 0) {
|
|
239
|
-
this.log.info(LOG_PREFIX + `Could not validate traffic type
|
|
242
|
+
this.log.info(LOG_PREFIX + `Could not validate traffic type existence of ${trafficType} due to data corruption of some sorts.`);
|
|
240
243
|
return false;
|
|
241
244
|
}
|
|
242
245
|
|
|
243
246
|
return ttCount > 0;
|
|
244
247
|
})
|
|
245
248
|
.catch(e => {
|
|
246
|
-
this.log.error(LOG_PREFIX + `Could not validate traffic type
|
|
249
|
+
this.log.error(LOG_PREFIX + `Could not validate traffic type existence of ${trafficType} due to an error: ${e}.`);
|
|
247
250
|
// If there is an error, bypass the validation so the event can get tracked.
|
|
248
251
|
return true;
|
|
249
252
|
});
|
|
@@ -3,13 +3,13 @@ import { Method, MultiConfigs, MultiMethodExceptions, MultiMethodLatencies } fro
|
|
|
3
3
|
import { KeyBuilderSS } from '../KeyBuilderSS';
|
|
4
4
|
import { ITelemetryCacheAsync } from '../types';
|
|
5
5
|
import { findLatencyIndex } from '../findLatencyIndex';
|
|
6
|
-
import { Redis } from 'ioredis';
|
|
7
6
|
import { getTelemetryConfigStats } from '../../sync/submitters/telemetrySubmitter';
|
|
8
7
|
import { CONSUMER_MODE, STORAGE_REDIS } from '../../utils/constants';
|
|
9
8
|
import { isNaNNumber, isString } from '../../utils/lang';
|
|
10
9
|
import { _Map } from '../../utils/lang/maps';
|
|
11
10
|
import { MAX_LATENCY_BUCKET_COUNT, newBuckets } from '../inMemory/TelemetryCacheInMemory';
|
|
12
11
|
import { parseLatencyField, parseExceptionField, parseMetadata } from '../utils';
|
|
12
|
+
import type { RedisAdapter } from './RedisAdapter';
|
|
13
13
|
|
|
14
14
|
export class TelemetryCacheInRedis implements ITelemetryCacheAsync {
|
|
15
15
|
|
|
@@ -19,7 +19,7 @@ export class TelemetryCacheInRedis implements ITelemetryCacheAsync {
|
|
|
19
19
|
* @param keys Key builder.
|
|
20
20
|
* @param redis Redis client.
|
|
21
21
|
*/
|
|
22
|
-
constructor(private readonly log: ILogger, private readonly keys: KeyBuilderSS, private readonly redis:
|
|
22
|
+
constructor(private readonly log: ILogger, private readonly keys: KeyBuilderSS, private readonly redis: RedisAdapter) { }
|
|
23
23
|
|
|
24
24
|
recordLatency(method: Method, latencyMs: number) {
|
|
25
25
|
const [key, field] = this.keys.buildLatencyKey(method, findLatencyIndex(latencyMs)).split('::');
|
|
@@ -1,21 +1,21 @@
|
|
|
1
1
|
import { IUniqueKeysCacheBase } from '../types';
|
|
2
|
-
import { Redis } from 'ioredis';
|
|
3
2
|
import { UniqueKeysCacheInMemory } from '../inMemory/UniqueKeysCacheInMemory';
|
|
4
3
|
import { setToArray } from '../../utils/lang/sets';
|
|
5
4
|
import { DEFAULT_CACHE_SIZE, REFRESH_RATE, TTL_REFRESH } from './constants';
|
|
6
5
|
import { LOG_PREFIX } from './constants';
|
|
7
6
|
import { ILogger } from '../../logger/types';
|
|
8
7
|
import { UniqueKeysItemSs } from '../../sync/submitters/types';
|
|
8
|
+
import type { RedisAdapter } from './RedisAdapter';
|
|
9
9
|
|
|
10
10
|
export class UniqueKeysCacheInRedis extends UniqueKeysCacheInMemory implements IUniqueKeysCacheBase {
|
|
11
11
|
|
|
12
12
|
private readonly log: ILogger;
|
|
13
13
|
private readonly key: string;
|
|
14
|
-
private readonly redis:
|
|
14
|
+
private readonly redis: RedisAdapter;
|
|
15
15
|
private readonly refreshRate: number;
|
|
16
16
|
private intervalId: any;
|
|
17
17
|
|
|
18
|
-
constructor(log: ILogger, key: string, redis:
|
|
18
|
+
constructor(log: ILogger, key: string, redis: RedisAdapter, uniqueKeysQueueSize = DEFAULT_CACHE_SIZE, refreshRate = REFRESH_RATE) {
|
|
19
19
|
super(uniqueKeysQueueSize);
|
|
20
20
|
this.log = log;
|
|
21
21
|
this.key = key;
|
|
@@ -177,14 +177,14 @@ export class SplitsCachePluggable extends AbstractSplitsCacheAsync {
|
|
|
177
177
|
}
|
|
178
178
|
|
|
179
179
|
/**
|
|
180
|
-
* Get list of
|
|
181
|
-
* The returned promise is resolved with the list of
|
|
182
|
-
*
|
|
183
|
-
|
|
180
|
+
* Get list of feature flag names related to a given list of flag set names.
|
|
181
|
+
* The returned promise is resolved with the list of feature flag names per flag set.
|
|
182
|
+
* It never rejects (If there is a wrapper error for some flag set, an empty set is returned for it).
|
|
183
|
+
*/
|
|
184
184
|
getNamesByFlagSets(flagSets: string[]): Promise<ISet<string>[]> {
|
|
185
185
|
return Promise.all(flagSets.map(flagSet => {
|
|
186
186
|
const flagSetKey = this.keys.buildFlagSetKey(flagSet);
|
|
187
|
-
return this.wrapper.getItems(flagSetKey);
|
|
187
|
+
return this.wrapper.getItems(flagSetKey).catch(() => []);
|
|
188
188
|
})).then(namesByFlagSets => namesByFlagSets.map(namesByFlagSet => new _Set(namesByFlagSet)));
|
|
189
189
|
}
|
|
190
190
|
|
|
@@ -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';
|
|
@@ -7,7 +7,7 @@ import { WARN_NOT_EXISTENT_SPLIT } from '../../logger/constants';
|
|
|
7
7
|
* 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.
|
|
8
8
|
* 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.
|
|
9
9
|
*/
|
|
10
|
-
export function
|
|
10
|
+
export function validateSplitExistence(log: ILogger, readinessManager: IReadinessManager, splitName: string, labelOrSplitObj: any, method: string): boolean {
|
|
11
11
|
if (readinessManager.isReady()) { // Only if it's ready we validate this, otherwise it may just be that the SDK is not ready yet.
|
|
12
12
|
if (labelOrSplitObj === SPLIT_NOT_FOUND || labelOrSplitObj == null) {
|
|
13
13
|
log.warn(WARN_NOT_EXISTENT_SPLIT, [method, splitName]);
|
|
@@ -7,14 +7,14 @@ import { MaybeThenable } from '../../dtos/types';
|
|
|
7
7
|
import { ILogger } from '../../logger/types';
|
|
8
8
|
import { WARN_NOT_EXISTENT_TT } from '../../logger/constants';
|
|
9
9
|
|
|
10
|
-
function
|
|
10
|
+
function logTTExistenceWarning(log: ILogger, maybeTT: string, method: string) {
|
|
11
11
|
log.warn(WARN_NOT_EXISTENT_TT, [method, maybeTT]);
|
|
12
12
|
}
|
|
13
13
|
|
|
14
14
|
/**
|
|
15
15
|
* Separated from the previous method since on some cases it'll be async.
|
|
16
16
|
*/
|
|
17
|
-
export function
|
|
17
|
+
export function validateTrafficTypeExistence(log: ILogger, readinessManager: IReadinessManager, splitsCache: ISplitsCacheBase, mode: SDKMode, maybeTT: string, method: string): MaybeThenable<boolean> {
|
|
18
18
|
|
|
19
19
|
// If not ready or in localhost mode, we won't run the validation
|
|
20
20
|
if (!readinessManager.isReady() || mode === LOCALHOST_MODE) return true;
|
|
@@ -23,11 +23,11 @@ export function validateTrafficTypeExistance(log: ILogger, readinessManager: IRe
|
|
|
23
23
|
|
|
24
24
|
if (thenable(res)) {
|
|
25
25
|
return res.then(function (isValid) {
|
|
26
|
-
if (!isValid)
|
|
26
|
+
if (!isValid) logTTExistenceWarning(log, maybeTT, method);
|
|
27
27
|
return isValid; // propagate result
|
|
28
28
|
});
|
|
29
29
|
} else {
|
|
30
|
-
if (!res)
|
|
30
|
+
if (!res) logTTExistenceWarning(log, maybeTT, method);
|
|
31
31
|
return res;
|
|
32
32
|
}
|
|
33
33
|
}
|
|
@@ -8,13 +8,10 @@ function asyncFunction(data: any): Promise<any> {
|
|
|
8
8
|
}
|
|
9
9
|
|
|
10
10
|
const IDENTITY_METHODS: string[] = [];
|
|
11
|
-
const ASYNC_METHODS = ['rpush', 'hincrby'];
|
|
12
|
-
const PIPELINE_METHODS = ['rpush', 'hincrby'];
|
|
11
|
+
const ASYNC_METHODS = ['rpush', 'hincrby', 'pipelineExec'];
|
|
13
12
|
|
|
14
13
|
export class RedisMock {
|
|
15
14
|
|
|
16
|
-
private pipelineMethods: any = { exec: jest.fn(asyncFunction) };
|
|
17
|
-
|
|
18
15
|
constructor() {
|
|
19
16
|
IDENTITY_METHODS.forEach(method => {
|
|
20
17
|
this[method] = jest.fn(identityFunction);
|
|
@@ -22,12 +19,5 @@ export class RedisMock {
|
|
|
22
19
|
ASYNC_METHODS.forEach(method => {
|
|
23
20
|
this[method] = jest.fn(asyncFunction);
|
|
24
21
|
});
|
|
25
|
-
PIPELINE_METHODS.forEach(method => {
|
|
26
|
-
this.pipelineMethods[method] = this[method];
|
|
27
|
-
});
|
|
28
|
-
|
|
29
|
-
this.pipeline = jest.fn(() => {return this.pipelineMethods;});
|
|
30
22
|
}
|
|
31
|
-
|
|
32
|
-
|
|
33
23
|
}
|
|
@@ -1,15 +1,15 @@
|
|
|
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 { StoredEventWithMetadata } from '../../sync/submitters/types';
|
|
6
|
+
import type { RedisAdapter } from './RedisAdapter';
|
|
7
7
|
export declare class EventsCacheInRedis implements IEventsCacheAsync {
|
|
8
8
|
private readonly log;
|
|
9
9
|
private readonly key;
|
|
10
10
|
private readonly redis;
|
|
11
11
|
private readonly metadata;
|
|
12
|
-
constructor(log: ILogger, key: string, redis:
|
|
12
|
+
constructor(log: ILogger, key: string, redis: RedisAdapter, metadata: IMetadata);
|
|
13
13
|
/**
|
|
14
14
|
* Add a new event object into the queue.
|
|
15
15
|
* Unlike `impressions::track`, result promise is never rejected.
|
|
@@ -1,14 +1,15 @@
|
|
|
1
|
-
|
|
1
|
+
/// <reference types="ioredis" />
|
|
2
2
|
import { ILogger } from '../../logger/types';
|
|
3
3
|
import { ImpressionCountsPayload } from '../../sync/submitters/types';
|
|
4
4
|
import { ImpressionCountsCacheInMemory } from '../inMemory/ImpressionCountsCacheInMemory';
|
|
5
|
+
import type { RedisAdapter } from './RedisAdapter';
|
|
5
6
|
export declare class ImpressionCountsCacheInRedis extends ImpressionCountsCacheInMemory {
|
|
6
7
|
private readonly log;
|
|
7
8
|
private readonly key;
|
|
8
9
|
private readonly redis;
|
|
9
10
|
private readonly refreshRate;
|
|
10
11
|
private intervalId;
|
|
11
|
-
constructor(log: ILogger, key: string, redis:
|
|
12
|
+
constructor(log: ILogger, key: string, redis: RedisAdapter, impressionCountsCacheSize?: number, refreshRate?: number);
|
|
12
13
|
private postImpressionCountsInRedis;
|
|
13
14
|
start(): void;
|
|
14
15
|
stop(): Promise<boolean | import("ioredis").BooleanResponse | undefined>;
|