From 6606b667cd5ab288a246015b7e02c1c87ae2701a Mon Sep 17 00:00:00 2001 From: Emiliano Sanchez Date: Tue, 11 Mar 2025 10:11:39 -0300 Subject: [PATCH 01/11] Refactor Engine class into engineParser function for improved clarity and code reduction --- src/evaluator/Engine.ts | 93 ++++++++++------------------------------- src/evaluator/index.ts | 12 +++--- 2 files changed, 27 insertions(+), 78 deletions(-) diff --git a/src/evaluator/Engine.ts b/src/evaluator/Engine.ts index 233ecc2a..a1465a51 100644 --- a/src/evaluator/Engine.ts +++ b/src/evaluator/Engine.ts @@ -1,13 +1,13 @@ -import { get } from '../utils/lang'; +import { get, isString } from '../utils/lang'; import { parser } from './parser'; import { keyParser } from '../utils/key'; import { thenable } from '../utils/promise/thenable'; -import { EXCEPTION, NO_CONDITION_MATCH, SPLIT_ARCHIVED, SPLIT_KILLED } from '../utils/labels'; +import { NO_CONDITION_MATCH, SPLIT_ARCHIVED, SPLIT_KILLED } from '../utils/labels'; import { CONTROL } from '../utils/constants'; import { ISplit, MaybeThenable } from '../dtos/types'; import SplitIO from '../../types/splitio'; import { IStorageAsync, IStorageSync } from '../storages/types'; -import { IEvaluation, IEvaluationResult, IEvaluator, ISplitEvaluator } from './types'; +import { IEvaluation, IEvaluationResult, ISplitEvaluator } from './types'; import { ILogger } from '../logger/types'; function evaluationResult(result: IEvaluation | undefined, defaultTreatment: string): IEvaluationResult { @@ -17,84 +17,35 @@ function evaluationResult(result: IEvaluation | undefined, defaultTreatment: str }; } -export class Engine { +export function engineParser(log: ILogger, split: ISplit, storage: IStorageSync | IStorageAsync) { + const { killed, seed, trafficAllocation, trafficAllocationSeed, status, conditions } = split; - constructor(private baseInfo: ISplit, private evaluator: IEvaluator) { + const defaultTreatment = isString(split.defaultTreatment) ? split.defaultTreatment : CONTROL; - // in case we don't have a default treatment in the instantiation, use 'control' - if (typeof this.baseInfo.defaultTreatment !== 'string') { - this.baseInfo.defaultTreatment = CONTROL; - } - } - - static parse(log: ILogger, splitFlatStructure: ISplit, storage: IStorageSync | IStorageAsync) { - const conditions = splitFlatStructure.conditions; - const evaluator = parser(log, conditions, storage); + const evaluator = parser(log, conditions, storage); - return new Engine(splitFlatStructure, evaluator); - } + return { - getKey() { - return this.baseInfo.name; - } + getTreatment(key: SplitIO.SplitKey, attributes: SplitIO.Attributes | undefined, splitEvaluator: ISplitEvaluator): MaybeThenable { - getTreatment(key: SplitIO.SplitKey, attributes: SplitIO.Attributes | undefined, splitEvaluator: ISplitEvaluator): MaybeThenable { - const { - killed, - seed, - defaultTreatment, - trafficAllocation, - trafficAllocationSeed - } = this.baseInfo; - let parsedKey; - let treatment; - let label; + const parsedKey = keyParser(key); - try { - parsedKey = keyParser(key); - } catch (err) { - return { + if (status === 'ARCHIVED') return { treatment: CONTROL, - label: EXCEPTION + label: SPLIT_ARCHIVED }; - } - - if (this.isGarbage()) { - treatment = CONTROL; - label = SPLIT_ARCHIVED; - } else if (killed) { - treatment = defaultTreatment; - label = SPLIT_KILLED; - } else { - const evaluation = this.evaluator( - parsedKey, - seed, - trafficAllocation, - trafficAllocationSeed, - attributes, - splitEvaluator - ) as MaybeThenable; - // Evaluation could be async, so we should handle that case checking for a - // thenable object - if (thenable(evaluation)) { - return evaluation.then(result => evaluationResult(result, defaultTreatment)); - } else { - return evaluationResult(evaluation, defaultTreatment); - } - } + if (killed) return { + treatment: defaultTreatment, + label: SPLIT_KILLED + }; - return { - treatment, - label - }; - } + const evaluation = evaluator(parsedKey, seed, trafficAllocation, trafficAllocationSeed, attributes, splitEvaluator) as MaybeThenable; - isGarbage() { - return this.baseInfo.status === 'ARCHIVED'; - } + return thenable(evaluation) ? + evaluation.then(result => evaluationResult(result, defaultTreatment)) : + evaluationResult(evaluation, defaultTreatment); + } + }; - getChangeNumber() { - return this.baseInfo.changeNumber; - } } diff --git a/src/evaluator/index.ts b/src/evaluator/index.ts index 7268962a..e465b9bf 100644 --- a/src/evaluator/index.ts +++ b/src/evaluator/index.ts @@ -1,4 +1,4 @@ -import { Engine } from './Engine'; +import { engineParser } from './Engine'; import { thenable } from '../utils/promise/thenable'; import { EXCEPTION, SPLIT_NOT_FOUND } from '../utils/labels'; import { CONTROL } from '../utils/constants'; @@ -99,9 +99,7 @@ export function evaluateFeaturesByFlagSets( ): MaybeThenable> { let storedFlagNames: MaybeThenable[]>; - function evaluate( - featureFlagsByFlagSets: Set[], - ) { + function evaluate(featureFlagsByFlagSets: Set[]) { let featureFlags = new Set(); for (let i = 0; i < flagSets.length; i++) { const featureFlagByFlagSet = featureFlagsByFlagSets[i]; @@ -148,20 +146,20 @@ function getEvaluation( }; if (splitJSON) { - const split = Engine.parse(log, splitJSON, storage); + const split = engineParser(log, splitJSON, storage); evaluation = split.getTreatment(key, attributes, evaluateFeature); // If the storage is async and the evaluated flag uses segments or dependencies, evaluation is thenable if (thenable(evaluation)) { return evaluation.then(result => { - result.changeNumber = split.getChangeNumber(); + result.changeNumber = splitJSON.changeNumber; result.config = splitJSON.configurations && splitJSON.configurations[result.treatment] || null; result.impressionsDisabled = splitJSON.impressionsDisabled; return result; }); } else { - evaluation.changeNumber = split.getChangeNumber(); // Always sync and optional + evaluation.changeNumber = splitJSON.changeNumber; evaluation.config = splitJSON.configurations && splitJSON.configurations[evaluation.treatment] || null; evaluation.impressionsDisabled = splitJSON.impressionsDisabled; } From fc5ecd5bc3768e4b0dcf2a335026ea027aabc36c Mon Sep 17 00:00:00 2001 From: Emiliano Sanchez Date: Tue, 11 Mar 2025 11:10:57 -0300 Subject: [PATCH 02/11] Add prerequisites support to ISplit and enhance engineParser for prerequisite evaluation --- src/dtos/types.ts | 4 ++++ src/evaluator/Engine.ts | 30 +++++++++++++++++++++++++----- 2 files changed, 29 insertions(+), 5 deletions(-) diff --git a/src/dtos/types.ts b/src/dtos/types.ts index 423de3a8..95dd0e6c 100644 --- a/src/dtos/types.ts +++ b/src/dtos/types.ts @@ -215,6 +215,10 @@ export interface ISplit { changeNumber: number, status: 'ACTIVE' | 'ARCHIVED', conditions: ISplitCondition[], + prerequisites?: { + n: string, + ts: string[] + }[] killed: boolean, defaultTreatment: string, trafficTypeName: string, diff --git a/src/evaluator/Engine.ts b/src/evaluator/Engine.ts index a1465a51..68b8d3e0 100644 --- a/src/evaluator/Engine.ts +++ b/src/evaluator/Engine.ts @@ -18,7 +18,7 @@ function evaluationResult(result: IEvaluation | undefined, defaultTreatment: str } export function engineParser(log: ILogger, split: ISplit, storage: IStorageSync | IStorageAsync) { - const { killed, seed, trafficAllocation, trafficAllocationSeed, status, conditions } = split; + const { killed, seed, trafficAllocation, trafficAllocationSeed, status, conditions, prerequisites } = split; const defaultTreatment = isString(split.defaultTreatment) ? split.defaultTreatment : CONTROL; @@ -30,6 +30,19 @@ export function engineParser(log: ILogger, split: ISplit, storage: IStorageSync const parsedKey = keyParser(key); + function evaluate(matchPrerequisites: boolean) { + if (!matchPrerequisites) return { + treatment: defaultTreatment, + label: NO_CONDITION_MATCH + }; + + const evaluation = evaluator(parsedKey, seed, trafficAllocation, trafficAllocationSeed, attributes, splitEvaluator) as MaybeThenable; + + return thenable(evaluation) ? + evaluation.then(result => evaluationResult(result, defaultTreatment)) : + evaluationResult(evaluation, defaultTreatment); + } + if (status === 'ARCHIVED') return { treatment: CONTROL, label: SPLIT_ARCHIVED @@ -40,11 +53,18 @@ export function engineParser(log: ILogger, split: ISplit, storage: IStorageSync label: SPLIT_KILLED }; - const evaluation = evaluator(parsedKey, seed, trafficAllocation, trafficAllocationSeed, attributes, splitEvaluator) as MaybeThenable; + const matchPrerequisites = prerequisites && prerequisites.length ? + prerequisites.map(prerequisite => { + const evaluation = splitEvaluator(log, key, prerequisite.n, attributes, storage); + return thenable(evaluation) ? + evaluation.then(evaluation => prerequisite.ts.indexOf(evaluation.treatment!) === -1) : + prerequisite.ts.indexOf(evaluation.treatment!) === -1; + }) : + true; - return thenable(evaluation) ? - evaluation.then(result => evaluationResult(result, defaultTreatment)) : - evaluationResult(evaluation, defaultTreatment); + return thenable(matchPrerequisites) ? + matchPrerequisites.then(evaluate) : + evaluate(matchPrerequisites as boolean); } }; From d67582dc3b97b65115105fcadc0b0ff5849576eb Mon Sep 17 00:00:00 2001 From: Emiliano Sanchez Date: Tue, 11 Mar 2025 11:15:36 -0300 Subject: [PATCH 03/11] Add debug logging for prerequisite evaluation --- src/evaluator/Engine.ts | 23 +++++++++++++++-------- src/logger/constants.ts | 1 + src/logger/messages/debug.ts | 1 + 3 files changed, 17 insertions(+), 8 deletions(-) diff --git a/src/evaluator/Engine.ts b/src/evaluator/Engine.ts index 68b8d3e0..1238497e 100644 --- a/src/evaluator/Engine.ts +++ b/src/evaluator/Engine.ts @@ -9,6 +9,7 @@ import SplitIO from '../../types/splitio'; import { IStorageAsync, IStorageSync } from '../storages/types'; import { IEvaluation, IEvaluationResult, ISplitEvaluator } from './types'; import { ILogger } from '../logger/types'; +import { ENGINE_DEFAULT } from '../logger/constants'; function evaluationResult(result: IEvaluation | undefined, defaultTreatment: string): IEvaluationResult { return { @@ -31,10 +32,13 @@ export function engineParser(log: ILogger, split: ISplit, storage: IStorageSync const parsedKey = keyParser(key); function evaluate(matchPrerequisites: boolean) { - if (!matchPrerequisites) return { - treatment: defaultTreatment, - label: NO_CONDITION_MATCH - }; + if (!matchPrerequisites) { + log.debug(ENGINE_DEFAULT, ['Prerequisite not met']); + return { + treatment: defaultTreatment, + label: NO_CONDITION_MATCH + }; + } const evaluation = evaluator(parsedKey, seed, trafficAllocation, trafficAllocationSeed, attributes, splitEvaluator) as MaybeThenable; @@ -48,10 +52,13 @@ export function engineParser(log: ILogger, split: ISplit, storage: IStorageSync label: SPLIT_ARCHIVED }; - if (killed) return { - treatment: defaultTreatment, - label: SPLIT_KILLED - }; + if (killed) { + log.debug(ENGINE_DEFAULT, ['Flag is killed']); + return { + treatment: defaultTreatment, + label: SPLIT_KILLED + }; + } const matchPrerequisites = prerequisites && prerequisites.length ? prerequisites.map(prerequisite => { diff --git a/src/logger/constants.ts b/src/logger/constants.ts index a2e53e15..729da6e1 100644 --- a/src/logger/constants.ts +++ b/src/logger/constants.ts @@ -28,6 +28,7 @@ export const SYNC_TASK_EXECUTE = 37; export const SYNC_TASK_STOP = 38; export const SETTINGS_SPLITS_FILTER = 39; export const ENGINE_MATCHER_RESULT = 40; +export const ENGINE_DEFAULT = 41; export const CLIENT_READY_FROM_CACHE = 100; export const CLIENT_READY = 101; diff --git a/src/logger/messages/debug.ts b/src/logger/messages/debug.ts index d9bce2ab..8f8028e3 100644 --- a/src/logger/messages/debug.ts +++ b/src/logger/messages/debug.ts @@ -12,6 +12,7 @@ export const codesDebug: [number, string][] = codesInfo.concat([ [c.ENGINE_VALUE, c.LOG_PREFIX_ENGINE_VALUE + 'Extracted attribute `%s`. %s will be used for matching.'], [c.ENGINE_SANITIZE, c.LOG_PREFIX_ENGINE + ':sanitize: Attempted to sanitize %s which should be of type %s. Sanitized and processed value => %s'], [c.ENGINE_MATCHER_RESULT, c.LOG_PREFIX_ENGINE_MATCHER + '[%s] Result: %s. Rule value: %s. Evaluation value: %s'], + [c.ENGINE_DEFAULT, c.LOG_PREFIX_ENGINE + 'Evaluates to default treatment. %s'], // SDK [c.CLEANUP_REGISTERING, c.LOG_PREFIX_CLEANUP + 'Registering cleanup handler %s'], [c.CLEANUP_DEREGISTERING, c.LOG_PREFIX_CLEANUP + 'Deregistering cleanup handler %s'], From d93091d4f08d16cd66435e2222590135e357cf53 Mon Sep 17 00:00:00 2001 From: Emiliano Sanchez Date: Tue, 11 Mar 2025 11:48:41 -0300 Subject: [PATCH 04/11] New label --- src/evaluator/Engine.ts | 4 ++-- src/utils/labels/index.ts | 1 + 2 files changed, 3 insertions(+), 2 deletions(-) diff --git a/src/evaluator/Engine.ts b/src/evaluator/Engine.ts index 1238497e..7073f3a7 100644 --- a/src/evaluator/Engine.ts +++ b/src/evaluator/Engine.ts @@ -2,7 +2,7 @@ import { get, isString } from '../utils/lang'; import { parser } from './parser'; import { keyParser } from '../utils/key'; import { thenable } from '../utils/promise/thenable'; -import { NO_CONDITION_MATCH, SPLIT_ARCHIVED, SPLIT_KILLED } from '../utils/labels'; +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 SplitIO from '../../types/splitio'; @@ -36,7 +36,7 @@ export function engineParser(log: ILogger, split: ISplit, storage: IStorageSync log.debug(ENGINE_DEFAULT, ['Prerequisite not met']); return { treatment: defaultTreatment, - label: NO_CONDITION_MATCH + label: PREREQUISITES_NOT_MET }; } diff --git a/src/utils/labels/index.ts b/src/utils/labels/index.ts index b5765299..957100d7 100644 --- a/src/utils/labels/index.ts +++ b/src/utils/labels/index.ts @@ -6,3 +6,4 @@ export const EXCEPTION = 'exception'; export const SPLIT_ARCHIVED = 'archived'; export const NOT_IN_SPLIT = 'not in split'; export const UNSUPPORTED_MATCHER_TYPE = 'targeting rule type unsupported by sdk'; +export const PREREQUISITES_NOT_MET = 'prerequisites not met'; From 77d44b6f36608fa296f3eabedb94b21707c3124e Mon Sep 17 00:00:00 2001 From: Emiliano Sanchez Date: Tue, 11 Mar 2025 11:52:21 -0300 Subject: [PATCH 05/11] Add prerequisites field to SplitView --- src/sdkManager/__tests__/mocks/input.json | 14 ++++++++++++-- src/sdkManager/__tests__/mocks/output.json | 8 ++++++-- src/sdkManager/index.ts | 3 ++- types/splitio.d.ts | 6 +++++- 4 files changed, 25 insertions(+), 6 deletions(-) diff --git a/src/sdkManager/__tests__/mocks/input.json b/src/sdkManager/__tests__/mocks/input.json index 7aea3413..2e951c2c 100644 --- a/src/sdkManager/__tests__/mocks/input.json +++ b/src/sdkManager/__tests__/mocks/input.json @@ -41,5 +41,15 @@ "configurations": { "on": "\"color\": \"green\"" }, - "sets": ["set_a"] -} + "sets": [ + "set_a" + ], + "prerequisites": [ + { + "n": "some_flag", + "ts": [ + "on" + ] + } + ] +} \ No newline at end of file diff --git a/src/sdkManager/__tests__/mocks/output.json b/src/sdkManager/__tests__/mocks/output.json index 52a0b636..7a4f72ef 100644 --- a/src/sdkManager/__tests__/mocks/output.json +++ b/src/sdkManager/__tests__/mocks/output.json @@ -9,5 +9,9 @@ }, "sets": ["set_a"], "defaultTreatment": "off", - "impressionsDisabled": false -} + "impressionsDisabled": false, + "prerequisites": [{ + "flagName": "some_flag", + "treatments": ["on"] + }] +} \ No newline at end of file diff --git a/src/sdkManager/index.ts b/src/sdkManager/index.ts index 831771c9..d241b82e 100644 --- a/src/sdkManager/index.ts +++ b/src/sdkManager/index.ts @@ -32,7 +32,8 @@ function objectToView(splitObject: ISplit | null): SplitIO.SplitView | null { configs: splitObject.configurations || {}, sets: splitObject.sets || [], defaultTreatment: splitObject.defaultTreatment, - impressionsDisabled: splitObject.impressionsDisabled === true + impressionsDisabled: splitObject.impressionsDisabled === true, + prerequisites: (splitObject.prerequisites || []).map(p => ({ flagName: p.n, treatments: p.ts })), }; } diff --git a/types/splitio.d.ts b/types/splitio.d.ts index 0cab4b66..4d83238d 100644 --- a/types/splitio.d.ts +++ b/types/splitio.d.ts @@ -842,7 +842,7 @@ declare namespace SplitIO { /** * The list of treatments available for the feature flag. */ - treatments: Array; + treatments: string[]; /** * Current change number of the feature flag. */ @@ -866,6 +866,10 @@ declare namespace SplitIO { * Whether the feature flag has impressions tracking disabled or not. */ impressionsDisabled: boolean; + /** + * Prerequisites for the feature flag. + */ + prerequisites: Array<{ flagName: string, treatments: string[] }>; }; /** * A promise that resolves to a feature flag view or null if the feature flag is not found. From fe893b041f37c9057ae1ad4ab5533d02845adc6e Mon Sep 17 00:00:00 2001 From: Emiliano Sanchez Date: Tue, 11 Mar 2025 13:43:26 -0300 Subject: [PATCH 06/11] Implement prerequisites matcher and refactor engineParser for prerequisite evaluation --- src/evaluator/Engine.ts | 21 ++++++++------------- src/evaluator/matchers/prerequisites.ts | 25 +++++++++++++++++++++++++ 2 files changed, 33 insertions(+), 13 deletions(-) create mode 100644 src/evaluator/matchers/prerequisites.ts diff --git a/src/evaluator/Engine.ts b/src/evaluator/Engine.ts index 7073f3a7..9a6df6f1 100644 --- a/src/evaluator/Engine.ts +++ b/src/evaluator/Engine.ts @@ -10,6 +10,7 @@ import { IStorageAsync, IStorageSync } from '../storages/types'; import { IEvaluation, IEvaluationResult, ISplitEvaluator } from './types'; import { ILogger } from '../logger/types'; import { ENGINE_DEFAULT } from '../logger/constants'; +import { prerequisitesMatcherContext } from './matchers/prerequisites'; function evaluationResult(result: IEvaluation | undefined, defaultTreatment: string): IEvaluationResult { return { @@ -24,6 +25,7 @@ export function engineParser(log: ILogger, split: ISplit, storage: IStorageSync const defaultTreatment = isString(split.defaultTreatment) ? split.defaultTreatment : CONTROL; const evaluator = parser(log, conditions, storage); + const prerequisiteMatcher = prerequisitesMatcherContext(prerequisites, storage, log); return { @@ -31,8 +33,8 @@ export function engineParser(log: ILogger, split: ISplit, storage: IStorageSync const parsedKey = keyParser(key); - function evaluate(matchPrerequisites: boolean) { - if (!matchPrerequisites) { + function evaluate(prerequisitesMet: boolean) { + if (!prerequisitesMet) { log.debug(ENGINE_DEFAULT, ['Prerequisite not met']); return { treatment: defaultTreatment, @@ -60,18 +62,11 @@ export function engineParser(log: ILogger, split: ISplit, storage: IStorageSync }; } - const matchPrerequisites = prerequisites && prerequisites.length ? - prerequisites.map(prerequisite => { - const evaluation = splitEvaluator(log, key, prerequisite.n, attributes, storage); - return thenable(evaluation) ? - evaluation.then(evaluation => prerequisite.ts.indexOf(evaluation.treatment!) === -1) : - prerequisite.ts.indexOf(evaluation.treatment!) === -1; - }) : - true; + const prerequisitesMet = prerequisiteMatcher(key, attributes, splitEvaluator); - return thenable(matchPrerequisites) ? - matchPrerequisites.then(evaluate) : - evaluate(matchPrerequisites as boolean); + return thenable(prerequisitesMet) ? + prerequisitesMet.then(evaluate) : + evaluate(prerequisitesMet); } }; diff --git a/src/evaluator/matchers/prerequisites.ts b/src/evaluator/matchers/prerequisites.ts new file mode 100644 index 00000000..a9b3b031 --- /dev/null +++ b/src/evaluator/matchers/prerequisites.ts @@ -0,0 +1,25 @@ +import { ISplit, MaybeThenable } from '../../dtos/types'; +import { IStorageAsync, IStorageSync } from '../../storages/types'; +import { ILogger } from '../../logger/types'; +import { thenable } from '../../utils/promise/thenable'; +import { ISplitEvaluator } from '../types'; +import SplitIO from '../../../types/splitio'; + +export function prerequisitesMatcherContext(prerequisites: ISplit['prerequisites'] = [], storage: IStorageSync | IStorageAsync, log: ILogger) { + + return function prerequisitesMatcher(key: SplitIO.SplitKey, attributes: SplitIO.Attributes | undefined, splitEvaluator: ISplitEvaluator): MaybeThenable { + + function evaluatePrerequisite(prerequisite: { n: string; ts: string[] }): MaybeThenable { + const evaluation = splitEvaluator(log, key, prerequisite.n, attributes, storage); + return thenable(evaluation) + ? evaluation.then(evaluation => prerequisite.ts.indexOf(evaluation.treatment!) === -1) + : prerequisite.ts.indexOf(evaluation.treatment!) === -1; + } + + return prerequisites.reduce>((prerequisitesMet, prerequisite) => { + return thenable(prerequisitesMet) ? + prerequisitesMet.then(prerequisitesMet => !prerequisitesMet ? false : evaluatePrerequisite(prerequisite)) : + !prerequisitesMet ? false : evaluatePrerequisite(prerequisite); + }, true); + }; +} From 65ae7b1ba25e791453bd23c59c644bc2ab4a3484 Mon Sep 17 00:00:00 2001 From: Emiliano Sanchez Date: Thu, 3 Apr 2025 17:39:39 -0300 Subject: [PATCH 07/11] Unit tests --- src/evaluator/Engine.ts | 2 +- .../matchers/__tests__/prerequisites.spec.ts | 106 ++++++++++++++++++ src/evaluator/matchers/prerequisites.ts | 15 ++- 3 files changed, 114 insertions(+), 9 deletions(-) create mode 100644 src/evaluator/matchers/__tests__/prerequisites.spec.ts diff --git a/src/evaluator/Engine.ts b/src/evaluator/Engine.ts index 9a6df6f1..4228316f 100644 --- a/src/evaluator/Engine.ts +++ b/src/evaluator/Engine.ts @@ -62,7 +62,7 @@ export function engineParser(log: ILogger, split: ISplit, storage: IStorageSync }; } - const prerequisitesMet = prerequisiteMatcher(key, attributes, splitEvaluator); + const prerequisitesMet = prerequisiteMatcher({ key, attributes }, splitEvaluator); return thenable(prerequisitesMet) ? prerequisitesMet.then(evaluate) : diff --git a/src/evaluator/matchers/__tests__/prerequisites.spec.ts b/src/evaluator/matchers/__tests__/prerequisites.spec.ts new file mode 100644 index 00000000..82059fc9 --- /dev/null +++ b/src/evaluator/matchers/__tests__/prerequisites.spec.ts @@ -0,0 +1,106 @@ +import { evaluateFeature } from '../../index'; +import { IStorageSync } from '../../../storages/types'; +import { loggerMock } from '../../../logger/__tests__/sdkLogger.mock'; +import { ISplit } from '../../../dtos/types'; +import { ALWAYS_ON_SPLIT, ALWAYS_OFF_SPLIT } from '../../../storages/__tests__/testUtils'; +import { prerequisitesMatcherContext } from '../prerequisites'; + +const STORED_SPLITS: Record = { + 'always-on': ALWAYS_ON_SPLIT, + 'always-off': ALWAYS_OFF_SPLIT +}; + +const mockStorage = { + splits: { + getSplit: (name: string) => STORED_SPLITS[name] + } +} as IStorageSync; + +test('MATCHER PREREQUISITES / should return true when all prerequisites are met', () => { + // A single prerequisite + const matcherTrueAlwaysOn = prerequisitesMatcherContext([{ + n: 'always-on', + ts: ['not-existing', 'on', 'other'] // We should match from a list of treatments + }], mockStorage, loggerMock); + expect(matcherTrueAlwaysOn({ key: 'a-key' }, evaluateFeature)).toBe(true); // Feature flag returns one of the expected treatments, so the matcher returns true + + const matcherFalseAlwaysOn = prerequisitesMatcherContext([{ + n: 'always-on', + ts: ['off', 'v1'] + }], mockStorage, loggerMock); + expect(matcherFalseAlwaysOn({ key: 'a-key' }, evaluateFeature)).toBe(false); // Feature flag returns treatment "on", but we are expecting ["off", "v1"], so the matcher returns false + + const matcherTrueAlwaysOff = prerequisitesMatcherContext([{ + n: 'always-off', + ts: ['not-existing', 'off'] + }], mockStorage, loggerMock); + expect(matcherTrueAlwaysOff({ key: 'a-key' }, evaluateFeature)).toBe(true); // Feature flag returns one of the expected treatments, so the matcher returns true + + const matcherFalseAlwaysOff = prerequisitesMatcherContext([{ + n: 'always-off', + ts: ['v1', 'on'] + }], mockStorage, loggerMock); + expect(matcherFalseAlwaysOff({ key: 'a-key' }, evaluateFeature)).toBe(false); // Feature flag returns treatment "on", but we are expecting ["off", "v1"], so the matcher returns false + + // Multiple prerequisites + const matcherTrueMultiplePrerequisites = prerequisitesMatcherContext([ + { + n: 'always-on', + ts: ['on'] + }, + { + n: 'always-off', + ts: ['off'] + } + ], mockStorage, loggerMock); + expect(matcherTrueMultiplePrerequisites({ key: 'a-key' }, evaluateFeature)).toBe(true); // All prerequisites are met, so the matcher returns true + + const matcherFalseMultiplePrerequisites = prerequisitesMatcherContext([ + { + n: 'always-on', + ts: ['on'] + }, + { + n: 'always-off', + ts: ['on'] + } + ], mockStorage, loggerMock); + expect(matcherFalseMultiplePrerequisites({ key: 'a-key' }, evaluateFeature)).toBe(false); // One of the prerequisites is not met, so the matcher returns false +}); + +test('MATCHER PREREQUISITES / Edge cases', () => { + // No prerequisites + const matcherTrueNoPrerequisites = prerequisitesMatcherContext(undefined, mockStorage, loggerMock); + expect(matcherTrueNoPrerequisites({ key: 'a-key' }, evaluateFeature)).toBe(true); + + const matcherTrueEmptyPrerequisites = prerequisitesMatcherContext([], mockStorage, loggerMock); + expect(matcherTrueEmptyPrerequisites({ key: 'a-key' }, evaluateFeature)).toBe(true); + + // Non existent feature flag + const matcherParentNotExist = prerequisitesMatcherContext([{ + n: 'not-existent-feature-flag', + ts: ['on', 'off'] + }], mockStorage, loggerMock); + expect(matcherParentNotExist({ key: 'a-key' }, evaluateFeature)).toBe(false); // If the feature flag does not exist, matcher should return false + + // Empty treatments list + const matcherNoTreatmentsExpected = prerequisitesMatcherContext([ + { + n: 'always-on', + ts: [] + }], mockStorage, loggerMock); + expect(matcherNoTreatmentsExpected({ key: 'a-key' }, evaluateFeature)).toBe(false); // If treatments expectation list is empty, matcher should return false (no treatment will match) + + const matcherExpectedTreatmentWrongTypeMatching = prerequisitesMatcherContext([{ + n: 'always-on', // @ts-ignore + ts: [null, [1, 2], 3, {}, true, 'on'] + + }], mockStorage, loggerMock); + expect(matcherExpectedTreatmentWrongTypeMatching({ key: 'a-key' }, evaluateFeature)).toBe(true); // If treatments expectation list has elements of the wrong type, those elements are overlooked. + + const matcherExpectedTreatmentWrongTypeNotMatching = prerequisitesMatcherContext([{ + n: 'always-off', // @ts-ignore + ts: [null, [1, 2], 3, {}, true, 'on'] + }], mockStorage, loggerMock); + expect(matcherExpectedTreatmentWrongTypeNotMatching({ key: 'a-key' }, evaluateFeature)).toBe(false); // If treatments expectation list has elements of the wrong type, those elements are overlooked. +}); diff --git a/src/evaluator/matchers/prerequisites.ts b/src/evaluator/matchers/prerequisites.ts index a9b3b031..9bee45b3 100644 --- a/src/evaluator/matchers/prerequisites.ts +++ b/src/evaluator/matchers/prerequisites.ts @@ -2,24 +2,23 @@ import { ISplit, MaybeThenable } from '../../dtos/types'; import { IStorageAsync, IStorageSync } from '../../storages/types'; import { ILogger } from '../../logger/types'; import { thenable } from '../../utils/promise/thenable'; -import { ISplitEvaluator } from '../types'; -import SplitIO from '../../../types/splitio'; +import { IDependencyMatcherValue, ISplitEvaluator } from '../types'; export function prerequisitesMatcherContext(prerequisites: ISplit['prerequisites'] = [], storage: IStorageSync | IStorageAsync, log: ILogger) { - return function prerequisitesMatcher(key: SplitIO.SplitKey, attributes: SplitIO.Attributes | undefined, splitEvaluator: ISplitEvaluator): MaybeThenable { + return function prerequisitesMatcher({ key, attributes }: IDependencyMatcherValue, splitEvaluator: ISplitEvaluator): MaybeThenable { function evaluatePrerequisite(prerequisite: { n: string; ts: string[] }): MaybeThenable { const evaluation = splitEvaluator(log, key, prerequisite.n, attributes, storage); - return thenable(evaluation) - ? evaluation.then(evaluation => prerequisite.ts.indexOf(evaluation.treatment!) === -1) - : prerequisite.ts.indexOf(evaluation.treatment!) === -1; + return thenable(evaluation) ? + evaluation.then(evaluation => prerequisite.ts.indexOf(evaluation.treatment!) !== -1) : + prerequisite.ts.indexOf(evaluation.treatment!) !== -1; } return prerequisites.reduce>((prerequisitesMet, prerequisite) => { return thenable(prerequisitesMet) ? - prerequisitesMet.then(prerequisitesMet => !prerequisitesMet ? false : evaluatePrerequisite(prerequisite)) : - !prerequisitesMet ? false : evaluatePrerequisite(prerequisite); + prerequisitesMet.then(prerequisitesMet => prerequisitesMet ? evaluatePrerequisite(prerequisite) : false) : + prerequisitesMet ? evaluatePrerequisite(prerequisite) : false; }, true); }; } From d0a0986ad4076689142c3651fa77e63d78ed06ef Mon Sep 17 00:00:00 2001 From: Emiliano Sanchez Date: Fri, 4 Apr 2025 16:04:43 -0300 Subject: [PATCH 08/11] Update changelog entry --- CHANGES.txt | 1 + 1 file changed, 1 insertion(+) diff --git a/CHANGES.txt b/CHANGES.txt index c2534182..2fa36d3b 100644 --- a/CHANGES.txt +++ b/CHANGES.txt @@ -1,5 +1,6 @@ 2.3.0 (April XXX, 2025) - Added support for targeting rules based on rule-based segments. + - Added support for feature flag prerequisites. 2.2.0 (March 28, 2025) - Added a new optional argument to the client `getTreatment` methods to allow passing additional evaluation options, such as a map of properties to append to the generated impressions sent to Split backend. Read more in our docs. From 280ed6e61ddfb214a52e8cd6c92ad005045188d9 Mon Sep 17 00:00:00 2001 From: Emiliano Sanchez Date: Tue, 15 Apr 2025 13:59:29 -0300 Subject: [PATCH 09/11] rc --- package-lock.json | 4 ++-- package.json | 2 +- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/package-lock.json b/package-lock.json index 5a5d556b..51246fd4 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,12 +1,12 @@ { "name": "@splitsoftware/splitio-commons", - "version": "2.2.0", + "version": "2.2.1-rc.0", "lockfileVersion": 2, "requires": true, "packages": { "": { "name": "@splitsoftware/splitio-commons", - "version": "2.2.0", + "version": "2.2.1-rc.0", "license": "Apache-2.0", "dependencies": { "@types/ioredis": "^4.28.0", diff --git a/package.json b/package.json index e7912d5c..9807d793 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "@splitsoftware/splitio-commons", - "version": "2.2.0", + "version": "2.2.1-rc.0", "description": "Split JavaScript SDK common components", "main": "cjs/index.js", "module": "esm/index.js", From e660afe5df41ef85234164e6784b727eb8250023 Mon Sep 17 00:00:00 2001 From: Emiliano Sanchez Date: Mon, 19 May 2025 18:51:16 -0300 Subject: [PATCH 10/11] Remove type assertion --- package-lock.json | 4 ++-- package.json | 2 +- src/evaluator/matchers/rbsegment.ts | 4 ++-- src/storages/types.ts | 1 + 4 files changed, 6 insertions(+), 5 deletions(-) diff --git a/package-lock.json b/package-lock.json index f098b430..2aa5d335 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,12 +1,12 @@ { "name": "@splitsoftware/splitio-commons", - "version": "2.3.0", + "version": "2.3.1-rc.0", "lockfileVersion": 2, "requires": true, "packages": { "": { "name": "@splitsoftware/splitio-commons", - "version": "2.3.0", + "version": "2.3.1-rc.0", "license": "Apache-2.0", "dependencies": { "@types/ioredis": "^4.28.0", diff --git a/package.json b/package.json index 3dc299c1..cd22d2ba 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "@splitsoftware/splitio-commons", - "version": "2.3.0", + "version": "2.3.1-rc.0", "description": "Split JavaScript SDK common components", "main": "cjs/index.js", "module": "esm/index.js", diff --git a/src/evaluator/matchers/rbsegment.ts b/src/evaluator/matchers/rbsegment.ts index 2bab7388..f9cc12e4 100644 --- a/src/evaluator/matchers/rbsegment.ts +++ b/src/evaluator/matchers/rbsegment.ts @@ -39,8 +39,8 @@ export function ruleBasedSegmentMatcherContext(segmentName: string, storage: ISt storage.segments.isInSegment(name, matchingKey) : type === RULE_BASED_SEGMENT ? ruleBasedSegmentMatcherContext(name, storage, log)({ key, attributes }, splitEvaluator) : - type === LARGE_SEGMENT && (storage as IStorageSync).largeSegments ? - (storage as IStorageSync).largeSegments!.isInSegment(name, matchingKey) : + type === LARGE_SEGMENT && storage.largeSegments ? + storage.largeSegments.isInSegment(name, matchingKey) : false; } diff --git a/src/storages/types.ts b/src/storages/types.ts index 451ec700..8e93daca 100644 --- a/src/storages/types.ts +++ b/src/storages/types.ts @@ -458,6 +458,7 @@ export interface IStorageBase< splits: TSplitsCache, rbSegments: TRBSegmentsCache, segments: TSegmentsCache, + largeSegments?: TSegmentsCache, impressions: TImpressionsCache, impressionCounts: TImpressionsCountCache, events: TEventsCache, From 68590fa654a8d4ec859c520c5df90f502fa3cb0c Mon Sep 17 00:00:00 2001 From: Emiliano Sanchez Date: Tue, 27 May 2025 15:50:58 -0300 Subject: [PATCH 11/11] Stable version --- CHANGES.txt | 2 +- package-lock.json | 4 ++-- package.json | 2 +- src/sdkManager/__tests__/mocks/input.json | 2 +- src/sdkManager/__tests__/mocks/output.json | 2 +- 5 files changed, 6 insertions(+), 6 deletions(-) diff --git a/CHANGES.txt b/CHANGES.txt index 517af5d2..1f48c5fa 100644 --- a/CHANGES.txt +++ b/CHANGES.txt @@ -1,4 +1,4 @@ -2.4.0 (May 20, 2025) +2.4.0 (May 27, 2025) - Added support for targeting rules based on rule-based segments. - Added support for feature flag prerequisites. diff --git a/package-lock.json b/package-lock.json index 2aa5d335..afd5917c 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,12 +1,12 @@ { "name": "@splitsoftware/splitio-commons", - "version": "2.3.1-rc.0", + "version": "2.4.0", "lockfileVersion": 2, "requires": true, "packages": { "": { "name": "@splitsoftware/splitio-commons", - "version": "2.3.1-rc.0", + "version": "2.4.0", "license": "Apache-2.0", "dependencies": { "@types/ioredis": "^4.28.0", diff --git a/package.json b/package.json index cd22d2ba..60d02afa 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "@splitsoftware/splitio-commons", - "version": "2.3.1-rc.0", + "version": "2.4.0", "description": "Split JavaScript SDK common components", "main": "cjs/index.js", "module": "esm/index.js", diff --git a/src/sdkManager/__tests__/mocks/input.json b/src/sdkManager/__tests__/mocks/input.json index 2e951c2c..313dd38e 100644 --- a/src/sdkManager/__tests__/mocks/input.json +++ b/src/sdkManager/__tests__/mocks/input.json @@ -52,4 +52,4 @@ ] } ] -} \ No newline at end of file +} diff --git a/src/sdkManager/__tests__/mocks/output.json b/src/sdkManager/__tests__/mocks/output.json index 7a4f72ef..67948bf9 100644 --- a/src/sdkManager/__tests__/mocks/output.json +++ b/src/sdkManager/__tests__/mocks/output.json @@ -14,4 +14,4 @@ "flagName": "some_flag", "treatments": ["on"] }] -} \ No newline at end of file +}