@splitsoftware/splitio-commons 1.12.1-rc.4 → 1.12.1-rc.5
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 +1 -1
- package/cjs/logger/constants.js +3 -3
- package/cjs/logger/messages/warn.js +2 -2
- package/cjs/sdkClient/client.js +11 -8
- package/cjs/sdkClient/clientInputValidation.js +6 -6
- package/cjs/sdkManager/index.js +6 -6
- package/cjs/storages/KeyBuilder.js +13 -1
- package/cjs/storages/KeyBuilderCS.js +1 -4
- package/cjs/storages/inLocalStorage/SplitsCacheInLocal.js +12 -16
- package/cjs/storages/inLocalStorage/index.js +1 -1
- package/cjs/storages/inRedis/SplitsCacheInRedis.js +2 -2
- package/cjs/storages/inRedis/index.js +1 -1
- package/cjs/storages/pluggable/SplitsCachePluggable.js +2 -2
- package/cjs/storages/pluggable/index.js +14 -3
- package/cjs/trackers/eventTracker.js +4 -4
- package/cjs/utils/lang/sets.js +3 -3
- package/cjs/utils/settingsValidation/index.js +1 -1
- package/cjs/utils/settingsValidation/mode.js +10 -3
- package/cjs/utils/settingsValidation/splitFilters.js +20 -19
- package/esm/logger/constants.js +2 -2
- package/esm/logger/messages/warn.js +2 -2
- package/esm/sdkClient/client.js +11 -8
- package/esm/sdkClient/clientInputValidation.js +7 -7
- package/esm/sdkManager/index.js +6 -6
- package/esm/storages/KeyBuilder.js +11 -0
- package/esm/storages/KeyBuilderCS.js +1 -4
- package/esm/storages/inLocalStorage/SplitsCacheInLocal.js +12 -16
- package/esm/storages/inLocalStorage/index.js +1 -1
- package/esm/storages/inRedis/SplitsCacheInRedis.js +3 -3
- package/esm/storages/inRedis/index.js +1 -1
- package/esm/storages/pluggable/SplitsCachePluggable.js +3 -3
- package/esm/storages/pluggable/index.js +15 -4
- package/esm/trackers/eventTracker.js +4 -4
- package/esm/utils/lang/sets.js +1 -1
- package/esm/utils/settingsValidation/index.js +2 -2
- package/esm/utils/settingsValidation/mode.js +7 -1
- package/esm/utils/settingsValidation/splitFilters.js +11 -10
- package/package.json +1 -1
- package/src/logger/constants.ts +2 -2
- package/src/logger/messages/warn.ts +2 -2
- package/src/sdkClient/client.ts +11 -8
- package/src/sdkClient/clientInputValidation.ts +7 -7
- package/src/sdkManager/index.ts +6 -6
- package/src/storages/KeyBuilder.ts +14 -1
- package/src/storages/KeyBuilderCS.ts +1 -5
- package/src/storages/KeyBuilderSS.ts +4 -4
- package/src/storages/inLocalStorage/SplitsCacheInLocal.ts +16 -14
- package/src/storages/inLocalStorage/index.ts +1 -1
- package/src/storages/inRedis/SplitsCacheInRedis.ts +3 -3
- package/src/storages/inRedis/index.ts +1 -1
- package/src/storages/pluggable/SplitsCachePluggable.ts +3 -3
- package/src/storages/pluggable/index.ts +15 -5
- package/src/storages/types.ts +3 -3
- package/src/trackers/eventTracker.ts +4 -4
- package/src/utils/lang/sets.ts +1 -1
- package/src/utils/murmur3/murmur3.ts +0 -1
- package/src/utils/settingsValidation/index.ts +2 -2
- package/src/utils/settingsValidation/mode.ts +8 -1
- package/src/utils/settingsValidation/splitFilters.ts +11 -10
- package/types/logger/constants.d.ts +2 -2
- package/types/storages/AbstractSplitsCache.d.ts +46 -0
- package/types/storages/KeyBuilder.d.ts +8 -1
- package/types/storages/KeyBuilderCS.d.ts +0 -1
- package/types/storages/KeyBuilderSS.d.ts +4 -4
- package/types/storages/inLocalStorage/SplitsCacheInLocal.d.ts +5 -5
- package/types/utils/lang/sets.d.ts +1 -1
- package/types/utils/settingsValidation/mode.d.ts +5 -1
- package/types/utils/settingsValidation/splitFilters.d.ts +1 -1
- package/cjs/trackers/impressionObserver/utils.js +0 -11
- package/cjs/utils/redis/RedisMock.js +0 -31
- package/esm/trackers/impressionObserver/utils.js +0 -7
- package/esm/utils/redis/RedisMock.js +0 -28
- package/src/trackers/impressionObserver/utils.ts +0 -9
- package/src/utils/redis/RedisMock.ts +0 -31
|
@@ -16,8 +16,8 @@ import { CONTROL, CONTROL_WITH_CONFIG, GET_TREATMENT, GET_TREATMENTS, GET_TREATM
|
|
|
16
16
|
import { IReadinessManager } from '../readiness/types';
|
|
17
17
|
import { MaybeThenable } from '../dtos/types';
|
|
18
18
|
import { ISettings, SplitIO } from '../types';
|
|
19
|
-
import {
|
|
20
|
-
import {
|
|
19
|
+
import { isConsumerMode } from '../utils/settingsValidation/mode';
|
|
20
|
+
import { validateFlagSets } from '../utils/settingsValidation/splitFilters';
|
|
21
21
|
|
|
22
22
|
/**
|
|
23
23
|
* Decorator that validates the input before actually executing the client methods.
|
|
@@ -25,8 +25,8 @@ import { flagSetsAreValid } from '../utils/settingsValidation/splitFilters';
|
|
|
25
25
|
*/
|
|
26
26
|
export function clientInputValidationDecorator<TClient extends SplitIO.IClient | SplitIO.IAsyncClient>(settings: ISettings, client: TClient, readinessManager: IReadinessManager): TClient {
|
|
27
27
|
|
|
28
|
-
const log = settings
|
|
29
|
-
const
|
|
28
|
+
const { log, mode } = settings;
|
|
29
|
+
const isAsync = isConsumerMode(mode);
|
|
30
30
|
|
|
31
31
|
/**
|
|
32
32
|
* Avoid repeating this validations code
|
|
@@ -42,7 +42,7 @@ export function clientInputValidationDecorator<TClient extends SplitIO.IClient |
|
|
|
42
42
|
const attributes = validateAttributes(log, maybeAttributes, methodName);
|
|
43
43
|
const isNotDestroyed = validateIfNotDestroyed(log, readinessManager, methodName);
|
|
44
44
|
if (maybeFlagSetNameOrNames) {
|
|
45
|
-
flagSetOrFlagSets =
|
|
45
|
+
flagSetOrFlagSets = validateFlagSets(log, methodName, maybeFlagSetNameOrNames, settings.sync.__splitFiltersValidation.groupedFilters.bySet);
|
|
46
46
|
}
|
|
47
47
|
|
|
48
48
|
validateIfOperational(log, readinessManager, methodName, splitOrSplits);
|
|
@@ -59,7 +59,7 @@ export function clientInputValidationDecorator<TClient extends SplitIO.IClient |
|
|
|
59
59
|
}
|
|
60
60
|
|
|
61
61
|
function wrapResult<T>(value: T): MaybeThenable<T> {
|
|
62
|
-
return
|
|
62
|
+
return isAsync ? Promise.resolve(value) : value;
|
|
63
63
|
}
|
|
64
64
|
|
|
65
65
|
function getTreatment(maybeKey: SplitIO.SplitKey, maybeFeatureFlagName: string, maybeAttributes?: SplitIO.Attributes) {
|
|
@@ -159,7 +159,7 @@ export function clientInputValidationDecorator<TClient extends SplitIO.IClient |
|
|
|
159
159
|
if (isNotDestroyed && key && tt && event && eventValue !== false && properties !== false) { // @ts-expect-error
|
|
160
160
|
return client.track(key, tt, event, eventValue, properties, size);
|
|
161
161
|
} else {
|
|
162
|
-
return
|
|
162
|
+
return isAsync ? Promise.resolve(false) : false;
|
|
163
163
|
}
|
|
164
164
|
}
|
|
165
165
|
|
package/src/sdkManager/index.ts
CHANGED
|
@@ -6,7 +6,7 @@ import { ISplitsCacheAsync, ISplitsCacheSync } from '../storages/types';
|
|
|
6
6
|
import { ISdkReadinessManager } from '../readiness/types';
|
|
7
7
|
import { ISplit } from '../dtos/types';
|
|
8
8
|
import { ISettings, SplitIO } from '../types';
|
|
9
|
-
import {
|
|
9
|
+
import { isConsumerMode } from '../utils/settingsValidation/mode';
|
|
10
10
|
import { SPLIT_FN_LABEL, SPLITS_FN_LABEL, NAMES_FN_LABEL } from '../utils/constants';
|
|
11
11
|
|
|
12
12
|
function collectTreatments(splitObject: ISplit) {
|
|
@@ -51,8 +51,8 @@ export function sdkManagerFactory<TSplitCache extends ISplitsCacheSync | ISplits
|
|
|
51
51
|
{ readinessManager, sdkStatus }: ISdkReadinessManager,
|
|
52
52
|
): TSplitCache extends ISplitsCacheAsync ? SplitIO.IAsyncManager : SplitIO.IManager {
|
|
53
53
|
|
|
54
|
-
const log = settings
|
|
55
|
-
const
|
|
54
|
+
const { log, mode } = settings;
|
|
55
|
+
const isAsync = isConsumerMode(mode);
|
|
56
56
|
|
|
57
57
|
return objectAssign(
|
|
58
58
|
// Proto-linkage of the readiness Event Emitter
|
|
@@ -64,7 +64,7 @@ export function sdkManagerFactory<TSplitCache extends ISplitsCacheSync | ISplits
|
|
|
64
64
|
split(featureFlagName: string) {
|
|
65
65
|
const splitName = validateSplit(log, featureFlagName, SPLIT_FN_LABEL);
|
|
66
66
|
if (!validateIfNotDestroyed(log, readinessManager, SPLIT_FN_LABEL) || !validateIfOperational(log, readinessManager, SPLIT_FN_LABEL) || !splitName) {
|
|
67
|
-
return
|
|
67
|
+
return isAsync ? Promise.resolve(null) : null;
|
|
68
68
|
}
|
|
69
69
|
|
|
70
70
|
const split = splits.getSplit(splitName);
|
|
@@ -85,7 +85,7 @@ export function sdkManagerFactory<TSplitCache extends ISplitsCacheSync | ISplits
|
|
|
85
85
|
*/
|
|
86
86
|
splits() {
|
|
87
87
|
if (!validateIfNotDestroyed(log, readinessManager, SPLITS_FN_LABEL) || !validateIfOperational(log, readinessManager, SPLITS_FN_LABEL)) {
|
|
88
|
-
return
|
|
88
|
+
return isAsync ? Promise.resolve([]) : [];
|
|
89
89
|
}
|
|
90
90
|
const currentSplits = splits.getAll();
|
|
91
91
|
|
|
@@ -98,7 +98,7 @@ export function sdkManagerFactory<TSplitCache extends ISplitsCacheSync | ISplits
|
|
|
98
98
|
*/
|
|
99
99
|
names() {
|
|
100
100
|
if (!validateIfNotDestroyed(log, readinessManager, NAMES_FN_LABEL) || !validateIfOperational(log, readinessManager, NAMES_FN_LABEL)) {
|
|
101
|
-
return
|
|
101
|
+
return isAsync ? Promise.resolve([]) : [];
|
|
102
102
|
}
|
|
103
103
|
const splitNames = splits.getSplitNames();
|
|
104
104
|
|
|
@@ -1,4 +1,6 @@
|
|
|
1
|
+
import { ISettings } from '../types';
|
|
1
2
|
import { startsWith } from '../utils/lang';
|
|
3
|
+
import { hash } from '../utils/murmur3/murmur3';
|
|
2
4
|
|
|
3
5
|
const everythingAtTheEnd = /[^.]+$/;
|
|
4
6
|
|
|
@@ -10,7 +12,7 @@ export function validatePrefix(prefix: unknown) {
|
|
|
10
12
|
|
|
11
13
|
export class KeyBuilder {
|
|
12
14
|
|
|
13
|
-
|
|
15
|
+
readonly prefix: string;
|
|
14
16
|
|
|
15
17
|
constructor(prefix: string = DEFAULT_PREFIX) {
|
|
16
18
|
this.prefix = prefix;
|
|
@@ -73,4 +75,15 @@ export class KeyBuilder {
|
|
|
73
75
|
}
|
|
74
76
|
}
|
|
75
77
|
|
|
78
|
+
buildHashKey() {
|
|
79
|
+
return `${this.prefix}.hash`;
|
|
80
|
+
}
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
/**
|
|
84
|
+
* Generates a murmur32 hash based on the authorization key and the feature flags filter query.
|
|
85
|
+
* The hash is in hexadecimal format (8 characters max, 32 bits).
|
|
86
|
+
*/
|
|
87
|
+
export function getStorageHash(settings: ISettings) {
|
|
88
|
+
return hash(`${settings.core.authorizationKey}::${settings.sync.__splitFiltersValidation.queryString}`).toString(16);
|
|
76
89
|
}
|
|
@@ -9,7 +9,7 @@ export class KeyBuilderCS extends KeyBuilder {
|
|
|
9
9
|
constructor(prefix: string, matchingKey: string) {
|
|
10
10
|
super(prefix);
|
|
11
11
|
this.matchingKey = matchingKey;
|
|
12
|
-
this.regexSplitsCacheKey = new RegExp(`^${prefix}
|
|
12
|
+
this.regexSplitsCacheKey = new RegExp(`^${prefix}\\.`);
|
|
13
13
|
}
|
|
14
14
|
|
|
15
15
|
/**
|
|
@@ -45,8 +45,4 @@ export class KeyBuilderCS extends KeyBuilder {
|
|
|
45
45
|
isSplitsCacheKey(key: string) {
|
|
46
46
|
return this.regexSplitsCacheKey.test(key);
|
|
47
47
|
}
|
|
48
|
-
|
|
49
|
-
buildSplitsFilterQueryKey() {
|
|
50
|
-
return `${this.prefix}.splits.filterQuery`;
|
|
51
|
-
}
|
|
52
48
|
}
|
|
@@ -16,10 +16,10 @@ export const METHOD_NAMES: Record<Method, string> = {
|
|
|
16
16
|
|
|
17
17
|
export class KeyBuilderSS extends KeyBuilder {
|
|
18
18
|
|
|
19
|
-
latencyPrefix: string;
|
|
20
|
-
exceptionPrefix: string;
|
|
21
|
-
initPrefix: string;
|
|
22
|
-
private versionablePrefix: string;
|
|
19
|
+
readonly latencyPrefix: string;
|
|
20
|
+
readonly exceptionPrefix: string;
|
|
21
|
+
readonly initPrefix: string;
|
|
22
|
+
private readonly versionablePrefix: string;
|
|
23
23
|
|
|
24
24
|
constructor(prefix: string, metadata: IMetadata) {
|
|
25
25
|
super(prefix);
|
|
@@ -1,10 +1,12 @@
|
|
|
1
|
-
import { ISplit
|
|
1
|
+
import { ISplit } from '../../dtos/types';
|
|
2
2
|
import { AbstractSplitsCacheSync, usesSegments } from '../AbstractSplitsCacheSync';
|
|
3
3
|
import { isFiniteNumber, toNumber, isNaNNumber } from '../../utils/lang';
|
|
4
4
|
import { KeyBuilderCS } from '../KeyBuilderCS';
|
|
5
5
|
import { ILogger } from '../../logger/types';
|
|
6
6
|
import { LOG_PREFIX } from './constants';
|
|
7
7
|
import { ISet, _Set, setToArray } from '../../utils/lang/sets';
|
|
8
|
+
import { ISettings } from '../../types';
|
|
9
|
+
import { getStorageHash } from '../KeyBuilder';
|
|
8
10
|
|
|
9
11
|
/**
|
|
10
12
|
* ISplitsCacheSync implementation that stores split definitions in browser LocalStorage.
|
|
@@ -12,7 +14,8 @@ import { ISet, _Set, setToArray } from '../../utils/lang/sets';
|
|
|
12
14
|
export class SplitsCacheInLocal extends AbstractSplitsCacheSync {
|
|
13
15
|
|
|
14
16
|
private readonly keys: KeyBuilderCS;
|
|
15
|
-
private readonly
|
|
17
|
+
private readonly log: ILogger;
|
|
18
|
+
private readonly storageHash: string;
|
|
16
19
|
private readonly flagSetsFilter: string[];
|
|
17
20
|
private hasSync?: boolean;
|
|
18
21
|
private updateNewFilter?: boolean;
|
|
@@ -22,11 +25,12 @@ export class SplitsCacheInLocal extends AbstractSplitsCacheSync {
|
|
|
22
25
|
* @param {number | undefined} expirationTimestamp
|
|
23
26
|
* @param {ISplitFiltersValidation} splitFiltersValidation
|
|
24
27
|
*/
|
|
25
|
-
constructor(
|
|
28
|
+
constructor(settings: ISettings, keys: KeyBuilderCS, expirationTimestamp?: number) {
|
|
26
29
|
super();
|
|
27
30
|
this.keys = keys;
|
|
28
|
-
this.
|
|
29
|
-
this.
|
|
31
|
+
this.log = settings.log;
|
|
32
|
+
this.storageHash = getStorageHash(settings);
|
|
33
|
+
this.flagSetsFilter = settings.sync.__splitFiltersValidation.groupedFilters.bySet;
|
|
30
34
|
|
|
31
35
|
this._checkExpiration(expirationTimestamp);
|
|
32
36
|
|
|
@@ -142,12 +146,10 @@ export class SplitsCacheInLocal extends AbstractSplitsCacheSync {
|
|
|
142
146
|
|
|
143
147
|
// when using a new split query, we must update it at the store
|
|
144
148
|
if (this.updateNewFilter) {
|
|
145
|
-
this.log.info(LOG_PREFIX + '
|
|
146
|
-
const
|
|
147
|
-
const queryString = this.splitFiltersValidation.queryString;
|
|
149
|
+
this.log.info(LOG_PREFIX + 'SDK key or feature flag filter criteria was modified. Updating cache.');
|
|
150
|
+
const storageHashKey = this.keys.buildHashKey();
|
|
148
151
|
try {
|
|
149
|
-
|
|
150
|
-
else localStorage.removeItem(queryKey);
|
|
152
|
+
localStorage.setItem(storageHashKey, this.storageHash);
|
|
151
153
|
} catch (e) {
|
|
152
154
|
this.log.error(LOG_PREFIX + e);
|
|
153
155
|
}
|
|
@@ -237,12 +239,12 @@ export class SplitsCacheInLocal extends AbstractSplitsCacheSync {
|
|
|
237
239
|
}
|
|
238
240
|
}
|
|
239
241
|
|
|
242
|
+
// @TODO eventually remove `_checkFilterQuery`. Cache should be cleared at the storage level, reusing same logic than PluggableStorage
|
|
240
243
|
private _checkFilterQuery() {
|
|
241
|
-
const
|
|
242
|
-
const
|
|
243
|
-
const currentQueryString = localStorage.getItem(queryKey);
|
|
244
|
+
const storageHashKey = this.keys.buildHashKey();
|
|
245
|
+
const storageHash = localStorage.getItem(storageHashKey);
|
|
244
246
|
|
|
245
|
-
if (
|
|
247
|
+
if (storageHash !== this.storageHash) {
|
|
246
248
|
try {
|
|
247
249
|
// mark cache to update the new query filter on first successful splits fetch
|
|
248
250
|
this.updateNewFilter = true;
|
|
@@ -41,7 +41,7 @@ export function InLocalStorage(options: InLocalStorageOptions = {}): IStorageSyn
|
|
|
41
41
|
const keys = new KeyBuilderCS(prefix, matchingKey as string);
|
|
42
42
|
const expirationTimestamp = Date.now() - DEFAULT_CACHE_EXPIRATION_IN_MILLIS;
|
|
43
43
|
|
|
44
|
-
const splits = new SplitsCacheInLocal(
|
|
44
|
+
const splits = new SplitsCacheInLocal(settings, keys, expirationTimestamp);
|
|
45
45
|
const segments = new MySegmentsCacheInLocal(log, keys);
|
|
46
46
|
|
|
47
47
|
return {
|
|
@@ -4,7 +4,7 @@ import { ILogger } from '../../logger/types';
|
|
|
4
4
|
import { LOG_PREFIX } from './constants';
|
|
5
5
|
import { ISplit, ISplitFiltersValidation } from '../../dtos/types';
|
|
6
6
|
import { AbstractSplitsCacheAsync } from '../AbstractSplitsCacheAsync';
|
|
7
|
-
import { ISet, _Set,
|
|
7
|
+
import { ISet, _Set, returnDifference } from '../../utils/lang/sets';
|
|
8
8
|
import type { RedisAdapter } from './RedisAdapter';
|
|
9
9
|
|
|
10
10
|
/**
|
|
@@ -60,9 +60,9 @@ export class SplitsCacheInRedis extends AbstractSplitsCacheAsync {
|
|
|
60
60
|
}
|
|
61
61
|
|
|
62
62
|
private _updateFlagSets(featureFlagName: string, flagSetsOfRemovedFlag?: string[], flagSetsOfAddedFlag?: string[]) {
|
|
63
|
-
const removeFromFlagSets =
|
|
63
|
+
const removeFromFlagSets = returnDifference(flagSetsOfRemovedFlag, flagSetsOfAddedFlag);
|
|
64
64
|
|
|
65
|
-
let addToFlagSets =
|
|
65
|
+
let addToFlagSets = returnDifference(flagSetsOfAddedFlag, flagSetsOfRemovedFlag);
|
|
66
66
|
if (this.flagSetsFilter.length > 0) {
|
|
67
67
|
addToFlagSets = addToFlagSets.filter(flagSet => {
|
|
68
68
|
return this.flagSetsFilter.some(filterFlagSet => filterFlagSet === flagSet);
|
|
@@ -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,
|
|
@@ -5,7 +5,7 @@ import { ILogger } from '../../logger/types';
|
|
|
5
5
|
import { ISplit, ISplitFiltersValidation } from '../../dtos/types';
|
|
6
6
|
import { LOG_PREFIX } from './constants';
|
|
7
7
|
import { AbstractSplitsCacheAsync } from '../AbstractSplitsCacheAsync';
|
|
8
|
-
import { ISet, _Set,
|
|
8
|
+
import { ISet, _Set, returnDifference } from '../../utils/lang/sets';
|
|
9
9
|
|
|
10
10
|
/**
|
|
11
11
|
* ISplitsCacheAsync implementation for pluggable storages.
|
|
@@ -44,9 +44,9 @@ export class SplitsCachePluggable extends AbstractSplitsCacheAsync {
|
|
|
44
44
|
}
|
|
45
45
|
|
|
46
46
|
private _updateFlagSets(featureFlagName: string, flagSetsOfRemovedFlag?: string[], flagSetsOfAddedFlag?: string[]) {
|
|
47
|
-
const removeFromFlagSets =
|
|
47
|
+
const removeFromFlagSets = returnDifference(flagSetsOfRemovedFlag, flagSetsOfAddedFlag);
|
|
48
48
|
|
|
49
|
-
let addToFlagSets =
|
|
49
|
+
let addToFlagSets = returnDifference(flagSetsOfAddedFlag, flagSetsOfRemovedFlag);
|
|
50
50
|
if (this.flagSetsFilter.length > 0) {
|
|
51
51
|
addToFlagSets = addToFlagSets.filter(flagSet => {
|
|
52
52
|
return this.flagSetsFilter.some(filterFlagSet => filterFlagSet === flagSet);
|
|
@@ -7,7 +7,7 @@ import { ImpressionsCachePluggable } from './ImpressionsCachePluggable';
|
|
|
7
7
|
import { EventsCachePluggable } from './EventsCachePluggable';
|
|
8
8
|
import { wrapperAdapter, METHODS_TO_PROMISE_WRAP } from './wrapperAdapter';
|
|
9
9
|
import { isObject } from '../../utils/lang';
|
|
10
|
-
import { validatePrefix } from '../KeyBuilder';
|
|
10
|
+
import { getStorageHash, validatePrefix } from '../KeyBuilder';
|
|
11
11
|
import { CONSUMER_PARTIAL_MODE, DEBUG, NONE, STORAGE_PLUGGABLE } from '../../utils/constants';
|
|
12
12
|
import { ImpressionsCacheInMemory } from '../inMemory/ImpressionsCacheInMemory';
|
|
13
13
|
import { EventsCacheInMemory } from '../inMemory/EventsCacheInMemory';
|
|
@@ -90,13 +90,23 @@ export function PluggableStorage(options: PluggableStorageOptions): IStorageAsyn
|
|
|
90
90
|
|
|
91
91
|
// Connects to wrapper and emits SDK_READY event on main client
|
|
92
92
|
const connectPromise = wrapper.connect().then(() => {
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
93
|
+
if (isSyncronizer) {
|
|
94
|
+
// In standalone or producer mode, clear storage if SDK key or feature flag filter has changed
|
|
95
|
+
return wrapper.get(keys.buildHashKey()).then((hash) => {
|
|
96
|
+
const currentHash = getStorageHash(settings);
|
|
97
|
+
if (hash !== currentHash) {
|
|
98
|
+
return wrapper.getKeysByPrefix(`${keys.prefix}.`).then(storageKeys => {
|
|
99
|
+
return Promise.all(storageKeys.map(storageKey => wrapper.del(storageKey)));
|
|
100
|
+
}).then(() => wrapper.set(keys.buildHashKey(), currentHash));
|
|
101
|
+
}
|
|
102
|
+
}).then(onReadyCb);
|
|
103
|
+
} else {
|
|
104
|
+
// Start periodic flush of async storages if not running synchronizer (producer mode)
|
|
97
105
|
if (impressionCountsCache && (impressionCountsCache as ImpressionCountsCachePluggable).start) (impressionCountsCache as ImpressionCountsCachePluggable).start();
|
|
98
106
|
if (uniqueKeysCache && (uniqueKeysCache as UniqueKeysCachePluggable).start) (uniqueKeysCache as UniqueKeysCachePluggable).start();
|
|
99
107
|
if (telemetry && (telemetry as ITelemetryCacheAsync).recordConfig) (telemetry as ITelemetryCacheAsync).recordConfig();
|
|
108
|
+
|
|
109
|
+
onReadyCb();
|
|
100
110
|
}
|
|
101
111
|
}).catch((e) => {
|
|
102
112
|
e = e || new Error('Error connecting wrapper');
|
package/src/storages/types.ts
CHANGED
|
@@ -457,7 +457,7 @@ export interface IStorageBase<
|
|
|
457
457
|
TEventsCache extends IEventsCacheBase,
|
|
458
458
|
TTelemetryCache extends ITelemetryCacheSync | ITelemetryCacheAsync,
|
|
459
459
|
TUniqueKeysCache extends IUniqueKeysCacheBase
|
|
460
|
-
|
|
460
|
+
> {
|
|
461
461
|
splits: TSplitsCache,
|
|
462
462
|
segments: TSegmentsCache,
|
|
463
463
|
impressions: TImpressionsCache,
|
|
@@ -477,7 +477,7 @@ export interface IStorageSync extends IStorageBase<
|
|
|
477
477
|
IEventsCacheSync,
|
|
478
478
|
ITelemetryCacheSync,
|
|
479
479
|
IUniqueKeysCacheSync
|
|
480
|
-
|
|
480
|
+
> { }
|
|
481
481
|
|
|
482
482
|
export interface IStorageAsync extends IStorageBase<
|
|
483
483
|
ISplitsCacheAsync,
|
|
@@ -487,7 +487,7 @@ export interface IStorageAsync extends IStorageBase<
|
|
|
487
487
|
IEventsCacheAsync | IEventsCacheSync,
|
|
488
488
|
ITelemetryCacheAsync | ITelemetryCacheSync,
|
|
489
489
|
IUniqueKeysCacheBase
|
|
490
|
-
|
|
490
|
+
> { }
|
|
491
491
|
|
|
492
492
|
/** StorageFactory */
|
|
493
493
|
|
|
@@ -5,7 +5,7 @@ import { IEventsHandler, IEventTracker } from './types';
|
|
|
5
5
|
import { ISettings, SplitIO } from '../types';
|
|
6
6
|
import { EVENTS_TRACKER_SUCCESS, ERROR_EVENTS_TRACKER } from '../logger/constants';
|
|
7
7
|
import { CONSENT_DECLINED, DROPPED, QUEUED } from '../utils/constants';
|
|
8
|
-
import {
|
|
8
|
+
import { isConsumerMode } from '../utils/settingsValidation/mode';
|
|
9
9
|
|
|
10
10
|
/**
|
|
11
11
|
* Event tracker stores events in cache and pass them to the integrations manager if provided.
|
|
@@ -20,8 +20,8 @@ export function eventTrackerFactory(
|
|
|
20
20
|
telemetryCache?: ITelemetryCacheSync | ITelemetryCacheAsync
|
|
21
21
|
): IEventTracker {
|
|
22
22
|
|
|
23
|
-
const log = settings
|
|
24
|
-
const
|
|
23
|
+
const { log, mode } = settings;
|
|
24
|
+
const isAsync = isConsumerMode(mode);
|
|
25
25
|
|
|
26
26
|
function queueEventsCallback(eventData: SplitIO.EventData, tracked: boolean) {
|
|
27
27
|
const { eventTypeId, trafficTypeName, key, value, timestamp, properties } = eventData;
|
|
@@ -50,7 +50,7 @@ export function eventTrackerFactory(
|
|
|
50
50
|
return {
|
|
51
51
|
track(eventData: SplitIO.EventData, size?: number) {
|
|
52
52
|
if (settings.userConsent === CONSENT_DECLINED) {
|
|
53
|
-
return
|
|
53
|
+
return isAsync ? Promise.resolve(false) : false;
|
|
54
54
|
}
|
|
55
55
|
|
|
56
56
|
const tracked = eventsCache.track(eventData, size);
|
package/src/utils/lang/sets.ts
CHANGED
|
@@ -120,7 +120,7 @@ export function returnSetsUnion<T>(set: ISet<T>, set2: ISet<T>): ISet<T> {
|
|
|
120
120
|
return result;
|
|
121
121
|
}
|
|
122
122
|
|
|
123
|
-
export function
|
|
123
|
+
export function returnDifference<T>(list: T[] = [], list2: T[] = []): T[] {
|
|
124
124
|
const result = new _Set(list);
|
|
125
125
|
list2.forEach(item => {
|
|
126
126
|
result.delete(item);
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import { merge, get } from '../lang';
|
|
2
|
-
import {
|
|
2
|
+
import { validateMode } from './mode';
|
|
3
3
|
import { validateSplitFilters } from './splitFilters';
|
|
4
4
|
import { STANDALONE_MODE, OPTIMIZED, LOCALHOST_MODE, DEBUG } from '../constants';
|
|
5
5
|
import { validImpressionsMode } from './impressionsMode';
|
|
@@ -146,7 +146,7 @@ export function settingsValidation(config: unknown, validationParams: ISettingsV
|
|
|
146
146
|
|
|
147
147
|
// ensure a valid SDK mode
|
|
148
148
|
// @ts-ignore, modify readonly prop
|
|
149
|
-
withDefaults.mode =
|
|
149
|
+
withDefaults.mode = validateMode(withDefaults.core.authorizationKey, withDefaults.mode);
|
|
150
150
|
|
|
151
151
|
// ensure a valid Storage based on mode defined.
|
|
152
152
|
// @ts-ignore, modify readonly prop
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import { LOCALHOST_MODE, STANDALONE_MODE, PRODUCER_MODE, CONSUMER_MODE, CONSUMER_PARTIAL_MODE } from '../constants';
|
|
2
2
|
|
|
3
|
-
export function
|
|
3
|
+
export function validateMode(key: string, mode: string) {
|
|
4
4
|
// Leaving the comparison as is, in case we change the mode name but not the setting.
|
|
5
5
|
if (key === 'localhost') return LOCALHOST_MODE;
|
|
6
6
|
|
|
@@ -8,3 +8,10 @@ export function mode(key: string, mode: string) {
|
|
|
8
8
|
|
|
9
9
|
return mode;
|
|
10
10
|
}
|
|
11
|
+
|
|
12
|
+
/**
|
|
13
|
+
* Storage is async if mode is consumer or partial consumer
|
|
14
|
+
*/
|
|
15
|
+
export function isConsumerMode(mode: string) {
|
|
16
|
+
return CONSUMER_MODE === mode || CONSUMER_PARTIAL_MODE === mode;
|
|
17
|
+
}
|
|
@@ -1,11 +1,11 @@
|
|
|
1
|
-
import { CONSUMER_MODE, CONSUMER_PARTIAL_MODE } from '../constants';
|
|
2
1
|
import { validateSplits } from '../inputValidation/splits';
|
|
3
2
|
import { ISplitFiltersValidation } from '../../dtos/types';
|
|
4
3
|
import { SplitIO } from '../../types';
|
|
5
4
|
import { ILogger } from '../../logger/types';
|
|
6
|
-
import { WARN_SPLITS_FILTER_IGNORED, WARN_SPLITS_FILTER_EMPTY, WARN_SPLITS_FILTER_INVALID, SETTINGS_SPLITS_FILTER, LOG_PREFIX_SETTINGS, ERROR_SETS_FILTER_EXCLUSIVE,
|
|
5
|
+
import { WARN_SPLITS_FILTER_IGNORED, WARN_SPLITS_FILTER_EMPTY, WARN_SPLITS_FILTER_INVALID, SETTINGS_SPLITS_FILTER, LOG_PREFIX_SETTINGS, ERROR_SETS_FILTER_EXCLUSIVE, WARN_LOWERCASE_FLAGSET, WARN_INVALID_FLAGSET, WARN_FLAGSET_NOT_CONFIGURED } from '../../logger/constants';
|
|
7
6
|
import { objectAssign } from '../lang/objectAssign';
|
|
8
7
|
import { find, uniq } from '../lang';
|
|
8
|
+
import { isConsumerMode } from './mode';
|
|
9
9
|
|
|
10
10
|
// Split filters metadata.
|
|
11
11
|
// Ordered according to their precedency when forming the filter query string: `&names=<values>&prefixes=<values>`
|
|
@@ -54,7 +54,7 @@ function validateSplitFilter(log: ILogger, type: SplitIO.SplitFilterType, values
|
|
|
54
54
|
if (result) {
|
|
55
55
|
|
|
56
56
|
if (type === 'bySet') {
|
|
57
|
-
result = sanitizeFlagSets(log, result);
|
|
57
|
+
result = sanitizeFlagSets(log, result, LOG_PREFIX_SETTINGS);
|
|
58
58
|
}
|
|
59
59
|
|
|
60
60
|
// check max length
|
|
@@ -88,7 +88,7 @@ function queryStringBuilder(groupedFilters: Record<SplitIO.SplitFilterType, stri
|
|
|
88
88
|
}
|
|
89
89
|
|
|
90
90
|
/**
|
|
91
|
-
* Sanitizes set names list taking
|
|
91
|
+
* Sanitizes set names list taking into account:
|
|
92
92
|
* - It should be lowercase
|
|
93
93
|
* - Must adhere the following regular expression /^[a-z0-9][_a-z0-9]{0,49}$/ that means
|
|
94
94
|
* - must start with a letter or number
|
|
@@ -98,20 +98,21 @@ function queryStringBuilder(groupedFilters: Record<SplitIO.SplitFilterType, stri
|
|
|
98
98
|
*
|
|
99
99
|
* @param {ILogger} log
|
|
100
100
|
* @param {string[]} flagSets
|
|
101
|
+
* @param {string} method
|
|
101
102
|
* @returns sanitized list of set names
|
|
102
103
|
*/
|
|
103
|
-
function sanitizeFlagSets(log: ILogger, flagSets: string[]) {
|
|
104
|
+
function sanitizeFlagSets(log: ILogger, flagSets: string[], method: string) {
|
|
104
105
|
let sanitizedSets = flagSets
|
|
105
106
|
.map(flagSet => {
|
|
106
107
|
if (CAPITAL_LETTERS_REGEX.test(flagSet)) {
|
|
107
|
-
log.warn(
|
|
108
|
+
log.warn(WARN_LOWERCASE_FLAGSET, [method, flagSet]);
|
|
108
109
|
flagSet = flagSet.toLowerCase();
|
|
109
110
|
}
|
|
110
111
|
return flagSet;
|
|
111
112
|
})
|
|
112
113
|
.filter(flagSet => {
|
|
113
114
|
if (!VALID_FLAGSET_REGEX.test(flagSet)) {
|
|
114
|
-
log.warn(
|
|
115
|
+
log.warn(WARN_INVALID_FLAGSET, [method, flagSet, VALID_FLAGSET_REGEX, flagSet]);
|
|
115
116
|
return false;
|
|
116
117
|
}
|
|
117
118
|
if (typeof flagSet !== 'string') return false;
|
|
@@ -148,7 +149,7 @@ export function validateSplitFilters(log: ILogger, maybeSplitFilters: any, mode:
|
|
|
148
149
|
// do nothing if `splitFilters` param is not a non-empty array or mode is not STANDALONE
|
|
149
150
|
if (!maybeSplitFilters) return res;
|
|
150
151
|
// Warn depending on the mode
|
|
151
|
-
if (mode
|
|
152
|
+
if (isConsumerMode(mode)) {
|
|
152
153
|
log.warn(WARN_SPLITS_FILTER_IGNORED);
|
|
153
154
|
return res;
|
|
154
155
|
}
|
|
@@ -188,9 +189,9 @@ export function validateSplitFilters(log: ILogger, maybeSplitFilters: any, mode:
|
|
|
188
189
|
return res;
|
|
189
190
|
}
|
|
190
191
|
|
|
191
|
-
export function
|
|
192
|
+
export function validateFlagSets(log: ILogger, method: string, flagSets: string[], flagSetsInConfig: string[]): string[] {
|
|
192
193
|
const sets = validateSplits(log, flagSets, method, 'flag sets', 'flag set');
|
|
193
|
-
let toReturn = sets ? sanitizeFlagSets(log, sets) : [];
|
|
194
|
+
let toReturn = sets ? sanitizeFlagSets(log, sets, method) : [];
|
|
194
195
|
if (flagSetsInConfig.length > 0) {
|
|
195
196
|
toReturn = toReturn.filter(flagSet => {
|
|
196
197
|
if (flagSetsInConfig.indexOf(flagSet) > -1) {
|
|
@@ -95,8 +95,8 @@ export declare const WARN_SPLITS_FILTER_EMPTY = 221;
|
|
|
95
95
|
export declare const WARN_SDK_KEY = 222;
|
|
96
96
|
export declare const STREAMING_PARSING_MY_SEGMENTS_UPDATE_V2 = 223;
|
|
97
97
|
export declare const STREAMING_PARSING_SPLIT_UPDATE = 224;
|
|
98
|
-
export declare const
|
|
99
|
-
export declare const
|
|
98
|
+
export declare const WARN_INVALID_FLAGSET = 225;
|
|
99
|
+
export declare const WARN_LOWERCASE_FLAGSET = 226;
|
|
100
100
|
export declare const WARN_FLAGSET_NOT_CONFIGURED = 227;
|
|
101
101
|
export declare const WARN_FLAGSET_WITHOUT_FLAGS = 228;
|
|
102
102
|
export declare const ERROR_ENGINE_COMBINER_IFELSEIF = 300;
|
|
@@ -0,0 +1,46 @@
|
|
|
1
|
+
import { ISplit, MaybeThenable } from '../dtos/types';
|
|
2
|
+
/**
|
|
3
|
+
* This class provides a skeletal implementation of the ISplitsCacheAsync interface
|
|
4
|
+
* to minimize the effort required to implement this interface.
|
|
5
|
+
*/
|
|
6
|
+
export declare abstract class AbstractSplitsCache {
|
|
7
|
+
/**
|
|
8
|
+
* Check if the splits information is already stored in cache. This data can be preloaded.
|
|
9
|
+
* It is used as condition to emit SDK_SPLITS_CACHE_LOADED, and then SDK_READY_FROM_CACHE.
|
|
10
|
+
*/
|
|
11
|
+
checkCache(): boolean;
|
|
12
|
+
protected abstract addSplit(name: string, split: ISplit): MaybeThenable<boolean>;
|
|
13
|
+
/**
|
|
14
|
+
* Add a list of splits.
|
|
15
|
+
* The returned promise is resolved when the operation success or rejected if it fails (e.g., wrapper operation fails).
|
|
16
|
+
*/
|
|
17
|
+
protected addSplits(entries: [string, ISplit][]): Promise<boolean[]>;
|
|
18
|
+
protected abstract removeSplit(name: string): MaybeThenable<boolean>;
|
|
19
|
+
/**
|
|
20
|
+
* Remove a list of splits.
|
|
21
|
+
* The returned promise is resolved when the operation success, with a boolean array indicating if the splits existed or not.
|
|
22
|
+
* or rejected if it fails (e.g., wrapper operation fails).
|
|
23
|
+
*/
|
|
24
|
+
protected removeSplits(names: string[]): Promise<boolean[]>;
|
|
25
|
+
protected abstract setChangeNumber(changeNumber: number): MaybeThenable<boolean | void>;
|
|
26
|
+
/**
|
|
27
|
+
* Updates the cache with the provided changeNumber, feature flags to add and feature flags to remove.
|
|
28
|
+
*
|
|
29
|
+
* @returns {Promise<boolean>} a promise that resolved once the operation is performed successfully. The fulfillment value is `true` if at least one feature flag was added, modified or removed; or `false` if there was no change.
|
|
30
|
+
* The promise will reject if some storage operation rejects.
|
|
31
|
+
*/
|
|
32
|
+
update(changeNumber: number, toAdd: [string, ISplit][], toRemove?: string[]): Promise<boolean>;
|
|
33
|
+
abstract getSplit(name: string): MaybeThenable<ISplit | null>;
|
|
34
|
+
/**
|
|
35
|
+
* Kill `name` split and set `defaultTreatment` and `changeNumber`.
|
|
36
|
+
* Used for SPLIT_KILL push notifications.
|
|
37
|
+
*
|
|
38
|
+
* @param {string} name
|
|
39
|
+
* @param {string} defaultTreatment
|
|
40
|
+
* @param {number} changeNumber
|
|
41
|
+
* @returns {Promise} a promise that is resolved once the split kill operation is performed. The fulfillment value is a boolean: `true` if the operation successed updating the split or `false` if no split is updated,
|
|
42
|
+
* for instance, if the `changeNumber` is old, or if the split is not found (e.g., `/splitchanges` hasn't been fetched yet), or if the storage fails to apply the update.
|
|
43
|
+
* The promise will never be rejected.
|
|
44
|
+
*/
|
|
45
|
+
killLocally(name: string, defaultTreatment: string, changeNumber: number): Promise<boolean>;
|
|
46
|
+
}
|
|
@@ -1,6 +1,7 @@
|
|
|
1
|
+
import { ISettings } from '../types';
|
|
1
2
|
export declare function validatePrefix(prefix: unknown): string;
|
|
2
3
|
export declare class KeyBuilder {
|
|
3
|
-
|
|
4
|
+
readonly prefix: string;
|
|
4
5
|
constructor(prefix?: string);
|
|
5
6
|
buildTrafficTypeKey(trafficType: string): string;
|
|
6
7
|
buildFlagSetKey(flagSet: string): string;
|
|
@@ -12,4 +13,10 @@ export declare class KeyBuilder {
|
|
|
12
13
|
buildSegmentNameKey(segmentName: string): string;
|
|
13
14
|
buildSegmentTillKey(segmentName: string): string;
|
|
14
15
|
extractKey(builtKey: string): string;
|
|
16
|
+
buildHashKey(): string;
|
|
15
17
|
}
|
|
18
|
+
/**
|
|
19
|
+
* Generates a murmur32 hash based on the authorization key and the feature flags filter query.
|
|
20
|
+
* The hash is in hexadecimal format (8 characters max, 32 bits).
|
|
21
|
+
*/
|
|
22
|
+
export declare function getStorageHash(settings: ISettings): string;
|
|
@@ -3,10 +3,10 @@ import { IMetadata } from '../dtos/types';
|
|
|
3
3
|
import { Method } from '../sync/submitters/types';
|
|
4
4
|
export declare const METHOD_NAMES: Record<Method, string>;
|
|
5
5
|
export declare class KeyBuilderSS extends KeyBuilder {
|
|
6
|
-
latencyPrefix: string;
|
|
7
|
-
exceptionPrefix: string;
|
|
8
|
-
initPrefix: string;
|
|
9
|
-
private versionablePrefix;
|
|
6
|
+
readonly latencyPrefix: string;
|
|
7
|
+
readonly exceptionPrefix: string;
|
|
8
|
+
readonly initPrefix: string;
|
|
9
|
+
private readonly versionablePrefix;
|
|
10
10
|
constructor(prefix: string, metadata: IMetadata);
|
|
11
11
|
buildRegisteredSegmentsKey(): string;
|
|
12
12
|
buildImpressionsKey(): string;
|