@splitsoftware/splitio-commons 1.10.1-rc.4 → 1.12.0-rc.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (109) hide show
  1. package/CHANGES.txt +16 -10
  2. package/cjs/evaluator/index.js +22 -3
  3. package/cjs/logger/constants.js +6 -4
  4. package/cjs/logger/messages/warn.js +5 -3
  5. package/cjs/sdkClient/client.js +19 -16
  6. package/cjs/sdkClient/clientInputValidation.js +16 -16
  7. package/cjs/sdkFactory/index.js +1 -1
  8. package/cjs/sdkManager/index.js +14 -13
  9. package/cjs/storages/KeyBuilder.js +1 -1
  10. package/cjs/storages/inLocalStorage/SplitsCacheInLocal.js +3 -10
  11. package/cjs/storages/inMemory/SplitsCacheInMemory.js +2 -10
  12. package/cjs/storages/inRedis/RedisAdapter.js +32 -13
  13. package/cjs/storages/inRedis/SegmentsCacheInRedis.js +2 -2
  14. package/cjs/storages/inRedis/SplitsCacheInRedis.js +38 -22
  15. package/cjs/storages/inRedis/index.js +1 -1
  16. package/cjs/storages/pluggable/SplitsCachePluggable.js +27 -11
  17. package/cjs/storages/pluggable/index.js +1 -1
  18. package/cjs/utils/constants/index.js +16 -2
  19. package/cjs/utils/inputValidation/index.js +5 -5
  20. package/cjs/utils/inputValidation/{splitExistance.js → splitExistence.js} +3 -3
  21. package/cjs/utils/inputValidation/{trafficTypeExistance.js → trafficTypeExistence.js} +6 -6
  22. package/cjs/utils/lang/sets.js +11 -1
  23. package/cjs/utils/settingsValidation/index.js +1 -1
  24. package/cjs/utils/settingsValidation/splitFilters.js +25 -17
  25. package/esm/evaluator/index.js +23 -4
  26. package/esm/logger/constants.js +4 -2
  27. package/esm/logger/messages/warn.js +5 -3
  28. package/esm/sdkClient/client.js +20 -17
  29. package/esm/sdkClient/clientInputValidation.js +18 -18
  30. package/esm/sdkFactory/index.js +1 -1
  31. package/esm/sdkManager/index.js +11 -10
  32. package/esm/storages/KeyBuilder.js +1 -1
  33. package/esm/storages/inLocalStorage/SplitsCacheInLocal.js +4 -11
  34. package/esm/storages/inMemory/SplitsCacheInMemory.js +3 -11
  35. package/esm/storages/inRedis/RedisAdapter.js +32 -13
  36. package/esm/storages/inRedis/SegmentsCacheInRedis.js +2 -2
  37. package/esm/storages/inRedis/SplitsCacheInRedis.js +40 -24
  38. package/esm/storages/inRedis/index.js +1 -1
  39. package/esm/storages/pluggable/SplitsCachePluggable.js +29 -13
  40. package/esm/storages/pluggable/index.js +1 -1
  41. package/esm/utils/constants/index.js +14 -0
  42. package/esm/utils/inputValidation/index.js +2 -2
  43. package/esm/utils/inputValidation/{splitExistance.js → splitExistence.js} +1 -1
  44. package/esm/utils/inputValidation/{trafficTypeExistance.js → trafficTypeExistence.js} +4 -4
  45. package/esm/utils/lang/sets.js +9 -0
  46. package/esm/utils/settingsValidation/index.js +1 -1
  47. package/esm/utils/settingsValidation/splitFilters.js +17 -9
  48. package/package.json +1 -1
  49. package/src/evaluator/index.ts +28 -5
  50. package/src/logger/constants.ts +4 -2
  51. package/src/logger/messages/warn.ts +9 -7
  52. package/src/sdkClient/client.ts +18 -18
  53. package/src/sdkClient/clientInputValidation.ts +18 -18
  54. package/src/sdkFactory/index.ts +1 -1
  55. package/src/sdkFactory/types.ts +3 -7
  56. package/src/sdkManager/index.ts +14 -14
  57. package/src/storages/AbstractSplitsCacheAsync.ts +1 -1
  58. package/src/storages/AbstractSplitsCacheSync.ts +1 -1
  59. package/src/storages/KeyBuilder.ts +1 -1
  60. package/src/storages/inLocalStorage/SplitsCacheInLocal.ts +8 -15
  61. package/src/storages/inMemory/SplitsCacheInMemory.ts +6 -14
  62. package/src/storages/inRedis/EventsCacheInRedis.ts +3 -3
  63. package/src/storages/inRedis/ImpressionCountsCacheInRedis.ts +3 -3
  64. package/src/storages/inRedis/ImpressionsCacheInRedis.ts +3 -3
  65. package/src/storages/inRedis/RedisAdapter.ts +38 -16
  66. package/src/storages/inRedis/SegmentsCacheInRedis.ts +5 -5
  67. package/src/storages/inRedis/SplitsCacheInRedis.ts +49 -28
  68. package/src/storages/inRedis/TelemetryCacheInRedis.ts +2 -2
  69. package/src/storages/inRedis/UniqueKeysCacheInRedis.ts +3 -3
  70. package/src/storages/inRedis/index.ts +1 -1
  71. package/src/storages/pluggable/SplitsCachePluggable.ts +35 -13
  72. package/src/storages/pluggable/index.ts +1 -1
  73. package/src/storages/types.ts +5 -5
  74. package/src/trackers/impressionObserver/utils.ts +1 -1
  75. package/src/types.ts +0 -2
  76. package/src/utils/constants/index.ts +16 -0
  77. package/src/utils/inputValidation/index.ts +2 -2
  78. package/src/utils/inputValidation/{splitExistance.ts → splitExistence.ts} +1 -1
  79. package/src/utils/inputValidation/{trafficTypeExistance.ts → trafficTypeExistence.ts} +4 -4
  80. package/src/utils/lang/sets.ts +9 -1
  81. package/src/utils/redis/RedisMock.ts +1 -3
  82. package/src/utils/settingsValidation/index.ts +1 -1
  83. package/src/utils/settingsValidation/splitFilters.ts +19 -11
  84. package/types/evaluator/index.d.ts +1 -1
  85. package/types/logger/constants.d.ts +4 -2
  86. package/types/sdkFactory/types.d.ts +3 -3
  87. package/types/sdkManager/index.d.ts +2 -3
  88. package/types/storages/AbstractSplitsCacheAsync.d.ts +1 -1
  89. package/types/storages/AbstractSplitsCacheSync.d.ts +1 -1
  90. package/types/storages/inLocalStorage/SplitsCacheInLocal.d.ts +1 -1
  91. package/types/storages/inMemory/SplitsCacheInMemory.d.ts +1 -1
  92. package/types/storages/inRedis/EventsCacheInRedis.d.ts +2 -2
  93. package/types/storages/inRedis/ImpressionCountsCacheInRedis.d.ts +3 -2
  94. package/types/storages/inRedis/ImpressionsCacheInRedis.d.ts +2 -2
  95. package/types/storages/inRedis/RedisAdapter.d.ts +1 -1
  96. package/types/storages/inRedis/SegmentsCacheInRedis.d.ts +3 -3
  97. package/types/storages/inRedis/SplitsCacheInRedis.d.ts +10 -14
  98. package/types/storages/inRedis/TelemetryCacheInRedis.d.ts +2 -2
  99. package/types/storages/inRedis/UniqueKeysCacheInRedis.d.ts +3 -2
  100. package/types/storages/pluggable/SplitsCachePluggable.d.ts +10 -9
  101. package/types/storages/types.d.ts +5 -5
  102. package/types/trackers/impressionObserver/utils.d.ts +1 -1
  103. package/types/types.d.ts +0 -2
  104. package/types/utils/constants/index.d.ts +12 -0
  105. package/types/utils/inputValidation/index.d.ts +2 -2
  106. package/types/utils/inputValidation/splitExistence.d.ts +7 -0
  107. package/types/utils/inputValidation/trafficTypeExistence.d.ts +9 -0
  108. package/types/utils/lang/sets.d.ts +1 -0
  109. package/types/utils/settingsValidation/splitFilters.d.ts +3 -2
@@ -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', 'pipeline', 'expire', 'mget', 'lrange', 'ltrim', 'hset'];
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
- METHODS_TO_PROMISE_WRAP.forEach(method => {
75
- const originalMethod = instance[method];
76
-
77
- instance[method] = function () {
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 + `Executing ${method}.`);
82
- // Return original method
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(LOG_PREFIX + `${method} operation threw an error or exceeded configured timeout of ${instance._options.operationTimeout}ms. Message: ${err}`);
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((res, rej) => {
108
+ return new Promise((resolve, reject) => {
107
109
  instance._notReadyCommandsQueue.unshift({
108
- resolve: res,
109
- reject: rej,
110
+ resolve,
111
+ reject,
110
112
  command: commandWrapper,
111
- name: method.toUpperCase()
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 deferedDisconnect() {
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
 
@@ -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: Redis;
11
+ private readonly redis: RedisAdapter;
12
12
  private readonly keys: KeyBuilderSS;
13
13
 
14
- constructor(log: ILogger, keys: KeyBuilderSS, redis: Redis) {
14
+ constructor(log: ILogger, keys: KeyBuilderSS, redis: RedisAdapter) {
15
15
  this.log = log;
16
16
  this.redis = redis;
17
17
  this.keys = keys;
@@ -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/review. It is not being used.
75
+ // @TODO remove or implement. It is not being used.
76
76
  clear() {
77
- return this.redis.flushdb().then(status => status === 'OK');
77
+ return Promise.resolve();
78
78
  }
79
79
  }
@@ -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
- import { ISplit } from '../../dtos/types';
5
+ import { ISplit, ISplitFiltersValidation } from '../../dtos/types';
7
6
  import { AbstractSplitsCacheAsync } from '../AbstractSplitsCacheAsync';
8
- import { ISet, _Set } from '../../utils/lang/sets';
7
+ import { ISet, _Set, returnDifference } 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,15 +24,17 @@ 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: Redis;
27
+ private readonly redis: RedisAdapter;
28
28
  private readonly keys: KeyBuilderSS;
29
29
  private redisError?: string;
30
+ private readonly flagSetsFilter: string[];
30
31
 
31
- constructor(log: ILogger, keys: KeyBuilderSS, redis: Redis) {
32
+ constructor(log: ILogger, keys: KeyBuilderSS, redis: RedisAdapter, splitFiltersValidation?: ISplitFiltersValidation) {
32
33
  super();
33
34
  this.log = log;
34
35
  this.redis = redis;
35
36
  this.keys = keys;
37
+ this.flagSetsFilter = splitFiltersValidation ? splitFiltersValidation.groupedFilters.bySet : [];
36
38
 
37
39
  // There is no need to listen for redis 'error' event, because in that case ioredis calls will be rejected and handled by redis storage adapters.
38
40
  // But it is done just to avoid getting the ioredis message `Unhandled error event`.
@@ -57,6 +59,24 @@ export class SplitsCacheInRedis extends AbstractSplitsCacheAsync {
57
59
  return this.redis.incr(ttKey);
58
60
  }
59
61
 
62
+ private _updateFlagSets(featureFlagName: string, flagSetsOfRemovedFlag?: string[], flagSetsOfAddedFlag?: string[]) {
63
+ const removeFromFlagSets = returnDifference(flagSetsOfRemovedFlag, flagSetsOfAddedFlag);
64
+
65
+ let addToFlagSets = returnDifference(flagSetsOfAddedFlag, flagSetsOfRemovedFlag);
66
+ if (this.flagSetsFilter.length > 0) {
67
+ addToFlagSets = addToFlagSets.filter(flagSet => {
68
+ return this.flagSetsFilter.some(filterFlagSet => filterFlagSet === flagSet);
69
+ });
70
+ }
71
+
72
+ const items = [featureFlagName];
73
+
74
+ return Promise.all([
75
+ ...removeFromFlagSets.map(flagSetName => this.redis.srem(this.keys.buildFlagSetKey(flagSetName), items)),
76
+ ...addToFlagSets.map(flagSetName => this.redis.sadd(this.keys.buildFlagSetKey(flagSetName), items))
77
+ ]);
78
+ }
79
+
60
80
  /**
61
81
  * Add a given split.
62
82
  * The returned promise is resolved when the operation success
@@ -66,16 +86,16 @@ export class SplitsCacheInRedis extends AbstractSplitsCacheAsync {
66
86
  const splitKey = this.keys.buildSplitKey(name);
67
87
  return this.redis.get(splitKey).then(splitFromStorage => {
68
88
 
69
- // handling parsing errors
70
- let parsedPreviousSplit: ISplit, newStringifiedSplit;
89
+ // handling parsing error
90
+ let parsedPreviousSplit: ISplit, stringifiedNewSplit;
71
91
  try {
72
92
  parsedPreviousSplit = splitFromStorage ? JSON.parse(splitFromStorage) : undefined;
73
- newStringifiedSplit = JSON.stringify(split);
93
+ stringifiedNewSplit = JSON.stringify(split);
74
94
  } catch (e) {
75
95
  throw new Error('Error parsing feature flag definition: ' + e);
76
96
  }
77
97
 
78
- return this.redis.set(splitKey, newStringifiedSplit).then(() => {
98
+ return this.redis.set(splitKey, stringifiedNewSplit).then(() => {
79
99
  // avoid unnecessary increment/decrement operations
80
100
  if (parsedPreviousSplit && parsedPreviousSplit.trafficTypeName === split.trafficTypeName) return;
81
101
 
@@ -83,7 +103,7 @@ export class SplitsCacheInRedis extends AbstractSplitsCacheAsync {
83
103
  return this._incrementCounts(split).then(() => {
84
104
  if (parsedPreviousSplit) return this._decrementCounts(parsedPreviousSplit);
85
105
  });
86
- });
106
+ }).then(() => this._updateFlagSets(name, parsedPreviousSplit && parsedPreviousSplit.sets, split.sets));
87
107
  }).then(() => true);
88
108
  }
89
109
 
@@ -101,11 +121,12 @@ export class SplitsCacheInRedis extends AbstractSplitsCacheAsync {
101
121
  * The returned promise is resolved when the operation success, with 1 or 0 indicating if the split existed or not.
102
122
  * or rejected if it fails (e.g., redis operation fails).
103
123
  */
104
- removeSplit(name: string): Promise<number> {
124
+ removeSplit(name: string) {
105
125
  return this.getSplit(name).then((split) => {
106
126
  if (split) {
107
- this._decrementCounts(split);
127
+ return this._decrementCounts(split).then(() => this._updateFlagSets(name, split.sets));
108
128
  }
129
+ }).then(() => {
109
130
  return this.redis.del(this.keys.buildSplitKey(name));
110
131
  });
111
132
  }
@@ -174,7 +195,7 @@ export class SplitsCacheInRedis extends AbstractSplitsCacheAsync {
174
195
  .then((listOfKeys) => this.redis.pipeline(listOfKeys.map(k => ['get', k])).exec())
175
196
  .then(processPipelineAnswer)
176
197
  .then((splitDefinitions) => splitDefinitions.map((splitDefinition) => {
177
- return JSON.parse(splitDefinition as string);
198
+ return JSON.parse(splitDefinition);
178
199
  }));
179
200
  }
180
201
 
@@ -190,14 +211,18 @@ export class SplitsCacheInRedis extends AbstractSplitsCacheAsync {
190
211
  }
191
212
 
192
213
  /**
193
- * Get list of split names related to a given flag set names list.
194
- * The returned promise is resolved with the list of split names,
195
- * or rejected if wrapper operation fails.
196
- * @todo this is a no-op method to be implemented
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 (e.g., timeout).
197
217
  */
198
- getNamesByFlagSets(): Promise<ISet<string>> {
199
- // eslint-disable-next-line @typescript-eslint/no-unused-vars
200
- return new Promise(flagSets => new _Set([]));
218
+ getNamesByFlagSets(flagSets: string[]): Promise<ISet<string>[]> {
219
+ return this.redis.pipeline(flagSets.map(flagSet => ['smembers', this.keys.buildFlagSetKey(flagSet)])).exec()
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)));
201
226
  }
202
227
 
203
228
  /**
@@ -214,26 +239,22 @@ export class SplitsCacheInRedis extends AbstractSplitsCacheAsync {
214
239
 
215
240
  ttCount = parseInt(ttCount as string, 10);
216
241
  if (!isFiniteNumber(ttCount) || ttCount < 0) {
217
- this.log.info(LOG_PREFIX + `Could not validate traffic type existance of ${trafficType} due to data corruption of some sorts.`);
242
+ this.log.info(LOG_PREFIX + `Could not validate traffic type existence of ${trafficType} due to data corruption of some sorts.`);
218
243
  return false;
219
244
  }
220
245
 
221
246
  return ttCount > 0;
222
247
  })
223
248
  .catch(e => {
224
- this.log.error(LOG_PREFIX + `Could not validate traffic type existance of ${trafficType} due to an error: ${e}.`);
249
+ this.log.error(LOG_PREFIX + `Could not validate traffic type existence of ${trafficType} due to an error: ${e}.`);
225
250
  // If there is an error, bypass the validation so the event can get tracked.
226
251
  return true;
227
252
  });
228
253
  }
229
254
 
230
- /**
231
- * Delete everything in the current database.
232
- *
233
- * @NOTE documentation says it never fails.
234
- */
255
+ // @TODO remove or implement. It is not being used.
235
256
  clear() {
236
- return this.redis.flushdb().then(status => status === 'OK');
257
+ return Promise.resolve();
237
258
  }
238
259
 
239
260
  /**
@@ -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: 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: 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: Redis, uniqueKeysQueueSize = DEFAULT_CACHE_SIZE, refreshRate = REFRESH_RATE) {
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;
@@ -45,7 +45,7 @@ export function InRedisStorage(options: InRedisStorageOptions = {}): IStorageAsy
45
45
  });
46
46
 
47
47
  return {
48
- splits: new SplitsCacheInRedis(log, keys, redisClient),
48
+ splits: new SplitsCacheInRedis(log, keys, redisClient, settings.sync.__splitFiltersValidation),
49
49
  segments: new SegmentsCacheInRedis(log, keys, redisClient),
50
50
  impressions: new ImpressionsCacheInRedis(log, keys.buildImpressionsKey(), redisClient, metadata),
51
51
  impressionCounts: impressionCountsCache,
@@ -2,10 +2,10 @@ import { isFiniteNumber, isNaNNumber } from '../../utils/lang';
2
2
  import { KeyBuilder } from '../KeyBuilder';
3
3
  import { IPluggableStorageWrapper } from '../types';
4
4
  import { ILogger } from '../../logger/types';
5
- import { ISplit } from '../../dtos/types';
5
+ import { ISplit, ISplitFiltersValidation } from '../../dtos/types';
6
6
  import { LOG_PREFIX } from './constants';
7
7
  import { AbstractSplitsCacheAsync } from '../AbstractSplitsCacheAsync';
8
- import { ISet, _Set } from '../../utils/lang/sets';
8
+ import { ISet, _Set, returnDifference } from '../../utils/lang/sets';
9
9
 
10
10
  /**
11
11
  * ISplitsCacheAsync implementation for pluggable storages.
@@ -15,6 +15,7 @@ export class SplitsCachePluggable extends AbstractSplitsCacheAsync {
15
15
  private readonly log: ILogger;
16
16
  private readonly keys: KeyBuilder;
17
17
  private readonly wrapper: IPluggableStorageWrapper;
18
+ private readonly flagSetsFilter: string[];
18
19
 
19
20
  /**
20
21
  * Create a SplitsCache that uses a storage wrapper.
@@ -22,11 +23,12 @@ export class SplitsCachePluggable extends AbstractSplitsCacheAsync {
22
23
  * @param keys Key builder.
23
24
  * @param wrapper Adapted wrapper storage.
24
25
  */
25
- constructor(log: ILogger, keys: KeyBuilder, wrapper: IPluggableStorageWrapper) {
26
+ constructor(log: ILogger, keys: KeyBuilder, wrapper: IPluggableStorageWrapper, splitFiltersValidation?: ISplitFiltersValidation) {
26
27
  super();
27
28
  this.log = log;
28
29
  this.keys = keys;
29
30
  this.wrapper = wrapper;
31
+ this.flagSetsFilter = splitFiltersValidation ? splitFiltersValidation.groupedFilters.bySet : [];
30
32
  }
31
33
 
32
34
  private _decrementCounts(split: ISplit) {
@@ -41,6 +43,24 @@ export class SplitsCachePluggable extends AbstractSplitsCacheAsync {
41
43
  return this.wrapper.incr(ttKey);
42
44
  }
43
45
 
46
+ private _updateFlagSets(featureFlagName: string, flagSetsOfRemovedFlag?: string[], flagSetsOfAddedFlag?: string[]) {
47
+ const removeFromFlagSets = returnDifference(flagSetsOfRemovedFlag, flagSetsOfAddedFlag);
48
+
49
+ let addToFlagSets = returnDifference(flagSetsOfAddedFlag, flagSetsOfRemovedFlag);
50
+ if (this.flagSetsFilter.length > 0) {
51
+ addToFlagSets = addToFlagSets.filter(flagSet => {
52
+ return this.flagSetsFilter.some(filterFlagSet => filterFlagSet === flagSet);
53
+ });
54
+ }
55
+
56
+ const items = [featureFlagName];
57
+
58
+ return Promise.all([
59
+ ...removeFromFlagSets.map(flagSetName => this.wrapper.removeItems(this.keys.buildFlagSetKey(flagSetName), items)),
60
+ ...addToFlagSets.map(flagSetName => this.wrapper.addItems(this.keys.buildFlagSetKey(flagSetName), items))
61
+ ]);
62
+ }
63
+
44
64
  /**
45
65
  * Add a given split.
46
66
  * The returned promise is resolved when the operation success
@@ -67,7 +87,7 @@ export class SplitsCachePluggable extends AbstractSplitsCacheAsync {
67
87
  return this._incrementCounts(split).then(() => {
68
88
  if (parsedPreviousSplit) return this._decrementCounts(parsedPreviousSplit);
69
89
  });
70
- });
90
+ }).then(() => this._updateFlagSets(name, parsedPreviousSplit && parsedPreviousSplit.sets, split.sets));
71
91
  }).then(() => true);
72
92
  }
73
93
 
@@ -88,8 +108,9 @@ export class SplitsCachePluggable extends AbstractSplitsCacheAsync {
88
108
  removeSplit(name: string) {
89
109
  return this.getSplit(name).then((split) => {
90
110
  if (split) {
91
- this._decrementCounts(split);
111
+ return this._decrementCounts(split).then(() => this._updateFlagSets(name, split.sets));
92
112
  }
113
+ }).then(() => {
93
114
  return this.wrapper.del(this.keys.buildSplitKey(name));
94
115
  });
95
116
  }
@@ -156,14 +177,15 @@ export class SplitsCachePluggable extends AbstractSplitsCacheAsync {
156
177
  }
157
178
 
158
179
  /**
159
- * Get list of split names related to a given flag set names list.
160
- * The returned promise is resolved with the list of split names,
161
- * or rejected if wrapper operation fails.
162
- * @todo this is a no-op method to be implemented
163
- */
164
- getNamesByFlagSets(): Promise<ISet<string>> {
165
- // eslint-disable-next-line @typescript-eslint/no-unused-vars
166
- return new Promise(flagSets => new _Set([]));
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
+ getNamesByFlagSets(flagSets: string[]): Promise<ISet<string>[]> {
185
+ return Promise.all(flagSets.map(flagSet => {
186
+ const flagSetKey = this.keys.buildFlagSetKey(flagSet);
187
+ return this.wrapper.getItems(flagSetKey).catch(() => []);
188
+ })).then(namesByFlagSets => namesByFlagSets.map(namesByFlagSet => new _Set(namesByFlagSet)));
167
189
  }
168
190
 
169
191
  /**
@@ -105,7 +105,7 @@ export function PluggableStorage(options: PluggableStorageOptions): IStorageAsyn
105
105
  });
106
106
 
107
107
  return {
108
- splits: new SplitsCachePluggable(log, keys, wrapper),
108
+ splits: new SplitsCachePluggable(log, keys, wrapper, settings.sync.__splitFiltersValidation),
109
109
  segments: new SegmentsCachePluggable(log, keys, wrapper),
110
110
  impressions: isPartialConsumer ? new ImpressionsCacheInMemory(impressionsQueueSize) : new ImpressionsCachePluggable(log, keys.buildImpressionsKey(), wrapper, metadata),
111
111
  impressionCounts: impressionCountsCache,
@@ -44,10 +44,10 @@ export interface IPluggableStorageWrapper {
44
44
  *
45
45
  * @function del
46
46
  * @param {string} key Item to delete
47
- * @returns {Promise<void>} A promise that resolves if the operation success, whether the key existed and was removed or it didn't exist.
47
+ * @returns {Promise<boolean>} A promise that resolves if the operation success, whether the key existed and was removed (resolves with true) or it didn't exist (resolves with false).
48
48
  * The promise rejects if the operation fails, for example, if there is a connection error.
49
49
  */
50
- del: (key: string) => Promise<boolean | void>
50
+ del: (key: string) => Promise<boolean>
51
51
  /**
52
52
  * Returns all keys matching the given prefix.
53
53
  *
@@ -210,7 +210,7 @@ export interface ISplitsCacheBase {
210
210
  // should never reject or throw an exception. Instead return false by default, to avoid emitting SDK_READY_FROM_CACHE.
211
211
  checkCache(): MaybeThenable<boolean>,
212
212
  killLocally(name: string, defaultTreatment: string, changeNumber: number): MaybeThenable<boolean>,
213
- getNamesByFlagSets(flagSets: string[]): MaybeThenable<ISet<string>>
213
+ getNamesByFlagSets(flagSets: string[]): MaybeThenable<ISet<string>[]>
214
214
  }
215
215
 
216
216
  export interface ISplitsCacheSync extends ISplitsCacheBase {
@@ -227,7 +227,7 @@ export interface ISplitsCacheSync extends ISplitsCacheBase {
227
227
  clear(): void,
228
228
  checkCache(): boolean,
229
229
  killLocally(name: string, defaultTreatment: string, changeNumber: number): boolean,
230
- getNamesByFlagSets(flagSets: string[]): ISet<string>
230
+ getNamesByFlagSets(flagSets: string[]): ISet<string>[]
231
231
  }
232
232
 
233
233
  export interface ISplitsCacheAsync extends ISplitsCacheBase {
@@ -244,7 +244,7 @@ export interface ISplitsCacheAsync extends ISplitsCacheBase {
244
244
  clear(): Promise<boolean | void>,
245
245
  checkCache(): Promise<boolean>,
246
246
  killLocally(name: string, defaultTreatment: string, changeNumber: number): Promise<boolean>,
247
- getNamesByFlagSets(flagSets: string[]): Promise<ISet<string>>
247
+ getNamesByFlagSets(flagSets: string[]): Promise<ISet<string>[]>
248
248
  }
249
249
 
250
250
  /** Segments cache */
@@ -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
  }
package/src/types.ts CHANGED
@@ -197,8 +197,6 @@ interface ISharedSettings {
197
197
  * List of feature flag filters. These filters are used to fetch a subset of the feature flag definitions in your environment, in order to reduce the delay of the SDK to be ready.
198
198
  * This configuration is only meaningful when the SDK is working in "standalone" mode.
199
199
  *
200
- * At the moment, only one type of feature flag filter is supported: by name.
201
- *
202
200
  * Example:
203
201
  * `splitFilter: [
204
202
  * { type: 'byName', values: ['my_feature_flag_1', 'my_feature_flag_2'] }, // will fetch feature flags named 'my_feature_flag_1' and 'my_feature_flag_2'
@@ -39,6 +39,22 @@ export const CONSENT_GRANTED = 'GRANTED'; // The user has granted consent for tr
39
39
  export const CONSENT_DECLINED = 'DECLINED'; // The user has declined consent for tracking events and impressions
40
40
  export const CONSENT_UNKNOWN = 'UNKNOWN'; // The user has neither granted nor declined consent for tracking events and impressions
41
41
 
42
+ // Client method names
43
+ export const GET_TREATMENT = 'getTreatment';
44
+ export const GET_TREATMENTS = 'getTreatments';
45
+ export const GET_TREATMENT_WITH_CONFIG = 'getTreatmentWithConfig';
46
+ export const GET_TREATMENTS_WITH_CONFIG = 'getTreatmentsWithConfig';
47
+ export const GET_TREATMENTS_BY_FLAG_SET = 'getTreatmentsByFlagSet';
48
+ export const GET_TREATMENTS_BY_FLAG_SETS = 'getTreatmentsByFlagSets';
49
+ export const GET_TREATMENTS_WITH_CONFIG_BY_FLAG_SET = 'getTreatmentsWithConfigByFlagSet';
50
+ export const GET_TREATMENTS_WITH_CONFIG_BY_FLAG_SETS = 'getTreatmentsWithConfigByFlagSets';
51
+ export const TRACK_FN_LABEL = 'track';
52
+
53
+ // Manager method names
54
+ export const SPLIT_FN_LABEL = 'split';
55
+ export const SPLITS_FN_LABEL = 'splits';
56
+ export const NAMES_FN_LABEL = 'names';
57
+
42
58
  // Telemetry
43
59
  export const QUEUED = 0;
44
60
  export const DROPPED = 1;
@@ -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 { validateSplitExistance } from './splitExistance';
12
- export { validateTrafficTypeExistance } from './trafficTypeExistance';
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 validateSplitExistance(log: ILogger, readinessManager: IReadinessManager, splitName: string, labelOrSplitObj: any, method: string): boolean {
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 logTTExistanceWarning(log: ILogger, maybeTT: string, method: string) {
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 validateTrafficTypeExistance(log: ILogger, readinessManager: IReadinessManager, splitsCache: ISplitsCacheBase, mode: SDKMode, maybeTT: string, method: string): MaybeThenable<boolean> {
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) logTTExistanceWarning(log, maybeTT, method);
26
+ if (!isValid) logTTExistenceWarning(log, maybeTT, method);
27
27
  return isValid; // propagate result
28
28
  });
29
29
  } else {
30
- if (!res) logTTExistanceWarning(log, maybeTT, method);
30
+ if (!res) logTTExistenceWarning(log, maybeTT, method);
31
31
  return res;
32
32
  }
33
33
  }
@@ -114,8 +114,16 @@ export const _Set = __getSetConstructor();
114
114
 
115
115
  export function returnSetsUnion<T>(set: ISet<T>, set2: ISet<T>): ISet<T> {
116
116
  const result = new _Set(setToArray(set));
117
- set2.forEach( value => {
117
+ set2.forEach(value => {
118
118
  result.add(value);
119
119
  });
120
120
  return result;
121
121
  }
122
+
123
+ export function returnDifference<T>(list: T[] = [], list2: T[] = []): T[] {
124
+ const result = new _Set(list);
125
+ list2.forEach(item => {
126
+ result.delete(item);
127
+ });
128
+ return setToArray(result);
129
+ }