@splitsoftware/splitio-commons 2.5.0-rc.0 → 2.5.0-rc.1
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 +3 -3
- package/cjs/evaluator/convertions/index.js +9 -1
- package/cjs/evaluator/matchersTransform/index.js +2 -3
- package/cjs/sdkClient/sdkClientMethodCS.js +5 -1
- package/cjs/sdkFactory/index.js +8 -2
- package/cjs/storages/getRolloutPlan.js +69 -0
- package/cjs/storages/inLocalStorage/validateCache.js +2 -2
- package/cjs/storages/inMemory/InMemoryStorageCS.js +14 -32
- package/cjs/storages/setRolloutPlan.js +66 -0
- package/cjs/utils/inputValidation/index.js +1 -3
- package/cjs/utils/settingsValidation/index.js +4 -0
- package/esm/evaluator/convertions/index.js +7 -0
- package/esm/evaluator/matchersTransform/index.js +3 -4
- package/esm/sdkClient/sdkClientMethodCS.js +5 -1
- package/esm/sdkFactory/index.js +8 -2
- package/esm/storages/getRolloutPlan.js +65 -0
- package/esm/storages/inLocalStorage/validateCache.js +2 -2
- package/esm/storages/inMemory/InMemoryStorageCS.js +14 -32
- package/esm/storages/setRolloutPlan.js +61 -0
- package/esm/utils/inputValidation/index.js +0 -1
- package/esm/utils/settingsValidation/index.js +4 -0
- package/package.json +1 -1
- package/src/evaluator/convertions/index.ts +10 -0
- package/src/evaluator/matchersTransform/index.ts +3 -4
- package/src/sdkClient/sdkClientMethodCS.ts +7 -1
- package/src/sdkFactory/index.ts +9 -2
- package/src/storages/getRolloutPlan.ts +72 -0
- package/src/storages/inLocalStorage/validateCache.ts +2 -2
- package/src/storages/inMemory/InMemoryStorageCS.ts +14 -37
- package/src/storages/setRolloutPlan.ts +71 -0
- package/src/storages/types.ts +20 -2
- package/src/types.ts +2 -0
- package/src/utils/inputValidation/index.ts +0 -1
- package/src/utils/settingsValidation/index.ts +4 -0
- package/types/splitio.d.ts +22 -34
- package/cjs/storages/dataLoader.js +0 -109
- package/cjs/utils/inputValidation/preloadedData.js +0 -59
- package/esm/storages/dataLoader.js +0 -104
- package/esm/utils/inputValidation/preloadedData.js +0 -55
- package/src/storages/dataLoader.ts +0 -113
- package/src/utils/inputValidation/preloadedData.ts +0 -57
|
@@ -0,0 +1,61 @@
|
|
|
1
|
+
import { isObject } from '../utils/lang';
|
|
2
|
+
import { isConsumerMode } from '../utils/settingsValidation/mode';
|
|
3
|
+
/**
|
|
4
|
+
* Validates if the given rollout plan is valid.
|
|
5
|
+
*/
|
|
6
|
+
export function validateRolloutPlan(log, settings) {
|
|
7
|
+
var mode = settings.mode, initialRolloutPlan = settings.initialRolloutPlan;
|
|
8
|
+
if (isConsumerMode(mode)) {
|
|
9
|
+
log.warn('storage: initial rollout plan is ignored in consumer mode');
|
|
10
|
+
return;
|
|
11
|
+
}
|
|
12
|
+
if (isObject(initialRolloutPlan) && isObject(initialRolloutPlan.splitChanges))
|
|
13
|
+
return initialRolloutPlan;
|
|
14
|
+
log.error('storage: invalid rollout plan provided');
|
|
15
|
+
return;
|
|
16
|
+
}
|
|
17
|
+
/**
|
|
18
|
+
* Sets the given synchronous storage with the provided rollout plan snapshot.
|
|
19
|
+
* If `matchingKey` is provided, the storage is handled as a client-side storage (segments and largeSegments are instances of MySegmentsCache).
|
|
20
|
+
* Otherwise, the storage is handled as a server-side storage (segments is an instance of SegmentsCache).
|
|
21
|
+
*/
|
|
22
|
+
export function setRolloutPlan(log, rolloutPlan, storage, matchingKey) {
|
|
23
|
+
var splits = storage.splits, rbSegments = storage.rbSegments, segments = storage.segments, largeSegments = storage.largeSegments;
|
|
24
|
+
var _a = rolloutPlan.splitChanges, ff = _a.ff, rbs = _a.rbs;
|
|
25
|
+
log.debug("storage: set feature flags and segments" + (matchingKey ? " for key " + matchingKey : ''));
|
|
26
|
+
if (splits && ff) {
|
|
27
|
+
splits.clear();
|
|
28
|
+
splits.update(ff.d, [], ff.t);
|
|
29
|
+
}
|
|
30
|
+
if (rbSegments && rbs) {
|
|
31
|
+
rbSegments.clear();
|
|
32
|
+
rbSegments.update(rbs.d, [], rbs.t);
|
|
33
|
+
}
|
|
34
|
+
var segmentChanges = rolloutPlan.segmentChanges;
|
|
35
|
+
if (matchingKey) { // add memberships data (client-side)
|
|
36
|
+
var memberships = rolloutPlan.memberships && rolloutPlan.memberships[matchingKey];
|
|
37
|
+
if (!memberships && segmentChanges) {
|
|
38
|
+
memberships = {
|
|
39
|
+
ms: {
|
|
40
|
+
k: segmentChanges.filter(function (segment) {
|
|
41
|
+
return segment.added.indexOf(matchingKey) > -1;
|
|
42
|
+
}).map(function (segment) { return ({ n: segment.name }); })
|
|
43
|
+
}
|
|
44
|
+
};
|
|
45
|
+
}
|
|
46
|
+
if (memberships) {
|
|
47
|
+
if (memberships.ms)
|
|
48
|
+
segments.resetSegments(memberships.ms);
|
|
49
|
+
if (memberships.ls && largeSegments)
|
|
50
|
+
largeSegments.resetSegments(memberships.ls);
|
|
51
|
+
}
|
|
52
|
+
}
|
|
53
|
+
else { // add segments data (server-side)
|
|
54
|
+
if (segmentChanges) {
|
|
55
|
+
segments.clear();
|
|
56
|
+
segmentChanges.forEach(function (segment) {
|
|
57
|
+
segments.update(segment.name, segment.added, segment.removed, segment.till);
|
|
58
|
+
});
|
|
59
|
+
}
|
|
60
|
+
}
|
|
61
|
+
}
|
|
@@ -10,5 +10,4 @@ export { validateTrafficType } from './trafficType';
|
|
|
10
10
|
export { validateIfNotDestroyed, validateIfOperational } from './isOperational';
|
|
11
11
|
export { validateSplitExistence } from './splitExistence';
|
|
12
12
|
export { validateTrafficTypeExistence } from './trafficTypeExistence';
|
|
13
|
-
export { validatePreloadedData } from './preloadedData';
|
|
14
13
|
export { validateEvaluationOptions } from './eventProperties';
|
|
@@ -5,6 +5,7 @@ import { STANDALONE_MODE, OPTIMIZED, LOCALHOST_MODE, DEBUG, FLAG_SPEC_VERSION }
|
|
|
5
5
|
import { validImpressionsMode } from './impressionsMode';
|
|
6
6
|
import { validateKey } from '../inputValidation/key';
|
|
7
7
|
import { ERROR_MIN_CONFIG_PARAM, LOG_PREFIX_CLIENT_INSTANTIATION } from '../../logger/constants';
|
|
8
|
+
import { validateRolloutPlan } from '../../storages/setRolloutPlan';
|
|
8
9
|
// Exported for telemetry
|
|
9
10
|
export var base = {
|
|
10
11
|
// Define which kind of object you want to retrieve from SplitFactory
|
|
@@ -128,6 +129,9 @@ export function settingsValidation(config, validationParams) {
|
|
|
128
129
|
// @ts-ignore, modify readonly prop
|
|
129
130
|
if (storage)
|
|
130
131
|
withDefaults.storage = storage(withDefaults);
|
|
132
|
+
// @ts-ignore, modify readonly prop
|
|
133
|
+
if (withDefaults.initialRolloutPlan)
|
|
134
|
+
withDefaults.initialRolloutPlan = validateRolloutPlan(log, withDefaults);
|
|
131
135
|
// Validate key and TT (for client-side)
|
|
132
136
|
var maybeKey = withDefaults.core.key;
|
|
133
137
|
if (validationParams.acceptKey) {
|
package/package.json
CHANGED
|
@@ -1,3 +1,5 @@
|
|
|
1
|
+
import { IBetweenMatcherData } from '../../dtos/types';
|
|
2
|
+
|
|
1
3
|
export function zeroSinceHH(millisSinceEpoch: number): number {
|
|
2
4
|
return new Date(millisSinceEpoch).setUTCHours(0, 0, 0, 0);
|
|
3
5
|
}
|
|
@@ -5,3 +7,11 @@ export function zeroSinceHH(millisSinceEpoch: number): number {
|
|
|
5
7
|
export function zeroSinceSS(millisSinceEpoch: number): number {
|
|
6
8
|
return new Date(millisSinceEpoch).setUTCSeconds(0, 0);
|
|
7
9
|
}
|
|
10
|
+
|
|
11
|
+
export function betweenDateTimeTransform(betweenMatcherData: IBetweenMatcherData): IBetweenMatcherData {
|
|
12
|
+
return {
|
|
13
|
+
dataType: betweenMatcherData.dataType,
|
|
14
|
+
start: zeroSinceSS(betweenMatcherData.start),
|
|
15
|
+
end: zeroSinceSS(betweenMatcherData.end)
|
|
16
|
+
};
|
|
17
|
+
}
|
|
@@ -3,7 +3,7 @@ import { matcherTypes, matcherTypesMapper, matcherDataTypes } from '../matchers/
|
|
|
3
3
|
import { segmentTransform } from './segment';
|
|
4
4
|
import { whitelistTransform } from './whitelist';
|
|
5
5
|
import { numericTransform } from './unaryNumeric';
|
|
6
|
-
import { zeroSinceHH, zeroSinceSS } from '../convertions';
|
|
6
|
+
import { zeroSinceHH, zeroSinceSS, betweenDateTimeTransform } from '../convertions';
|
|
7
7
|
import { IBetweenMatcherData, IInLargeSegmentMatcherData, IInSegmentMatcherData, ISplitMatcher, IUnaryNumericMatcherData } from '../../dtos/types';
|
|
8
8
|
import { IMatcherDto } from '../types';
|
|
9
9
|
|
|
@@ -32,7 +32,7 @@ export function matchersTransform(matchers: ISplitMatcher[]): IMatcherDto[] {
|
|
|
32
32
|
let type = matcherTypesMapper(matcherType);
|
|
33
33
|
// As default input data type we use string (even for ALL_KEYS)
|
|
34
34
|
let dataType = matcherDataTypes.STRING;
|
|
35
|
-
let value
|
|
35
|
+
let value;
|
|
36
36
|
|
|
37
37
|
if (type === matcherTypes.IN_SEGMENT) {
|
|
38
38
|
value = segmentTransform(userDefinedSegmentMatcherData as IInSegmentMatcherData);
|
|
@@ -60,8 +60,7 @@ export function matchersTransform(matchers: ISplitMatcher[]): IMatcherDto[] {
|
|
|
60
60
|
dataType = matcherDataTypes.NUMBER;
|
|
61
61
|
|
|
62
62
|
if (value.dataType === 'DATETIME') {
|
|
63
|
-
value
|
|
64
|
-
value.end = zeroSinceSS(value.end);
|
|
63
|
+
value = betweenDateTimeTransform(value);
|
|
65
64
|
dataType = matcherDataTypes.DATETIME;
|
|
66
65
|
}
|
|
67
66
|
} else if (type === matcherTypes.BETWEEN_SEMVER) {
|
|
@@ -9,13 +9,15 @@ import { RETRIEVE_CLIENT_DEFAULT, NEW_SHARED_CLIENT, RETRIEVE_CLIENT_EXISTING, L
|
|
|
9
9
|
import { SDK_SEGMENTS_ARRIVED } from '../readiness/constants';
|
|
10
10
|
import { ISdkFactoryContext } from '../sdkFactory/types';
|
|
11
11
|
import { buildInstanceId } from './identity';
|
|
12
|
+
import { setRolloutPlan } from '../storages/setRolloutPlan';
|
|
13
|
+
import { ISegmentsCacheSync } from '../storages/types';
|
|
12
14
|
|
|
13
15
|
/**
|
|
14
16
|
* Factory of client method for the client-side API variant where TT is ignored.
|
|
15
17
|
* Therefore, clients don't have a bound TT for the track method.
|
|
16
18
|
*/
|
|
17
19
|
export function sdkClientMethodCSFactory(params: ISdkFactoryContext): (key?: SplitIO.SplitKey) => SplitIO.IBrowserClient {
|
|
18
|
-
const { clients, storage, syncManager, sdkReadinessManager, settings: { core: { key }, log } } = params;
|
|
20
|
+
const { clients, storage, syncManager, sdkReadinessManager, settings: { core: { key }, log, initialRolloutPlan } } = params;
|
|
19
21
|
|
|
20
22
|
const mainClientInstance = clientCSDecorator(
|
|
21
23
|
log,
|
|
@@ -56,6 +58,10 @@ export function sdkClientMethodCSFactory(params: ISdkFactoryContext): (key?: Spl
|
|
|
56
58
|
sharedSdkReadiness.readinessManager.segments.emit(SDK_SEGMENTS_ARRIVED);
|
|
57
59
|
});
|
|
58
60
|
|
|
61
|
+
if (sharedStorage && initialRolloutPlan) {
|
|
62
|
+
setRolloutPlan(log, initialRolloutPlan, { segments: sharedStorage.segments as ISegmentsCacheSync, largeSegments: sharedStorage.largeSegments as ISegmentsCacheSync }, matchingKey);
|
|
63
|
+
}
|
|
64
|
+
|
|
59
65
|
// 3 possibilities:
|
|
60
66
|
// - Standalone mode: both syncManager and sharedSyncManager are defined
|
|
61
67
|
// - Consumer mode: both syncManager and sharedSyncManager are undefined
|
package/src/sdkFactory/index.ts
CHANGED
|
@@ -14,6 +14,9 @@ import { strategyOptimizedFactory } from '../trackers/strategy/strategyOptimized
|
|
|
14
14
|
import { strategyNoneFactory } from '../trackers/strategy/strategyNone';
|
|
15
15
|
import { uniqueKeysTrackerFactory } from '../trackers/uniqueKeysTracker';
|
|
16
16
|
import { DEBUG, OPTIMIZED } from '../utils/constants';
|
|
17
|
+
import { setRolloutPlan } from '../storages/setRolloutPlan';
|
|
18
|
+
import { IStorageSync } from '../storages/types';
|
|
19
|
+
import { getMatching } from '../utils/key';
|
|
17
20
|
|
|
18
21
|
/**
|
|
19
22
|
* Modular SDK factory
|
|
@@ -24,7 +27,7 @@ export function sdkFactory(params: ISdkFactoryParams): SplitIO.ISDK | SplitIO.IA
|
|
|
24
27
|
syncManagerFactory, SignalListener, impressionsObserverFactory,
|
|
25
28
|
integrationsManagerFactory, sdkManagerFactory, sdkClientMethodFactory,
|
|
26
29
|
filterAdapterFactory, lazyInit } = params;
|
|
27
|
-
const { log, sync: { impressionsMode } } = settings;
|
|
30
|
+
const { log, sync: { impressionsMode }, initialRolloutPlan, core: { key } } = settings;
|
|
28
31
|
|
|
29
32
|
// @TODO handle non-recoverable errors, such as, global `fetch` not available, invalid SDK Key, etc.
|
|
30
33
|
// On non-recoverable errors, we should mark the SDK as destroyed and not start synchronization.
|
|
@@ -57,7 +60,11 @@ export function sdkFactory(params: ISdkFactoryParams): SplitIO.ISDK | SplitIO.IA
|
|
|
57
60
|
}
|
|
58
61
|
});
|
|
59
62
|
|
|
60
|
-
|
|
63
|
+
if (initialRolloutPlan) {
|
|
64
|
+
setRolloutPlan(log, initialRolloutPlan, storage as IStorageSync, key && getMatching(key));
|
|
65
|
+
if ((storage as IStorageSync).splits.getChangeNumber() > -1) readiness.splits.emit(SDK_SPLITS_CACHE_LOADED);
|
|
66
|
+
}
|
|
67
|
+
|
|
61
68
|
const clients: Record<string, SplitIO.IBasicClient> = {};
|
|
62
69
|
const telemetryTracker = telemetryTrackerFactory(storage.telemetry, platform.now);
|
|
63
70
|
const integrationsManager = integrationsManagerFactory && integrationsManagerFactory({ settings, storage, telemetryTracker });
|
|
@@ -0,0 +1,72 @@
|
|
|
1
|
+
import SplitIO from '../../types/splitio';
|
|
2
|
+
import { IStorageSync } from './types';
|
|
3
|
+
import { setToArray } from '../utils/lang/sets';
|
|
4
|
+
import { getMatching } from '../utils/key';
|
|
5
|
+
import { ILogger } from '../logger/types';
|
|
6
|
+
import { RolloutPlan } from './types';
|
|
7
|
+
import { IMembershipsResponse, IMySegmentsResponse } from '../dtos/types';
|
|
8
|
+
|
|
9
|
+
/**
|
|
10
|
+
* Gets the rollout plan snapshot from the given synchronous storage.
|
|
11
|
+
*/
|
|
12
|
+
export function getRolloutPlan(log: ILogger, storage: IStorageSync, options: SplitIO.RolloutPlanOptions = {}): RolloutPlan {
|
|
13
|
+
|
|
14
|
+
const { keys, exposeSegments } = options;
|
|
15
|
+
const { splits, segments, rbSegments } = storage;
|
|
16
|
+
|
|
17
|
+
log.debug(`storage: get feature flags${keys ? `, and memberships for keys ${keys}` : ''}${exposeSegments ? ', and segments' : ''}`);
|
|
18
|
+
|
|
19
|
+
return {
|
|
20
|
+
splitChanges: {
|
|
21
|
+
ff: {
|
|
22
|
+
t: splits.getChangeNumber(),
|
|
23
|
+
s: -1,
|
|
24
|
+
d: splits.getAll(),
|
|
25
|
+
},
|
|
26
|
+
rbs: {
|
|
27
|
+
t: rbSegments.getChangeNumber(),
|
|
28
|
+
s: -1,
|
|
29
|
+
d: rbSegments.getAll(),
|
|
30
|
+
}
|
|
31
|
+
},
|
|
32
|
+
segmentChanges: exposeSegments ? // @ts-ignore accessing private prop
|
|
33
|
+
Object.keys(segments.segmentCache).map(segmentName => ({
|
|
34
|
+
name: segmentName, // @ts-ignore
|
|
35
|
+
added: setToArray(segments.segmentCache[segmentName] as Set<string>),
|
|
36
|
+
removed: [],
|
|
37
|
+
since: -1,
|
|
38
|
+
till: segments.getChangeNumber(segmentName)!
|
|
39
|
+
})) :
|
|
40
|
+
undefined,
|
|
41
|
+
memberships: keys ?
|
|
42
|
+
keys.reduce<Record<string, IMembershipsResponse>>((prev, key) => {
|
|
43
|
+
const matchingKey = getMatching(key);
|
|
44
|
+
if (storage.shared) { // Client-side segments
|
|
45
|
+
const sharedStorage = storage.shared(matchingKey);
|
|
46
|
+
prev[matchingKey] = {
|
|
47
|
+
ms: { // @ts-ignore
|
|
48
|
+
k: Object.keys(sharedStorage.segments.segmentCache).map(segmentName => ({ n: segmentName })),
|
|
49
|
+
},
|
|
50
|
+
ls: sharedStorage.largeSegments ? { // @ts-ignore
|
|
51
|
+
k: Object.keys(sharedStorage.largeSegments.segmentCache).map(segmentName => ({ n: segmentName })),
|
|
52
|
+
} : undefined
|
|
53
|
+
};
|
|
54
|
+
} else { // Server-side segments
|
|
55
|
+
prev[matchingKey] = {
|
|
56
|
+
ms: { // @ts-ignore
|
|
57
|
+
k: Object.keys(storage.segments.segmentCache).reduce<IMySegmentsResponse['k']>((prev, segmentName) => { // @ts-ignore
|
|
58
|
+
return storage.segments.segmentCache[segmentName].has(matchingKey) ?
|
|
59
|
+
prev!.concat({ n: segmentName }) :
|
|
60
|
+
prev;
|
|
61
|
+
}, [])
|
|
62
|
+
},
|
|
63
|
+
ls: {
|
|
64
|
+
k: []
|
|
65
|
+
}
|
|
66
|
+
};
|
|
67
|
+
}
|
|
68
|
+
return prev;
|
|
69
|
+
}, {}) :
|
|
70
|
+
undefined
|
|
71
|
+
};
|
|
72
|
+
}
|
|
@@ -17,7 +17,7 @@ const MILLIS_IN_A_DAY = 86400000;
|
|
|
17
17
|
* @returns `true` if cache should be cleared, `false` otherwise
|
|
18
18
|
*/
|
|
19
19
|
function validateExpiration(options: SplitIO.InLocalStorageOptions, settings: ISettings, keys: KeyBuilderCS, currentTimestamp: number, isThereCache: boolean) {
|
|
20
|
-
const { log } = settings;
|
|
20
|
+
const { log, initialRolloutPlan } = settings;
|
|
21
21
|
|
|
22
22
|
// Check expiration
|
|
23
23
|
const lastUpdatedTimestamp = parseInt(localStorage.getItem(keys.buildLastUpdatedKey()) as string, 10);
|
|
@@ -41,7 +41,7 @@ function validateExpiration(options: SplitIO.InLocalStorageOptions, settings: IS
|
|
|
41
41
|
} catch (e) {
|
|
42
42
|
log.error(LOG_PREFIX + e);
|
|
43
43
|
}
|
|
44
|
-
if (isThereCache) {
|
|
44
|
+
if (isThereCache && !initialRolloutPlan) {
|
|
45
45
|
log.info(LOG_PREFIX + 'SDK key, flags filter criteria, or flags spec version has changed. Cleaning up cache');
|
|
46
46
|
return true;
|
|
47
47
|
}
|
|
@@ -7,8 +7,6 @@ import { ImpressionCountsCacheInMemory } from './ImpressionCountsCacheInMemory';
|
|
|
7
7
|
import { LOCALHOST_MODE, STORAGE_MEMORY } from '../../utils/constants';
|
|
8
8
|
import { shouldRecordTelemetry, TelemetryCacheInMemory } from './TelemetryCacheInMemory';
|
|
9
9
|
import { UniqueKeysCacheInMemoryCS } from './UniqueKeysCacheInMemoryCS';
|
|
10
|
-
import { getMatching } from '../../utils/key';
|
|
11
|
-
import { setCache } from '../dataLoader';
|
|
12
10
|
import { RBSegmentsCacheInMemory } from './RBSegmentsCacheInMemory';
|
|
13
11
|
|
|
14
12
|
/**
|
|
@@ -17,9 +15,7 @@ import { RBSegmentsCacheInMemory } from './RBSegmentsCacheInMemory';
|
|
|
17
15
|
* @param params - parameters required by EventsCacheSync
|
|
18
16
|
*/
|
|
19
17
|
export function InMemoryStorageCSFactory(params: IStorageFactoryParams): IStorageSync {
|
|
20
|
-
const { settings: {
|
|
21
|
-
|
|
22
|
-
const storages: Record<string, IStorageSync> = {};
|
|
18
|
+
const { settings: { scheduler: { impressionsQueueSize, eventsQueueSize }, sync: { __splitFiltersValidation } } } = params;
|
|
23
19
|
|
|
24
20
|
const splits = new SplitsCacheInMemory(__splitFiltersValidation);
|
|
25
21
|
const rbSegments = new RBSegmentsCacheInMemory();
|
|
@@ -40,31 +36,20 @@ export function InMemoryStorageCSFactory(params: IStorageFactoryParams): IStorag
|
|
|
40
36
|
destroy() { },
|
|
41
37
|
|
|
42
38
|
// When using shared instantiation with MEMORY we reuse everything but segments (they are unique per key)
|
|
43
|
-
shared(
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
rbSegments: this.rbSegments,
|
|
55
|
-
segments,
|
|
56
|
-
largeSegments,
|
|
57
|
-
impressions: this.impressions,
|
|
58
|
-
impressionCounts: this.impressionCounts,
|
|
59
|
-
events: this.events,
|
|
60
|
-
telemetry: this.telemetry,
|
|
61
|
-
uniqueKeys: this.uniqueKeys,
|
|
62
|
-
|
|
63
|
-
destroy() { }
|
|
64
|
-
};
|
|
65
|
-
}
|
|
39
|
+
shared() {
|
|
40
|
+
return {
|
|
41
|
+
splits: this.splits,
|
|
42
|
+
rbSegments: this.rbSegments,
|
|
43
|
+
segments: new MySegmentsCacheInMemory(),
|
|
44
|
+
largeSegments: new MySegmentsCacheInMemory(),
|
|
45
|
+
impressions: this.impressions,
|
|
46
|
+
impressionCounts: this.impressionCounts,
|
|
47
|
+
events: this.events,
|
|
48
|
+
telemetry: this.telemetry,
|
|
49
|
+
uniqueKeys: this.uniqueKeys,
|
|
66
50
|
|
|
67
|
-
|
|
51
|
+
destroy() { }
|
|
52
|
+
};
|
|
68
53
|
},
|
|
69
54
|
};
|
|
70
55
|
|
|
@@ -78,14 +63,6 @@ export function InMemoryStorageCSFactory(params: IStorageFactoryParams): IStorag
|
|
|
78
63
|
storage.uniqueKeys.track = noopTrack;
|
|
79
64
|
}
|
|
80
65
|
|
|
81
|
-
const matchingKey = getMatching(params.settings.core.key);
|
|
82
|
-
storages[matchingKey] = storage;
|
|
83
|
-
|
|
84
|
-
if (preloadedData) {
|
|
85
|
-
setCache(log, preloadedData, storage, matchingKey);
|
|
86
|
-
if (splits.getChangeNumber() > -1) onReadyFromCacheCb();
|
|
87
|
-
}
|
|
88
|
-
|
|
89
66
|
return storage;
|
|
90
67
|
}
|
|
91
68
|
|
|
@@ -0,0 +1,71 @@
|
|
|
1
|
+
import SplitIO from '../../types/splitio';
|
|
2
|
+
import { IRBSegmentsCacheSync, ISegmentsCacheSync, ISplitsCacheSync } from './types';
|
|
3
|
+
import { ILogger } from '../logger/types';
|
|
4
|
+
import { isObject } from '../utils/lang';
|
|
5
|
+
import { isConsumerMode } from '../utils/settingsValidation/mode';
|
|
6
|
+
import { RolloutPlan } from './types';
|
|
7
|
+
|
|
8
|
+
/**
|
|
9
|
+
* Validates if the given rollout plan is valid.
|
|
10
|
+
*/
|
|
11
|
+
export function validateRolloutPlan(log: ILogger, settings: SplitIO.ISettings): RolloutPlan | undefined {
|
|
12
|
+
const { mode, initialRolloutPlan } = settings;
|
|
13
|
+
|
|
14
|
+
if (isConsumerMode(mode)) {
|
|
15
|
+
log.warn('storage: initial rollout plan is ignored in consumer mode');
|
|
16
|
+
return;
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
if (isObject(initialRolloutPlan) && isObject((initialRolloutPlan as any).splitChanges)) return initialRolloutPlan as RolloutPlan;
|
|
20
|
+
|
|
21
|
+
log.error('storage: invalid rollout plan provided');
|
|
22
|
+
return;
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
/**
|
|
26
|
+
* Sets the given synchronous storage with the provided rollout plan snapshot.
|
|
27
|
+
* If `matchingKey` is provided, the storage is handled as a client-side storage (segments and largeSegments are instances of MySegmentsCache).
|
|
28
|
+
* Otherwise, the storage is handled as a server-side storage (segments is an instance of SegmentsCache).
|
|
29
|
+
*/
|
|
30
|
+
export function setRolloutPlan(log: ILogger, rolloutPlan: RolloutPlan, storage: { splits?: ISplitsCacheSync, rbSegments?: IRBSegmentsCacheSync, segments: ISegmentsCacheSync, largeSegments?: ISegmentsCacheSync }, matchingKey?: string) {
|
|
31
|
+
const { splits, rbSegments, segments, largeSegments } = storage;
|
|
32
|
+
const { splitChanges: { ff, rbs } } = rolloutPlan;
|
|
33
|
+
|
|
34
|
+
log.debug(`storage: set feature flags and segments${matchingKey ? ` for key ${matchingKey}` : ''}`);
|
|
35
|
+
|
|
36
|
+
if (splits && ff) {
|
|
37
|
+
splits.clear();
|
|
38
|
+
splits.update(ff.d, [], ff.t);
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
if (rbSegments && rbs) {
|
|
42
|
+
rbSegments.clear();
|
|
43
|
+
rbSegments.update(rbs.d, [], rbs.t);
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
const segmentChanges = rolloutPlan.segmentChanges;
|
|
47
|
+
if (matchingKey) { // add memberships data (client-side)
|
|
48
|
+
let memberships = rolloutPlan.memberships && rolloutPlan.memberships[matchingKey];
|
|
49
|
+
if (!memberships && segmentChanges) {
|
|
50
|
+
memberships = {
|
|
51
|
+
ms: {
|
|
52
|
+
k: segmentChanges.filter(segment => {
|
|
53
|
+
return segment.added.indexOf(matchingKey) > -1;
|
|
54
|
+
}).map(segment => ({ n: segment.name }))
|
|
55
|
+
}
|
|
56
|
+
};
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
if (memberships) {
|
|
60
|
+
if (memberships.ms) segments.resetSegments(memberships.ms!);
|
|
61
|
+
if (memberships.ls && largeSegments) largeSegments.resetSegments(memberships.ls!);
|
|
62
|
+
}
|
|
63
|
+
} else { // add segments data (server-side)
|
|
64
|
+
if (segmentChanges) {
|
|
65
|
+
segments.clear();
|
|
66
|
+
segmentChanges.forEach(segment => {
|
|
67
|
+
segments.update(segment.name, segment.added, segment.removed, segment.till);
|
|
68
|
+
});
|
|
69
|
+
}
|
|
70
|
+
}
|
|
71
|
+
}
|
package/src/storages/types.ts
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import SplitIO from '../../types/splitio';
|
|
2
|
-
import { MaybeThenable, ISplit, IRBSegment, IMySegmentsResponse } from '../dtos/types';
|
|
2
|
+
import { MaybeThenable, ISplit, IRBSegment, IMySegmentsResponse, IMembershipsResponse, ISegmentChangesResponse, ISplitChangesResponse } from '../dtos/types';
|
|
3
3
|
import { MySegmentsData } from '../sync/polling/types';
|
|
4
4
|
import { EventDataType, HttpErrors, HttpLatencies, ImpressionDataType, LastSync, Method, MethodExceptions, MethodLatencies, MultiMethodExceptions, MultiMethodLatencies, MultiConfigs, OperationType, StoredEventWithMetadata, StoredImpressionWithMetadata, StreamingEvent, UniqueKeysPayloadCs, UniqueKeysPayloadSs, TelemetryUsageStatsPayload, UpdatesFromSSEEnum } from '../sync/submitters/types';
|
|
5
5
|
import { ISettings } from '../types';
|
|
@@ -505,7 +505,7 @@ export interface IStorageFactoryParams {
|
|
|
505
505
|
*/
|
|
506
506
|
onReadyCb: (error?: any) => void,
|
|
507
507
|
/**
|
|
508
|
-
* For emitting SDK_READY_FROM_CACHE event in consumer mode with Redis
|
|
508
|
+
* For emitting SDK_READY_FROM_CACHE event in consumer mode with Redis to allow immediate evaluations
|
|
509
509
|
*/
|
|
510
510
|
onReadyFromCacheCb: () => void,
|
|
511
511
|
}
|
|
@@ -520,3 +520,21 @@ export type IStorageAsyncFactory = SplitIO.StorageAsyncFactory & {
|
|
|
520
520
|
readonly type: SplitIO.StorageType,
|
|
521
521
|
(params: IStorageFactoryParams): IStorageAsync
|
|
522
522
|
}
|
|
523
|
+
|
|
524
|
+
export type RolloutPlan = {
|
|
525
|
+
/**
|
|
526
|
+
* Feature flags and rule-based segments.
|
|
527
|
+
*/
|
|
528
|
+
splitChanges: ISplitChangesResponse;
|
|
529
|
+
/**
|
|
530
|
+
* Optional map of matching keys to their memberships.
|
|
531
|
+
*/
|
|
532
|
+
memberships?: {
|
|
533
|
+
[matchingKey: string]: IMembershipsResponse;
|
|
534
|
+
};
|
|
535
|
+
/**
|
|
536
|
+
* Optional list of standard segments.
|
|
537
|
+
* This property is ignored if `memberships` is provided.
|
|
538
|
+
*/
|
|
539
|
+
segmentChanges?: ISegmentChangesResponse[];
|
|
540
|
+
};
|
package/src/types.ts
CHANGED
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import SplitIO from '../types/splitio';
|
|
2
2
|
import { ISplitFiltersValidation } from './dtos/types';
|
|
3
3
|
import { ILogger } from './logger/types';
|
|
4
|
+
import { RolloutPlan } from './storages/types';
|
|
4
5
|
|
|
5
6
|
/**
|
|
6
7
|
* SplitIO.ISettings interface extended with private properties for internal use
|
|
@@ -10,6 +11,7 @@ export interface ISettings extends SplitIO.ISettings {
|
|
|
10
11
|
__splitFiltersValidation: ISplitFiltersValidation;
|
|
11
12
|
};
|
|
12
13
|
readonly log: ILogger;
|
|
14
|
+
readonly initialRolloutPlan?: RolloutPlan;
|
|
13
15
|
}
|
|
14
16
|
|
|
15
17
|
/**
|
|
@@ -10,5 +10,4 @@ export { validateTrafficType } from './trafficType';
|
|
|
10
10
|
export { validateIfNotDestroyed, validateIfOperational } from './isOperational';
|
|
11
11
|
export { validateSplitExistence } from './splitExistence';
|
|
12
12
|
export { validateTrafficTypeExistence } from './trafficTypeExistence';
|
|
13
|
-
export { validatePreloadedData } from './preloadedData';
|
|
14
13
|
export { validateEvaluationOptions } from './eventProperties';
|
|
@@ -7,6 +7,7 @@ import { ISettingsValidationParams } from './types';
|
|
|
7
7
|
import { ISettings } from '../../types';
|
|
8
8
|
import { validateKey } from '../inputValidation/key';
|
|
9
9
|
import { ERROR_MIN_CONFIG_PARAM, LOG_PREFIX_CLIENT_INSTANTIATION } from '../../logger/constants';
|
|
10
|
+
import { validateRolloutPlan } from '../../storages/setRolloutPlan';
|
|
10
11
|
|
|
11
12
|
// Exported for telemetry
|
|
12
13
|
export const base = {
|
|
@@ -152,6 +153,9 @@ export function settingsValidation(config: unknown, validationParams: ISettingsV
|
|
|
152
153
|
// @ts-ignore, modify readonly prop
|
|
153
154
|
if (storage) withDefaults.storage = storage(withDefaults);
|
|
154
155
|
|
|
156
|
+
// @ts-ignore, modify readonly prop
|
|
157
|
+
if (withDefaults.initialRolloutPlan) withDefaults.initialRolloutPlan = validateRolloutPlan(log, withDefaults);
|
|
158
|
+
|
|
155
159
|
// Validate key and TT (for client-side)
|
|
156
160
|
const maybeKey = withDefaults.core.key;
|
|
157
161
|
if (validationParams.acceptKey) {
|
package/types/splitio.d.ts
CHANGED
|
@@ -351,9 +351,10 @@ interface IClientSideSyncSharedSettings extends IClientSideSharedSettings, ISync
|
|
|
351
351
|
*/
|
|
352
352
|
features?: SplitIO.MockedFeaturesMap;
|
|
353
353
|
/**
|
|
354
|
-
*
|
|
354
|
+
* Rollout plan object (i.e., feature flags and segment definitions) to initialize the SDK storage with. If provided and valid, the SDK will be ready from cache immediately.
|
|
355
|
+
* This object is derived from calling the Node.js SDK’s `getRolloutPlan` method.
|
|
355
356
|
*/
|
|
356
|
-
|
|
357
|
+
initialRolloutPlan?: SplitIO.RolloutPlan;
|
|
357
358
|
/**
|
|
358
359
|
* SDK Startup settings.
|
|
359
360
|
*/
|
|
@@ -559,7 +560,7 @@ declare namespace SplitIO {
|
|
|
559
560
|
eventsFirstPushWindow: number;
|
|
560
561
|
};
|
|
561
562
|
readonly storage: StorageSyncFactory | StorageAsyncFactory | StorageOptions;
|
|
562
|
-
readonly
|
|
563
|
+
readonly initialRolloutPlan?: SplitIO.RolloutPlan;
|
|
563
564
|
readonly urls: {
|
|
564
565
|
events: string;
|
|
565
566
|
sdk: string;
|
|
@@ -1025,41 +1026,28 @@ declare namespace SplitIO {
|
|
|
1025
1026
|
type: NodeSyncStorage | NodeAsyncStorage | BrowserStorage;
|
|
1026
1027
|
prefix?: string;
|
|
1027
1028
|
options?: Object;
|
|
1028
|
-
}
|
|
1029
|
+
};
|
|
1029
1030
|
/**
|
|
1030
|
-
*
|
|
1031
|
+
* A JSON-serializable plain object that defines the format of rollout plan data to preload the SDK cache with feature flags and segments.
|
|
1031
1032
|
*/
|
|
1032
|
-
type
|
|
1033
|
-
|
|
1034
|
-
|
|
1035
|
-
|
|
1036
|
-
|
|
1037
|
-
/**
|
|
1038
|
-
* List of feature flags.
|
|
1039
|
-
*/
|
|
1040
|
-
flags: Object[],
|
|
1041
|
-
/**
|
|
1042
|
-
* Change number of rule-based segments.
|
|
1043
|
-
*/
|
|
1044
|
-
rbSince?: number,
|
|
1045
|
-
/**
|
|
1046
|
-
* List of rule-based segments.
|
|
1047
|
-
*/
|
|
1048
|
-
rbSegments?: Object[],
|
|
1033
|
+
type RolloutPlan = Object;
|
|
1034
|
+
/**
|
|
1035
|
+
* Options for the `factory.getRolloutPlan` method.
|
|
1036
|
+
*/
|
|
1037
|
+
type RolloutPlanOptions = {
|
|
1049
1038
|
/**
|
|
1050
|
-
* Optional
|
|
1039
|
+
* Optional list of keys to generate the rollout plan snapshot with the memberships of the given keys.
|
|
1040
|
+
*
|
|
1041
|
+
* @defaultValue `undefined`
|
|
1051
1042
|
*/
|
|
1052
|
-
|
|
1053
|
-
[key: string]: Object
|
|
1054
|
-
},
|
|
1043
|
+
keys?: SplitKey[];
|
|
1055
1044
|
/**
|
|
1056
|
-
* Optional
|
|
1057
|
-
*
|
|
1045
|
+
* Optional flag to expose segments data in the rollout plan snapshot.
|
|
1046
|
+
*
|
|
1047
|
+
* @defaultValue `false`
|
|
1058
1048
|
*/
|
|
1059
|
-
|
|
1060
|
-
|
|
1061
|
-
},
|
|
1062
|
-
}
|
|
1049
|
+
exposeSegments?: boolean;
|
|
1050
|
+
};
|
|
1063
1051
|
/**
|
|
1064
1052
|
* Impression listener interface. This is the interface that needs to be implemented
|
|
1065
1053
|
* by the element you provide to the SDK as impression listener.
|
|
@@ -1082,7 +1070,7 @@ declare namespace SplitIO {
|
|
|
1082
1070
|
type IntegrationFactory = {
|
|
1083
1071
|
readonly type: string;
|
|
1084
1072
|
(params: any): (Integration | void);
|
|
1085
|
-
}
|
|
1073
|
+
};
|
|
1086
1074
|
/**
|
|
1087
1075
|
* A pair of user key and it's trafficType, required for tracking valid Split events.
|
|
1088
1076
|
*/
|
|
@@ -1609,7 +1597,7 @@ declare namespace SplitIO {
|
|
|
1609
1597
|
* @param keys - Optional list of keys to generate the rollout plan snapshot with the memberships of the given keys, rather than the complete segments data.
|
|
1610
1598
|
* @returns The current snapshot of the SDK rollout plan.
|
|
1611
1599
|
*/
|
|
1612
|
-
|
|
1600
|
+
getRolloutPlan(options?: RolloutPlanOptions): RolloutPlan;
|
|
1613
1601
|
}
|
|
1614
1602
|
/**
|
|
1615
1603
|
* This represents the interface for the SDK instance for server-side with asynchronous storage.
|