@splitsoftware/splitio-commons
Version:
Split JavaScript SDK common components
245 lines (216 loc) • 7.68 kB
text/typescript
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<string, IEvaluationResult> = {};
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<IEvaluationResult> {
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<Record<string, IEvaluationResult>> {
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<Record<string, IEvaluationResult>> {
let storedFlagNames: MaybeThenable<Set<string>[]>;
function evaluate(featureFlagsByFlagSets: Set<string>[]) {
let featureFlags = new Set<string>();
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<IEvaluationResult> {
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<string, IDefinition | null>,
attributes: SplitIO.Attributes | undefined,
storage: IStorageSync | IStorageAsync,
options?: SplitIO.EvaluationOptions,
): MaybeThenable<Record<string, IEvaluationResult>> {
const result: Record<string, IEvaluationResult> = {};
const thenables: Promise<void>[] = [];
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<IEvaluationResult> {
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<IEvaluationResult> {
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;
}