@splitsoftware/splitio-commons
Version:
Split JavaScript SDK common components
192 lines (191 loc) • 10.9 kB
JavaScript
import { timeout } from '../../../utils/promise/timeout';
import { SDK_DEFINITIONS_ARRIVED, FLAGS_UPDATE, SEGMENTS_UPDATE, CONFIGS_UPDATE } from '../../../readiness/constants';
import { SYNC_FETCH, SYNC_UPDATE, SYNC_FETCH_FAILS, SYNC_FETCH_RETRY } from '../../../logger/constants';
import { startsWith } from '../../../utils/lang';
import { IN_RULE_BASED_SEGMENT, IN_SEGMENT, RULE_BASED_SEGMENT, STANDARD_SEGMENT } from '../../../utils/constants';
import { setToArray } from '../../../utils/lang/sets';
import { SPLIT_UPDATE } from '../../streaming/constants';
/**
* Collect segments from a raw FF or RBS definition.
* Exported for testing purposes.
*/
export function parseSegments(ruleEntity, matcherType) {
if (matcherType === void 0) { matcherType = IN_SEGMENT; }
var _a = ruleEntity, conditions = _a.conditions, excluded = _a.excluded;
var segments = new Set();
if (excluded && excluded.segments) {
excluded.segments.forEach(function (_a) {
var type = _a.type, name = _a.name;
if ((type === STANDARD_SEGMENT && matcherType === IN_SEGMENT) || (type === RULE_BASED_SEGMENT && matcherType === IN_RULE_BASED_SEGMENT)) {
segments.add(name);
}
});
}
if (conditions) {
for (var i = 0; i < conditions.length; i++) {
var matchers = conditions[i].matcherGroup.matchers;
matchers.forEach(function (matcher) {
if (matcher.matcherType === matcherType)
segments.add(matcher.userDefinedSegmentMatcherData.segmentName);
});
}
}
return segments;
}
/**
* If there are defined filters and one definition doesn't match with them, its status is changed to 'ARCHIVE' to avoid storing it
* If there is `bySet` filter, `byName` and `byPrefix` filters are ignored
*
* @param definition - definition to be evaluated
* @param filters - splitFiltersValidation bySet | byName
*/
function matchFilters(definition, filters) {
var _a = filters.groupedFilters, setsFilter = _a.bySet, namesFilter = _a.byName, prefixFilter = _a.byPrefix;
if (setsFilter.length > 0)
return definition.sets && definition.sets.some(function (definitionSet) { return setsFilter.indexOf(definitionSet) > -1; });
var namesFilterConfigured = namesFilter.length > 0;
var prefixFilterConfigured = prefixFilter.length > 0;
if (!namesFilterConfigured && !prefixFilterConfigured)
return true;
var matchNames = namesFilterConfigured && namesFilter.indexOf(definition.name) > -1;
var matchPrefix = prefixFilterConfigured && prefixFilter.some(function (prefix) { return startsWith(definition.name, prefix); });
return matchNames || matchPrefix;
}
/**
* Given the list of definitions from /splitChanges or /configs endpoint, it returns the mutations,
* i.e., an object with added definitions, removed definitions, and used segments.
* Exported for testing purposes.
*/
export function computeMutation(update, segments, filters) {
return update.updated.reduce(function (accum, ruleEntity) {
if (!filters || matchFilters(ruleEntity, filters)) {
accum.updated.push(ruleEntity);
parseSegments(ruleEntity).forEach(function (segmentName) {
segments.add(segmentName);
});
}
else {
accum.removed.push(ruleEntity.name);
}
accum.names.push(ruleEntity.name);
return accum;
}, { updated: [], removed: update.removed, names: update.removed.slice() });
}
function convertInstantUpdateToDefinitionChanges(instantUpdate) {
var payload = instantUpdate.payload, changeNumber = instantUpdate.changeNumber;
return payload.status === 'ARCHIVED' ?
{ till: changeNumber, updated: [], removed: [payload.name] } :
{ till: changeNumber, updated: [payload], removed: [] };
}
/**
* Factory of DefinitionChanges updater, a task that:
* - fetches definition changes using `definitionChangesFetcher`
* - updates definitions storage
* - uses `definitionsEventEmitter` to emit events related to definition data updates
*
* @param log - Logger instance
* @param definitionChangesFetcher - Fetcher of `/splitChanges` or `/configs`
* @param definitions - Definitions storage, with sync or async methods
* @param segments - Segments storage, with sync or async methods
* @param definitionsEventEmitter - Optional readiness manager. Not required for synchronizer or producer mode.
* @param requestTimeoutBeforeReady - How long the updater will wait for the request to timeout. Default 0, i.e., never timeout.
* @param retriesOnFailureBeforeReady - How many retries on `/splitChanges` or `/configs` we the updater do in case of failure or timeout. Default 0, i.e., no retries.
*/
export function definitionChangesUpdaterFactory(log, definitionChangesFetcher, storage, splitFiltersValidation, definitionsEventEmitter, requestTimeoutBeforeReady, retriesOnFailureBeforeReady, segmentsSyncTask // undefined for client-side
) {
if (requestTimeoutBeforeReady === void 0) { requestTimeoutBeforeReady = 0; }
if (retriesOnFailureBeforeReady === void 0) { retriesOnFailureBeforeReady = 0; }
var definitions = storage.definitions, rbSegments = storage.rbSegments, segments = storage.segments;
var startingUp = true;
/** timeout decorator for `definitionChangesFetcher` promise */
function _promiseDecorator(promise) {
if (startingUp && requestTimeoutBeforeReady)
promise = timeout(requestTimeoutBeforeReady, promise);
return promise;
}
/**
* DefinitionChanges updater returns a promise that resolves with a `false` boolean value if it fails to fetch definitions or synchronize them with the storage.
* Returned promise will not be rejected.
*
* @param noCache - true to revalidate data to fetch
* @param till - query param to bypass CDN requests
*/
return function definitionChangesUpdater(noCache, till, instantUpdate) {
/**
* @param since - current changeNumber at definitionsCache
* @param retry - current number of retry attempts
*/
function _definitionChangesUpdater(sinces, retry) {
if (retry === void 0) { retry = 0; }
var since = sinces[0], rbSince = sinces[1];
log.debug(SYNC_FETCH, [definitionChangesFetcher.type, since, rbSince]);
return Promise.resolve(instantUpdate ?
instantUpdate.type === SPLIT_UPDATE ?
// IFFU edge case: a change to definition that adds an IN_RULE_BASED_SEGMENT matcher that is not present yet
Promise.resolve(rbSegments.contains(parseSegments(instantUpdate.payload, IN_RULE_BASED_SEGMENT))).then(function (contains) {
return contains ?
{ d: convertInstantUpdateToDefinitionChanges(instantUpdate) } :
definitionChangesFetcher(since, noCache, till, rbSince, _promiseDecorator);
}) :
{ rbs: convertInstantUpdateToDefinitionChanges(instantUpdate) } :
definitionChangesFetcher(since, noCache, till, rbSince, _promiseDecorator))
.then(function (definitionChanges) {
var usedSegments = new Set();
var updatedDefinitions = [];
var ffUpdate = false;
if (definitionChanges.d) {
var _a = computeMutation(definitionChanges.d, usedSegments, splitFiltersValidation), updated = _a.updated, removed = _a.removed, names = _a.names;
updatedDefinitions = names;
log.debug(SYNC_UPDATE, [definitionChangesFetcher.type, updated.length, removed.length]);
ffUpdate = definitions.update(updated, removed, definitionChanges.d.till);
}
var rbsUpdate = false;
if (definitionChanges.rbs) {
var _b = computeMutation(definitionChanges.rbs, usedSegments), updated = _b.updated, removed = _b.removed;
log.debug(SYNC_UPDATE, ['rule-based segments', updated.length, removed.length]);
rbsUpdate = rbSegments.update(updated, removed, definitionChanges.rbs.till);
}
return Promise.all([ffUpdate, rbsUpdate,
// @TODO if at least 1 segment fetch fails due to 404 and other segments are updated in the storage, SDK_UPDATE is not emitted
segments.registerSegments(setToArray(usedSegments))
]).then(function (_a) {
var ffChanged = _a[0], rbsChanged = _a[1];
if (storage.save)
storage.save();
startingUp = false;
if (definitionsEventEmitter) {
// To emit SDK_DEFINITIONS_ARRIVED for server-side SDK, we must wait for all registered segments to be fetched
return Promise.resolve(!definitionsEventEmitter.definitionsArrived || ((ffChanged || rbsChanged) && (!segmentsSyncTask || segmentsSyncTask.execute(true))))
.catch(function () { return false; } /** noop. just to handle a possible `checkAllSegmentsExist` rejection, before emitting SDK event */)
.then(function (emitSplitsArrivedEvent) {
// emit SDK events
if (emitSplitsArrivedEvent) {
var metadata = {
type: updatedDefinitions.length > 0 ? definitionChangesFetcher.type === 'configs' ? CONFIGS_UPDATE : FLAGS_UPDATE : SEGMENTS_UPDATE,
names: updatedDefinitions.length > 0 ? updatedDefinitions : []
};
definitionsEventEmitter.emit(SDK_DEFINITIONS_ARRIVED, metadata);
}
return true;
});
}
return true;
});
})
.catch(function (error) {
if (startingUp && retriesOnFailureBeforeReady > retry) {
retry += 1;
log.warn(SYNC_FETCH_RETRY, [definitionChangesFetcher.type, retry, error]);
return _definitionChangesUpdater(sinces, retry);
}
else {
startingUp = false;
log.warn(SYNC_FETCH_FAILS, [definitionChangesFetcher.type, error]);
}
return false;
});
}
// `getChangeNumber` never rejects or throws error
return Promise.all([definitions.getChangeNumber(), rbSegments.getChangeNumber()]).then(_definitionChangesUpdater);
};
}