Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
21 changes: 13 additions & 8 deletions src/dtos/types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -66,6 +66,11 @@ interface IInSegmentMatcher extends ISplitMatcherBase {
userDefinedSegmentMatcherData: IInSegmentMatcherData
}

interface IInRBSegmentMatcher extends ISplitMatcherBase {
matcherType: 'IN_RULE_BASED_SEGMENT',
userDefinedSegmentMatcherData: IInSegmentMatcherData
}

interface IInLargeSegmentMatcher extends ISplitMatcherBase {
matcherType: 'IN_LARGE_SEGMENT',
userDefinedLargeSegmentMatcherData: IInLargeSegmentMatcherData
Expand Down Expand Up @@ -176,7 +181,7 @@ export type ISplitMatcher = IAllKeysMatcher | IInSegmentMatcher | IWhitelistMatc
ILessThanOrEqualToMatcher | IBetweenMatcher | IEqualToSetMatcher | IContainsAnyOfSetMatcher | IContainsAllOfSetMatcher | IPartOfSetMatcher |
IStartsWithMatcher | IEndsWithMatcher | IContainsStringMatcher | IInSplitTreatmentMatcher | IEqualToBooleanMatcher | IMatchesStringMatcher |
IEqualToSemverMatcher | IGreaterThanOrEqualToSemverMatcher | ILessThanOrEqualToSemverMatcher | IBetweenSemverMatcher | IInListSemverMatcher |
IInLargeSegmentMatcher
IInLargeSegmentMatcher | IInRBSegmentMatcher

/** Split object */
export interface ISplitPartition {
Expand All @@ -189,30 +194,30 @@ export interface ISplitCondition {
combiner: 'AND',
matchers: ISplitMatcher[]
}
partitions: ISplitPartition[]
label: string
conditionType: 'ROLLOUT' | 'WHITELIST'
partitions?: ISplitPartition[]
label?: string
conditionType?: 'ROLLOUT' | 'WHITELIST'
}

export interface IRBSegment {
name: string,
changeNumber: number,
status: 'ACTIVE' | 'ARCHIVED',
conditions: ISplitCondition[],
excluded: {
keys: string[],
segments: string[]
},
conditions: ISplitCondition[],
}
}

export interface ISplit {
name: string,
changeNumber: number,
status: 'ACTIVE' | 'ARCHIVED',
conditions: ISplitCondition[],
killed: boolean,
defaultTreatment: string,
trafficTypeName: string,
conditions: ISplitCondition[],
status: 'ACTIVE' | 'ARCHIVED',
seed: number,
trafficAllocation?: number,
trafficAllocationSeed?: number
Expand Down
2 changes: 1 addition & 1 deletion src/evaluator/Engine.ts
Original file line number Diff line number Diff line change
Expand Up @@ -73,7 +73,7 @@ export class Engine {
trafficAllocationSeed,
attributes,
splitEvaluator
);
) as MaybeThenable<IEvaluation>;

// Evaluation could be async, so we should handle that case checking for a
// thenable object
Expand Down
9 changes: 5 additions & 4 deletions src/evaluator/combiners/and.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,10 +2,11 @@ import { findIndex } from '../../utils/lang';
import { ILogger } from '../../logger/types';
import { thenable } from '../../utils/promise/thenable';
import { MaybeThenable } from '../../dtos/types';
import { IMatcher } from '../types';
import { ISplitEvaluator } from '../types';
import { ENGINE_COMBINER_AND } from '../../logger/constants';
import SplitIO from '../../../types/splitio';

export function andCombinerContext(log: ILogger, matchers: IMatcher[]) {
export function andCombinerContext(log: ILogger, matchers: Array<(key: SplitIO.SplitKey, attributes?: SplitIO.Attributes, splitEvaluator?: ISplitEvaluator) => MaybeThenable<boolean>>) {

function andResults(results: boolean[]): boolean {
// Array.prototype.every is supported by target environments
Expand All @@ -15,8 +16,8 @@ export function andCombinerContext(log: ILogger, matchers: IMatcher[]) {
return hasMatchedAll;
}

return function andCombiner(...params: any): MaybeThenable<boolean> {
const matcherResults = matchers.map(matcher => matcher(...params));
return function andCombiner(key: SplitIO.SplitKey, attributes?: SplitIO.Attributes, splitEvaluator?: ISplitEvaluator): MaybeThenable<boolean> {
const matcherResults = matchers.map(matcher => matcher(key, attributes, splitEvaluator));

// If any matching result is a thenable we should use Promise.all
if (findIndex(matcherResults, thenable) !== -1) {
Expand Down
16 changes: 7 additions & 9 deletions src/evaluator/combiners/ifelseif.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import { findIndex } from '../../utils/lang';
import { findIndex, isBoolean } from '../../utils/lang';
import { ILogger } from '../../logger/types';
import { thenable } from '../../utils/promise/thenable';
import { UNSUPPORTED_MATCHER_TYPE } from '../../utils/labels';
Expand All @@ -18,14 +18,12 @@ export function ifElseIfCombinerContext(log: ILogger, predicates: IEvaluator[]):
};
}

function computeTreatment(predicateResults: Array<IEvaluation | undefined>) {
const len = predicateResults.length;

for (let i = 0; i < len; i++) {
function computeEvaluation(predicateResults: Array<IEvaluation | boolean | undefined>): IEvaluation | boolean | undefined {
for (let i = 0, len = predicateResults.length; i < len; i++) {
const evaluation = predicateResults[i];

if (evaluation !== undefined) {
log.debug(ENGINE_COMBINER_IFELSEIF, [evaluation.treatment]);
if (!isBoolean(evaluation)) log.debug(ENGINE_COMBINER_IFELSEIF, [evaluation.treatment]);

return evaluation;
}
Expand All @@ -35,18 +33,18 @@ export function ifElseIfCombinerContext(log: ILogger, predicates: IEvaluator[]):
return undefined;
}

function ifElseIfCombiner(key: SplitIO.SplitKey, seed?: number, trafficAllocation?: number, trafficAllocationSeed?: number, attributes?: SplitIO.Attributes, splitEvaluator?: ISplitEvaluator) {
function ifElseIfCombiner(key: SplitIO.SplitKeyObject, seed?: number, trafficAllocation?: number, trafficAllocationSeed?: number, attributes?: SplitIO.Attributes, splitEvaluator?: ISplitEvaluator) {
// In Async environments we are going to have async predicates. There is none way to know
// before hand so we need to evaluate all the predicates, verify for thenables, and finally,
// define how to return the treatment (wrap result into a Promise or not).
const predicateResults = predicates.map(evaluator => evaluator(key, seed, trafficAllocation, trafficAllocationSeed, attributes, splitEvaluator));

// if we find a thenable
if (findIndex(predicateResults, thenable) !== -1) {
return Promise.all(predicateResults).then(results => computeTreatment(results));
return Promise.all(predicateResults).then(results => computeEvaluation(results));
}

return computeTreatment(predicateResults as IEvaluation[]);
return computeEvaluation(predicateResults as IEvaluation[]);
}

// if there is none predicates, then there was an error in parsing phase
Expand Down
24 changes: 12 additions & 12 deletions src/evaluator/condition/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -7,27 +7,27 @@ import SplitIO from '../../../types/splitio';
import { ILogger } from '../../logger/types';

// Build Evaluation object if and only if matchingResult is true
function match(log: ILogger, matchingResult: boolean, bucketingKey: string | undefined, seed: number | undefined, treatments: { getTreatmentFor: (x: number) => string }, label: string): IEvaluation | undefined {
function match(log: ILogger, matchingResult: boolean, bucketingKey: string | undefined, seed?: number, treatments?: { getTreatmentFor: (x: number) => string }, label?: string): IEvaluation | boolean | undefined {
if (matchingResult) {
const treatment = getTreatment(log, bucketingKey as string, seed, treatments);

return {
treatment,
label
};
return treatments ? // Feature flag
{
treatment: getTreatment(log, bucketingKey as string, seed, treatments),
label: label!
} : // Rule-based segment
true;
}

// else we should notify the engine to continue evaluating
return undefined;
}

// Condition factory
export function conditionContext(log: ILogger, matcherEvaluator: (...args: any) => MaybeThenable<boolean>, treatments: { getTreatmentFor: (x: number) => string }, label: string, conditionType: 'ROLLOUT' | 'WHITELIST'): IEvaluator {
export function conditionContext(log: ILogger, matcherEvaluator: (key: SplitIO.SplitKeyObject, attributes?: SplitIO.Attributes, splitEvaluator?: ISplitEvaluator) => MaybeThenable<boolean>, treatments?: { getTreatmentFor: (x: number) => string }, label?: string, conditionType?: 'ROLLOUT' | 'WHITELIST'): IEvaluator {

return function conditionEvaluator(key: SplitIO.SplitKey, seed?: number, trafficAllocation?: number, trafficAllocationSeed?: number, attributes?: SplitIO.Attributes, splitEvaluator?: ISplitEvaluator) {
return function conditionEvaluator(key: SplitIO.SplitKeyObject, seed?: number, trafficAllocation?: number, trafficAllocationSeed?: number, attributes?: SplitIO.Attributes, splitEvaluator?: ISplitEvaluator) {

// Whitelisting has more priority than traffic allocation, so we don't apply this filtering to those conditions.
if (conditionType === 'ROLLOUT' && !shouldApplyRollout(trafficAllocation as number, (key as SplitIO.SplitKeyObject).bucketingKey as string, trafficAllocationSeed as number)) {
if (conditionType === 'ROLLOUT' && !shouldApplyRollout(trafficAllocation!, key.bucketingKey, trafficAllocationSeed!)) {
return {
treatment: undefined, // treatment value is assigned later
label: NOT_IN_SPLIT
Expand All @@ -41,10 +41,10 @@ export function conditionContext(log: ILogger, matcherEvaluator: (...args: any)
const matches = matcherEvaluator(key, attributes, splitEvaluator);

if (thenable(matches)) {
return matches.then(result => match(log, result, (key as SplitIO.SplitKeyObject).bucketingKey, seed, treatments, label));
return matches.then(result => match(log, result, key.bucketingKey, seed, treatments, label));
}

return match(log, matches, (key as SplitIO.SplitKeyObject).bucketingKey, seed, treatments, label);
return match(log, matches, key.bucketingKey, seed, treatments, label);
};

}
4 changes: 2 additions & 2 deletions src/evaluator/matchers/__tests__/between.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,6 @@ test('MATCHER BETWEEN / should return true ONLY when the value is between 10 and
expect(matcher(15)).toBe(true); // 15 is between 10 and 20
expect(matcher(20)).toBe(true); // 20 is between 10 and 20
expect(matcher(21)).toBe(false); // 21 is not between 10 and 20
expect(matcher(undefined)).toBe(false); // undefined is not between 10 and 20
expect(matcher(null)).toBe(false); // null is not between 10 and 20
expect(matcher(undefined as any)).toBe(false); // undefined is not between 10 and 20
expect(matcher(null as any)).toBe(false); // null is not between 10 and 20
});
8 changes: 4 additions & 4 deletions src/evaluator/matchers/__tests__/segment/client_side.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -29,8 +29,8 @@ test('MATCHER IN_SEGMENT / should return true ONLY when the segment is defined i
}
} as IStorageSync) as IMatcher;

expect(await matcherTrue()).toBe(true); // segment found in mySegments list
expect(await matcherFalse()).toBe(false); // segment not found in mySegments list
expect(await matcherTrue('key')).toBe(true); // segment found in mySegments list
expect(await matcherFalse('key')).toBe(false); // segment not found in mySegments list
});

test('MATCHER IN_LARGE_SEGMENT / should return true ONLY when the segment is defined inside the segment storage', async function () {
Expand All @@ -54,6 +54,6 @@ test('MATCHER IN_LARGE_SEGMENT / should return true ONLY when the segment is def
largeSegments: undefined
} as IStorageSync) as IMatcher;

expect(await matcherTrue()).toBe(true); // large segment found in mySegments list
expect(await matcherFalse()).toBe(false); // large segment storage is not defined
expect(await matcherTrue('key')).toBe(true); // large segment found in mySegments list
expect(await matcherFalse('key')).toBe(false); // large segment storage is not defined
});
2 changes: 1 addition & 1 deletion src/evaluator/matchers/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -64,5 +64,5 @@ export function matcherFactory(log: ILogger, matcherDto: IMatcherDto, storage?:
let matcherFn;
// @ts-ignore
if (matchers[type]) matcherFn = matchers[type](value, storage, log); // There is no index-out-of-bound exception in JavaScript
return matcherFn;
return matcherFn as IMatcher;
}
6 changes: 3 additions & 3 deletions src/evaluator/parser/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -37,7 +37,7 @@ export function parser(log: ILogger, conditions: ISplitCondition[], storage: ISt
}

// Evaluator function.
return (key: string, attributes: SplitIO.Attributes | undefined, splitEvaluator: ISplitEvaluator) => {
return (key: SplitIO.SplitKey, attributes?: SplitIO.Attributes, splitEvaluator?: ISplitEvaluator) => {
const value = sanitizeValue(log, key, matcherDto, attributes);
let result: MaybeThenable<boolean> = false;

Expand Down Expand Up @@ -71,12 +71,12 @@ export function parser(log: ILogger, conditions: ISplitCondition[], storage: ISt
predicates.push(conditionContext(
log,
andCombinerContext(log, expressions),
Treatments.parse(partitions),
partitions && Treatments.parse(partitions),
label,
conditionType
));
}

// Instanciate evaluator given the set of conditions using if else if logic
// Instantiate evaluator given the set of conditions using if else if logic
return ifElseIfCombinerContext(log, predicates);
}
4 changes: 2 additions & 2 deletions src/evaluator/types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -29,6 +29,6 @@ export type IEvaluationResult = IEvaluation & { treatment: string; impressionsDi

export type ISplitEvaluator = (log: ILogger, key: SplitIO.SplitKey, splitName: string, attributes: SplitIO.Attributes | undefined, storage: IStorageSync | IStorageAsync) => MaybeThenable<IEvaluation>

export type IEvaluator = (key: SplitIO.SplitKey, seed?: number, trafficAllocation?: number, trafficAllocationSeed?: number, attributes?: SplitIO.Attributes, splitEvaluator?: ISplitEvaluator) => MaybeThenable<IEvaluation | undefined>
export type IEvaluator = (key: SplitIO.SplitKeyObject, seed?: number, trafficAllocation?: number, trafficAllocationSeed?: number, attributes?: SplitIO.Attributes, splitEvaluator?: ISplitEvaluator) => MaybeThenable<IEvaluation | boolean | undefined>

export type IMatcher = (...args: any) => MaybeThenable<boolean>
export type IMatcher = (value: string | number | boolean | string[] | IDependencyMatcherValue, splitEvaluator?: ISplitEvaluator) => MaybeThenable<boolean>
4 changes: 2 additions & 2 deletions src/evaluator/value/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@ import { ILogger } from '../../logger/types';
import { sanitize } from './sanitize';
import { ENGINE_VALUE, ENGINE_VALUE_NO_ATTRIBUTES, ENGINE_VALUE_INVALID } from '../../logger/constants';

function parseValue(log: ILogger, key: string, attributeName: string | null, attributes?: SplitIO.Attributes) {
function parseValue(log: ILogger, key: SplitIO.SplitKey, attributeName: string | null, attributes?: SplitIO.Attributes) {
let value = undefined;
if (attributeName) {
if (attributes) {
Expand All @@ -23,7 +23,7 @@ function parseValue(log: ILogger, key: string, attributeName: string | null, att
/**
* Defines value to be matched (key / attribute).
*/
export function sanitizeValue(log: ILogger, key: string, matcherDto: IMatcherDto, attributes?: SplitIO.Attributes) {
export function sanitizeValue(log: ILogger, key: SplitIO.SplitKey, matcherDto: IMatcherDto, attributes?: SplitIO.Attributes) {
const attributeName = matcherDto.attribute;
const valueToMatch = parseValue(log, key, attributeName, attributes);
const sanitizedValue = sanitize(log, matcherDto.type, valueToMatch, matcherDto.dataType, attributes);
Expand Down
6 changes: 3 additions & 3 deletions src/evaluator/value/sanitize.ts
Original file line number Diff line number Diff line change
Expand Up @@ -69,9 +69,9 @@ function getProcessingFunction(matcherTypeID: number, dataType: string) {
/**
* Sanitize matcher value
*/
export function sanitize(log: ILogger, matcherTypeID: number, value: string | number | boolean | Array<string | number> | undefined, dataType: string, attributes?: SplitIO.Attributes) {
export function sanitize(log: ILogger, matcherTypeID: number, value: string | number | boolean | Array<string | number> | SplitIO.SplitKey | undefined, dataType: string, attributes?: SplitIO.Attributes) {
const processor = getProcessingFunction(matcherTypeID, dataType);
let sanitizedValue: string | number | boolean | Array<string | number> | IDependencyMatcherValue | undefined;
let sanitizedValue: string | number | boolean | Array<string> | IDependencyMatcherValue | undefined;

switch (dataType) {
case matcherDataTypes.NUMBER:
Expand All @@ -88,7 +88,7 @@ export function sanitize(log: ILogger, matcherTypeID: number, value: string | nu
sanitizedValue = sanitizeBoolean(value);
break;
case matcherDataTypes.NOT_SPECIFIED:
sanitizedValue = value;
sanitizedValue = value as any;
break;
default:
sanitizedValue = undefined;
Expand Down
2 changes: 1 addition & 1 deletion src/sdkManager/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -17,7 +17,7 @@ function collectTreatments(splitObject: ISplit) {
// Localstorage mode could fall into a no rollout conditions state. Take the first condition in that case.
if (!allTreatmentsCondition) allTreatmentsCondition = conditions[0];
// Then extract the treatments from the partitions
return allTreatmentsCondition ? allTreatmentsCondition.partitions.map(v => v.treatment) : [];
return allTreatmentsCondition ? allTreatmentsCondition.partitions!.map(v => v.treatment) : [];
}

function objectToView(splitObject: ISplit | null): SplitIO.SplitView | null {
Expand Down
2 changes: 1 addition & 1 deletion src/utils/lang/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -111,7 +111,7 @@ export function groupBy<T extends Record<string, any>>(source: T[], prop: string
/**
* Checks if a given value is a boolean.
*/
export function isBoolean(val: any): boolean {
export function isBoolean(val: any): val is boolean {
return val === true || val === false;
}

Expand Down