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
110 changes: 51 additions & 59 deletions src/dtos/types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,8 +2,6 @@ import SplitIO from '../../types/splitio';

export type MaybeThenable<T> = T | Promise<T>

/** Split Matchers */

export type IMatcherDataType = null | 'DATETIME' | 'NUMBER'

export interface IUnaryNumericMatcherData {
Expand Down Expand Up @@ -39,7 +37,7 @@ export interface IDependencyMatcherData {
treatments: string[]
}

interface ISplitMatcherBase {
interface IDefinitionMatcherBase {
matcherType: string
negate?: boolean
keySelector?: null | {
Expand All @@ -57,144 +55,143 @@ interface ISplitMatcherBase {
betweenStringMatcherData?: null | IBetweenStringMatcherData
}

interface IAllKeysMatcher extends ISplitMatcherBase {
interface IAllKeysMatcher extends IDefinitionMatcherBase {
matcherType: 'ALL_KEYS'
}

interface IInSegmentMatcher extends ISplitMatcherBase {
interface IInSegmentMatcher extends IDefinitionMatcherBase {
matcherType: 'IN_SEGMENT',
userDefinedSegmentMatcherData: IInSegmentMatcherData
}

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

interface IInLargeSegmentMatcher extends ISplitMatcherBase {
interface IInLargeSegmentMatcher extends IDefinitionMatcherBase {
matcherType: 'IN_LARGE_SEGMENT',
userDefinedLargeSegmentMatcherData: IInLargeSegmentMatcherData
}

interface IWhitelistMatcher extends ISplitMatcherBase {
interface IWhitelistMatcher extends IDefinitionMatcherBase {
matcherType: 'WHITELIST',
whitelistMatcherData: IWhitelistMatcherData
}

interface IEqualToMatcher extends ISplitMatcherBase {
interface IEqualToMatcher extends IDefinitionMatcherBase {
matcherType: 'EQUAL_TO',
unaryNumericMatcherData: IUnaryNumericMatcherData
}

interface IGreaterThanOrEqualToMatcher extends ISplitMatcherBase {
interface IGreaterThanOrEqualToMatcher extends IDefinitionMatcherBase {
matcherType: 'GREATER_THAN_OR_EQUAL_TO',
unaryNumericMatcherData: IUnaryNumericMatcherData
}

interface ILessThanOrEqualToMatcher extends ISplitMatcherBase {
interface ILessThanOrEqualToMatcher extends IDefinitionMatcherBase {
matcherType: 'LESS_THAN_OR_EQUAL_TO',
unaryNumericMatcherData: IUnaryNumericMatcherData
}

interface IBetweenMatcher extends ISplitMatcherBase {
interface IBetweenMatcher extends IDefinitionMatcherBase {
matcherType: 'BETWEEN'
betweenMatcherData: IBetweenMatcherData
}

interface IEqualToSetMatcher extends ISplitMatcherBase {
interface IEqualToSetMatcher extends IDefinitionMatcherBase {
matcherType: 'EQUAL_TO_SET',
whitelistMatcherData: IWhitelistMatcherData
}

interface IContainsAnyOfSetMatcher extends ISplitMatcherBase {
interface IContainsAnyOfSetMatcher extends IDefinitionMatcherBase {
matcherType: 'CONTAINS_ANY_OF_SET',
whitelistMatcherData: IWhitelistMatcherData
}

interface IContainsAllOfSetMatcher extends ISplitMatcherBase {
interface IContainsAllOfSetMatcher extends IDefinitionMatcherBase {
matcherType: 'CONTAINS_ALL_OF_SET',
whitelistMatcherData: IWhitelistMatcherData
}

interface IPartOfSetMatcher extends ISplitMatcherBase {
interface IPartOfSetMatcher extends IDefinitionMatcherBase {
matcherType: 'PART_OF_SET',
whitelistMatcherData: IWhitelistMatcherData
}

interface IStartsWithMatcher extends ISplitMatcherBase {
interface IStartsWithMatcher extends IDefinitionMatcherBase {
matcherType: 'STARTS_WITH',
whitelistMatcherData: IWhitelistMatcherData
}

interface IEndsWithMatcher extends ISplitMatcherBase {
interface IEndsWithMatcher extends IDefinitionMatcherBase {
matcherType: 'ENDS_WITH',
whitelistMatcherData: IWhitelistMatcherData
}

interface IContainsStringMatcher extends ISplitMatcherBase {
interface IContainsStringMatcher extends IDefinitionMatcherBase {
matcherType: 'CONTAINS_STRING',
whitelistMatcherData: IWhitelistMatcherData
}

interface IInSplitTreatmentMatcher extends ISplitMatcherBase {
interface IInSplitTreatmentMatcher extends IDefinitionMatcherBase {
matcherType: 'IN_SPLIT_TREATMENT',
dependencyMatcherData: IDependencyMatcherData,
}

interface IEqualToBooleanMatcher extends ISplitMatcherBase {
interface IEqualToBooleanMatcher extends IDefinitionMatcherBase {
matcherType: 'EQUAL_TO_BOOLEAN',
booleanMatcherData: boolean
}

interface IMatchesStringMatcher extends ISplitMatcherBase {
interface IMatchesStringMatcher extends IDefinitionMatcherBase {
matcherType: 'MATCHES_STRING',
stringMatcherData: string
}

interface IEqualToSemverMatcher extends ISplitMatcherBase {
interface IEqualToSemverMatcher extends IDefinitionMatcherBase {
matcherType: 'EQUAL_TO_SEMVER',
stringMatcherData: string
}

interface IGreaterThanOrEqualToSemverMatcher extends ISplitMatcherBase {
interface IGreaterThanOrEqualToSemverMatcher extends IDefinitionMatcherBase {
matcherType: 'GREATER_THAN_OR_EQUAL_TO_SEMVER',
stringMatcherData: string
}


interface ILessThanOrEqualToSemverMatcher extends ISplitMatcherBase {
interface ILessThanOrEqualToSemverMatcher extends IDefinitionMatcherBase {
matcherType: 'LESS_THAN_OR_EQUAL_TO_SEMVER',
stringMatcherData: string
}

interface IBetweenSemverMatcher extends ISplitMatcherBase {
interface IBetweenSemverMatcher extends IDefinitionMatcherBase {
matcherType: 'BETWEEN_SEMVER'
betweenStringMatcherData: IBetweenStringMatcherData
}

interface IInListSemverMatcher extends ISplitMatcherBase {
interface IInListSemverMatcher extends IDefinitionMatcherBase {
matcherType: 'IN_LIST_SEMVER',
whitelistMatcherData: IWhitelistMatcherData
}

export type ISplitMatcher = IAllKeysMatcher | IInSegmentMatcher | IWhitelistMatcher | IEqualToMatcher | IGreaterThanOrEqualToMatcher |
export type IDefinitionMatcher = IAllKeysMatcher | IInSegmentMatcher | IWhitelistMatcher | IEqualToMatcher | IGreaterThanOrEqualToMatcher |
ILessThanOrEqualToMatcher | IBetweenMatcher | IEqualToSetMatcher | IContainsAnyOfSetMatcher | IContainsAllOfSetMatcher | IPartOfSetMatcher |
IStartsWithMatcher | IEndsWithMatcher | IContainsStringMatcher | IInSplitTreatmentMatcher | IEqualToBooleanMatcher | IMatchesStringMatcher |
IEqualToSemverMatcher | IGreaterThanOrEqualToSemverMatcher | ILessThanOrEqualToSemverMatcher | IBetweenSemverMatcher | IInListSemverMatcher |
IInLargeSegmentMatcher | IInRBSegmentMatcher

/** Split object */
export interface ISplitPartition {
export interface IDefinitionPartition {
treatment: string
size: number
}

export interface ISplitCondition {
export interface IDefinitionCondition {
matcherGroup: {
combiner: 'AND',
matchers: ISplitMatcher[]
matchers: IDefinitionMatcher[]
}
partitions?: ISplitPartition[]
partitions?: IDefinitionPartition[]
label?: string
conditionType?: 'ROLLOUT' | 'WHITELIST'
}
Expand All @@ -204,49 +201,44 @@ export interface IExcludedSegment {
name: string,
}

export interface IRBSegment {
name: string,
changeNumber: number,
status?: 'ACTIVE' | 'ARCHIVED',
conditions?: ISplitCondition[] | null,
export interface TargetingEntity {
name: string;
changeNumber: number;
status: 'ACTIVE' | 'ARCHIVED';
conditions: IDefinitionCondition[];
}

export interface IRBSegment extends TargetingEntity {
excluded?: {
keys?: string[] | null,
segments?: IExcludedSegment[] | null
} | null
}

// @TODO: rename to IDefinition (Configs and Feature Flags are definitions)
export interface ISplit {
name: string,
changeNumber: number,
status?: 'ACTIVE' | 'ARCHIVED',
conditions: ISplitCondition[],
export interface IDefinition extends TargetingEntity {
trafficTypeName: string;
sets?: string[];
impressionsDisabled?: boolean;
prerequisites?: null | {
n: string,
ts: string[]
}[]
killed: boolean,
defaultTreatment: string,
trafficTypeName: string,
seed: number,
trafficAllocation?: number,
trafficAllocationSeed?: number
}[];
killed: boolean;
defaultTreatment: string;
seed: number;
trafficAllocation?: number;
trafficAllocationSeed?: number;
configurations?: {
[treatmentName: string]: string | SplitIO.JsonObject
},
sets?: string[],
impressionsDisabled?: boolean
};
}

// Split definition used in offline mode
export type ISplitPartial = Pick<ISplit, 'conditions' | 'configurations' | 'trafficTypeName'>

/** Interface of the parsed JSON response of `/splitChanges` */
export interface ISplitChangesResponse {
export interface IDefinitionChangesResponse {
ff?: {
t: number,
s?: number,
d: ISplit[]
d: IDefinition[]
},
rbs?: {
t: number,
Expand Down
8 changes: 4 additions & 4 deletions src/evaluator/Engine.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,10 +4,10 @@ import { keyParser } from '../utils/key';
import { thenable } from '../utils/promise/thenable';
import { NO_CONDITION_MATCH, SPLIT_ARCHIVED, SPLIT_KILLED, PREREQUISITES_NOT_MET } from '../utils/labels';
import { CONTROL } from '../utils/constants';
import { ISplit, MaybeThenable } from '../dtos/types';
import { IDefinition, MaybeThenable } from '../dtos/types';
import SplitIO from '../../types/splitio';
import { IStorageAsync, IStorageSync } from '../storages/types';
import { IEvaluation, IEvaluationResult, ISplitEvaluator } from './types';
import { IEvaluation, IEvaluationResult, IDefinitionEvaluator } from './types';
import { ILogger } from '../logger/types';
import { ENGINE_DEFAULT } from '../logger/constants';
import { prerequisitesMatcherContext } from './matchers/prerequisites';
Expand All @@ -19,7 +19,7 @@ function evaluationResult(result: IEvaluation | undefined, defaultTreatment: str
};
}

export function engineParser(log: ILogger, split: ISplit, storage: IStorageSync | IStorageAsync) {
export function engineParser(log: ILogger, split: IDefinition, storage: IStorageSync | IStorageAsync) {
const { killed, seed, trafficAllocation, trafficAllocationSeed, status, conditions, prerequisites } = split;

const defaultTreatment = isString(split.defaultTreatment) ? split.defaultTreatment : CONTROL;
Expand All @@ -29,7 +29,7 @@ export function engineParser(log: ILogger, split: ISplit, storage: IStorageSync

return {

getTreatment(key: SplitIO.SplitKey, attributes: SplitIO.Attributes | undefined, splitEvaluator: ISplitEvaluator): MaybeThenable<IEvaluationResult> {
getTreatment(key: SplitIO.SplitKey, attributes: SplitIO.Attributes | undefined, splitEvaluator: IDefinitionEvaluator): MaybeThenable<IEvaluationResult> {

const parsedKey = keyParser(key);

Expand Down
8 changes: 4 additions & 4 deletions src/evaluator/__tests__/evaluate-feature.spec.ts
Original file line number Diff line number Diff line change
@@ -1,10 +1,10 @@
import { evaluateFeature } from '../index';
import { EXCEPTION, NOT_IN_SPLIT, SPLIT_ARCHIVED, SPLIT_KILLED, SPLIT_NOT_FOUND } from '../../utils/labels';
import { EXCEPTION, NOT_IN_SPLIT, SPLIT_ARCHIVED, SPLIT_KILLED, DEFINITION_NOT_FOUND } from '../../utils/labels';
import { loggerMock } from '../../logger/__tests__/sdkLogger.mock';
import { ISplit } from '../../dtos/types';
import { IDefinition } from '../../dtos/types';
import { IStorageSync } from '../../storages/types';

const splitsMock: Record<string, ISplit> = {
const splitsMock: Record<string, IDefinition> = {
regular: { 'changeNumber': 1487277320548, 'trafficAllocationSeed': 1667452163, 'trafficAllocation': 100, 'trafficTypeName': 'user', 'name': 'always-on', 'seed': 1684183541, 'configurations': {}, 'status': 'ACTIVE', 'killed': false, 'defaultTreatment': 'off', 'conditions': [{ 'conditionType': 'ROLLOUT', 'matcherGroup': { 'combiner': 'AND', 'matchers': [{ 'keySelector': { 'trafficType': 'user', 'attribute': '' }, 'matcherType': 'ALL_KEYS', 'negate': false, 'userDefinedSegmentMatcherData': { 'segmentName': '' }, 'unaryNumericMatcherData': { 'dataType': null, 'value': 0 }, 'whitelistMatcherData': { 'whitelist': null }, 'betweenMatcherData': { 'dataType': null, 'start': 0, 'end': 0 } }] }, 'partitions': [{ 'treatment': 'on', 'size': 100 }, { 'treatment': 'off', 'size': 0 }], 'label': 'in segment all' }] },
config: { 'changeNumber': 1487277320548, 'trafficAllocationSeed': 1667452163, 'trafficAllocation': 100, 'trafficTypeName': 'user', 'name': 'always-on', 'seed': 1684183541, 'configurations': { 'on': "{color:'black'}" }, 'status': 'ACTIVE', 'killed': false, 'defaultTreatment': 'off', 'conditions': [{ 'conditionType': 'ROLLOUT', 'matcherGroup': { 'combiner': 'AND', 'matchers': [{ 'keySelector': { 'trafficType': 'user', 'attribute': '' }, 'matcherType': 'ALL_KEYS', 'negate': false, 'userDefinedSegmentMatcherData': { 'segmentName': '' }, 'unaryNumericMatcherData': { 'dataType': null, 'value': 0 }, 'whitelistMatcherData': { 'whitelist': null }, 'betweenMatcherData': { 'dataType': null, 'start': 0, 'end': 0 } }] }, 'partitions': [{ 'treatment': 'on', 'size': 100 }, { 'treatment': 'off', 'size': 0 }], 'label': 'in segment all' }] },
killed: { 'changeNumber': 1487277320548, 'trafficAllocationSeed': 1667452163, 'trafficAllocation': 100, 'trafficTypeName': 'user', 'name': 'always-on2', 'seed': 1684183541, 'configurations': {}, 'status': 'ACTIVE', 'killed': true, 'defaultTreatment': 'off', 'conditions': [{ 'conditionType': 'ROLLOUT', 'matcherGroup': { 'combiner': 'AND', 'matchers': [{ 'keySelector': { 'trafficType': 'user', 'attribute': '' }, 'matcherType': 'ALL_KEYS', 'negate': false, 'userDefinedSegmentMatcherData': { 'segmentName': '' }, 'unaryNumericMatcherData': { 'dataType': null, 'value': 0 }, 'whitelistMatcherData': { 'whitelist': null }, 'betweenMatcherData': { 'dataType': null, 'start': 0, 'end': 0 } }] }, 'partitions': [{ 'treatment': 'on', 'size': 100 }, { 'treatment': 'off', 'size': 0 }], 'label': 'in segment all' }] },
Expand Down Expand Up @@ -53,7 +53,7 @@ test('EVALUATOR / should return right label, treatment and config if storage ret
config: '{color:\'black\'}', changeNumber: 1487277320548
};
const expectedOutputControl = {
treatment: 'control', label: SPLIT_NOT_FOUND, config: null
treatment: 'control', label: DEFINITION_NOT_FOUND, config: null
};

const evaluationWithConfig = evaluateFeature(
Expand Down
12 changes: 6 additions & 6 deletions src/evaluator/__tests__/evaluate-features.spec.ts
Original file line number Diff line number Diff line change
@@ -1,11 +1,11 @@
import { evaluateFeatures, evaluateFeaturesByFlagSets } from '../index';
import { EXCEPTION, NOT_IN_SPLIT, SPLIT_ARCHIVED, SPLIT_KILLED, SPLIT_NOT_FOUND } from '../../utils/labels';
import { EXCEPTION, NOT_IN_SPLIT, SPLIT_ARCHIVED, SPLIT_KILLED, DEFINITION_NOT_FOUND } from '../../utils/labels';
import { loggerMock } from '../../logger/__tests__/sdkLogger.mock';
import { WARN_FLAGSET_WITHOUT_FLAGS } from '../../logger/constants';
import { ISplit } from '../../dtos/types';
import { IDefinition } from '../../dtos/types';
import { IStorageSync } from '../../storages/types';

const splitsMock: Record<string, ISplit> = {
const splitsMock: Record<string, IDefinition> = {
regular: { 'changeNumber': 1487277320548, 'trafficAllocationSeed': 1667452163, 'trafficAllocation': 100, 'trafficTypeName': 'user', 'name': 'always-on', 'seed': 1684183541, 'configurations': {}, 'status': 'ACTIVE', 'killed': false, 'defaultTreatment': 'off', 'conditions': [{ 'conditionType': 'ROLLOUT', 'matcherGroup': { 'combiner': 'AND', 'matchers': [{ 'keySelector': { 'trafficType': 'user', 'attribute': '' }, 'matcherType': 'ALL_KEYS', 'negate': false, 'userDefinedSegmentMatcherData': { 'segmentName': '' }, 'unaryNumericMatcherData': { 'dataType': null, 'value': 0 }, 'whitelistMatcherData': { 'whitelist': null }, 'betweenMatcherData': { 'dataType': null, 'start': 0, 'end': 0 } }] }, 'partitions': [{ 'treatment': 'on', 'size': 100 }, { 'treatment': 'off', 'size': 0 }], 'label': 'in segment all' }] },
config: { 'changeNumber': 1487277320548, 'trafficAllocationSeed': 1667452163, 'trafficAllocation': 100, 'trafficTypeName': 'user', 'name': 'always-on', 'seed': 1684183541, 'configurations': { 'on': "{color:'black'}" }, 'status': 'ACTIVE', 'killed': false, 'defaultTreatment': 'off', 'conditions': [{ 'conditionType': 'ROLLOUT', 'matcherGroup': { 'combiner': 'AND', 'matchers': [{ 'keySelector': { 'trafficType': 'user', 'attribute': '' }, 'matcherType': 'ALL_KEYS', 'negate': false, 'userDefinedSegmentMatcherData': { 'segmentName': '' }, 'unaryNumericMatcherData': { 'dataType': null, 'value': 0 }, 'whitelistMatcherData': { 'whitelist': null }, 'betweenMatcherData': { 'dataType': null, 'start': 0, 'end': 0 } }] }, 'partitions': [{ 'treatment': 'on', 'size': 100 }, { 'treatment': 'off', 'size': 0 }], 'label': 'in segment all' }] },
killed: { 'changeNumber': 1487277320548, 'trafficAllocationSeed': 1667452163, 'trafficAllocation': 100, 'trafficTypeName': 'user', 'name': 'always-on2', 'seed': 1684183541, 'configurations': {}, 'status': 'ACTIVE', 'killed': true, 'defaultTreatment': 'off', 'conditions': [{ 'conditionType': 'ROLLOUT', 'matcherGroup': { 'combiner': 'AND', 'matchers': [{ 'keySelector': { 'trafficType': 'user', 'attribute': '' }, 'matcherType': 'ALL_KEYS', 'negate': false, 'userDefinedSegmentMatcherData': { 'segmentName': '' }, 'unaryNumericMatcherData': { 'dataType': null, 'value': 0 }, 'whitelistMatcherData': { 'whitelist': null }, 'betweenMatcherData': { 'dataType': null, 'start': 0, 'end': 0 } }] }, 'partitions': [{ 'treatment': 'on', 'size': 100 }, { 'treatment': 'off', 'size': 0 }], 'label': 'in segment all' }] },
Expand Down Expand Up @@ -33,7 +33,7 @@ const mockStorage = {
return names.reduce((acc, name) => {
acc[name] = this.getSplit(name);
return acc;
}, {} as Record<string, ISplit | null>);
}, {} as Record<string, IDefinition | null>);
},
getNamesByFlagSets(flagSets: string[]) {
return flagSets.map(flagset => flagSetsMock[flagset] || new Set());
Expand Down Expand Up @@ -71,7 +71,7 @@ test('EVALUATOR - Multiple evaluations at once / should return right labels, tre
config: '{color:\'black\'}', changeNumber: 1487277320548
},
not_existent_split: {
treatment: 'control', label: SPLIT_NOT_FOUND, config: null
treatment: 'control', label: DEFINITION_NOT_FOUND, config: null
},
};

Expand Down Expand Up @@ -122,7 +122,7 @@ describe('EVALUATOR - Multiple evaluations at once by flag sets', () => {
config: '{color:\'black\'}', changeNumber: 1487277320548
},
not_existent_split: {
treatment: 'control', label: SPLIT_NOT_FOUND, config: null
treatment: 'control', label: DEFINITION_NOT_FOUND, config: null
},
};

Expand Down
6 changes: 3 additions & 3 deletions src/evaluator/combiners/and.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,11 +2,11 @@ import { findIndex } from '../../utils/lang';
import { ILogger } from '../../logger/types';
import { thenable } from '../../utils/promise/thenable';
import { MaybeThenable } from '../../dtos/types';
import { ISplitEvaluator } from '../types';
import { IDefinitionEvaluator } from '../types';
import { ENGINE_COMBINER_AND } from '../../logger/constants';
import SplitIO from '../../../types/splitio';

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

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

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

// If any matching result is a thenable we should use Promise.all
Expand Down
Loading