import { engineParser } from './Engine'; import { thenable } from '../utils/promise/thenable'; import { EXCEPTION, NO_CONDITION_MATCH, DEFINITION_NOT_FOUND } from '../utils/labels'; import { CONTROL } from '../utils/constants'; import { IDefinition, MaybeThenable } from '../dtos/types'; import { IStorageAsync, IStorageSync } from '../storages/types'; import { IEvaluationResult } from './types'; import SplitIO from '../../types/splitio'; import { ILogger } from '../logger/types'; import { returnSetsUnion, setToArray } from '../utils/lang/sets'; import { WARN_FLAGSET_WITHOUT_FLAGS } from '../logger/constants'; const EVALUATION_EXCEPTION = { treatment: CONTROL, label: EXCEPTION, config: null }; const EVALUATION_DEFINITION_NOT_FOUND = { treatment: CONTROL, label: DEFINITION_NOT_FOUND, config: null }; function treatmentsException(definitionNames: string[]) { const evaluations: Record = {}; definitionNames.forEach(definitionName => { evaluations[definitionName] = EVALUATION_EXCEPTION; }); return evaluations; } export function evaluateFeature( log: ILogger, key: SplitIO.SplitKey, definitionName: string, attributes: SplitIO.Attributes | undefined, storage: IStorageSync | IStorageAsync, options?: SplitIO.EvaluationOptions ): MaybeThenable { let definition; try { definition = storage.definitions.get(definitionName); } catch (e) { // Exception on sync storage. Not possible ATM with InMemory and InLocal storages. return EVALUATION_EXCEPTION; } if (thenable(definition)) { return definition.then((definition) => getEvaluation( log, key, definition, attributes, storage, options, )).catch( // Exception on async storage. For example, when the storage is redis or // pluggable and there is a connection issue and we can't retrieve the split to be evaluated () => EVALUATION_EXCEPTION ); } return getEvaluation( log, key, definition, attributes, storage, options, ); } export function evaluateFeatures( log: ILogger, key: SplitIO.SplitKey, definitionNames: string[], attributes: SplitIO.Attributes | undefined, storage: IStorageSync | IStorageAsync, options?: SplitIO.EvaluationOptions, ): MaybeThenable> { let definitions; try { definitions = storage.definitions.getMany(definitionNames); } catch (e) { // Exception on sync storage. Not possible ATM with InMemory and InLocal storages. return treatmentsException(definitionNames); } return thenable(definitions) ? definitions.then(definitions => getEvaluations(log, key, definitionNames, definitions, attributes, storage, options)) .catch(() => { // Exception on async storage. For example, when the storage is redis or // pluggable and there is a connection issue and we can't retrieve the split to be evaluated return treatmentsException(definitionNames); }) : getEvaluations(log, key, definitionNames, definitions, attributes, storage, options); } export function evaluateFeaturesByFlagSets( log: ILogger, key: SplitIO.SplitKey, flagSets: string[], attributes: SplitIO.Attributes | undefined, storage: IStorageSync | IStorageAsync, method: string, options?: SplitIO.EvaluationOptions, ): MaybeThenable> { let storedFlagNames: MaybeThenable[]>; function evaluate(featureFlagsByFlagSets: Set[]) { let featureFlags = new Set(); for (let i = 0; i < flagSets.length; i++) { const featureFlagByFlagSet = featureFlagsByFlagSets[i]; if (featureFlagByFlagSet.size) { featureFlags = returnSetsUnion(featureFlags, featureFlagByFlagSet); } else { log.warn(WARN_FLAGSET_WITHOUT_FLAGS, [method, flagSets[i]]); } } return featureFlags.size ? evaluateFeatures(log, key, setToArray(featureFlags), attributes, storage, options) : {}; } // get features by flag sets try { storedFlagNames = storage.definitions.getNamesBySets(flagSets); } catch (e) { // return empty evaluations return {}; } // evaluate related features return thenable(storedFlagNames) ? storedFlagNames.then((storedFlagNames) => evaluate(storedFlagNames)) .catch(() => { return {}; }) : evaluate(storedFlagNames); } function getEvaluation( log: ILogger, key: SplitIO.SplitKey, definition: IDefinition | null, attributes: SplitIO.Attributes | undefined, storage: IStorageSync | IStorageAsync, options?: SplitIO.EvaluationOptions, ): MaybeThenable { if (definition) { const split = engineParser(log, definition, storage); const evaluation = split.getTreatment(key, attributes, evaluateFeature); // If the storage is async and the evaluated definition uses segments or dependencies, evaluation is thenable if (thenable(evaluation)) { return evaluation.then(result => { result.changeNumber = definition.changeNumber; result.config = definition.configurations && definition.configurations[result.treatment] || null; // @ts-expect-error impressionsDisabled is not exposed in the public typings yet. result.impressionsDisabled = options?.impressionsDisabled || definition.impressionsDisabled; return result; }); } else { evaluation.changeNumber = definition.changeNumber; evaluation.config = definition.configurations && definition.configurations[evaluation.treatment] || null; // @ts-expect-error impressionsDisabled is not exposed in the public typings yet. evaluation.impressionsDisabled = options?.impressionsDisabled || definition.impressionsDisabled; } return evaluation; } return EVALUATION_DEFINITION_NOT_FOUND; } function getEvaluations( log: ILogger, key: SplitIO.SplitKey, definitionNames: string[], definitions: Record, attributes: SplitIO.Attributes | undefined, storage: IStorageSync | IStorageAsync, options?: SplitIO.EvaluationOptions, ): MaybeThenable> { const result: Record = {}; const thenables: Promise[] = []; definitionNames.forEach(definitionName => { const evaluation = getEvaluation( log, key, definitions[definitionName], attributes, storage, options ); if (thenable(evaluation)) { thenables.push(evaluation.then(res => { result[definitionName] = res; })); } else { result[definitionName] = evaluation; } }); return thenables.length > 0 ? Promise.all(thenables).then(() => result) : result; } export function evaluateDefaultTreatment( definitionName: string, storage: IStorageSync | IStorageAsync, ): MaybeThenable { let definition; try { definition = storage.definitions.get(definitionName); } catch (e) { return EVALUATION_EXCEPTION; } return thenable(definition) ? definition.then(getDefaultTreatment).catch(() => EVALUATION_EXCEPTION) : getDefaultTreatment(definition); } function getDefaultTreatment( definition: IDefinition | null, ): MaybeThenable { if (definition) { return { treatment: definition.defaultTreatment, label: NO_CONDITION_MATCH, // "default rule" config: definition.configurations && definition.configurations[definition.defaultTreatment] || null, changeNumber: definition.changeNumber }; } return EVALUATION_DEFINITION_NOT_FOUND; }