@splitsoftware/splitio-commons
Version:
Split JavaScript SDK common components
189 lines (188 loc) • 10.5 kB
JavaScript
;
Object.defineProperty(exports, "__esModule", { value: true });
exports.splitChangesUpdaterFactory = exports.computeMutation = exports.parseSegments = void 0;
var timeout_1 = require("../../../utils/promise/timeout");
var constants_1 = require("../../../readiness/constants");
var constants_2 = require("../../../logger/constants");
var lang_1 = require("../../../utils/lang");
var constants_3 = require("../../../utils/constants");
var sets_1 = require("../../../utils/lang/sets");
var constants_4 = require("../../streaming/constants");
// Checks that all registered segments have been fetched (changeNumber !== -1 for every segment).
// Returns a promise that could be rejected.
// @TODO review together with Segments and MySegments storage APIs
function checkAllSegmentsExist(segments) {
var registeredSegments = Promise.resolve(segments.getRegisteredSegments());
return registeredSegments.then(function (segmentNames) {
return Promise.all(segmentNames.map(function (segmentName) { return segments.getChangeNumber(segmentName); }))
.then(function (changeNumbers) { return changeNumbers.every(function (changeNumber) { return changeNumber !== undefined; }); });
});
}
/**
* Collect segments from a raw FF or RBS definition.
* Exported for testing purposes.
*/
function parseSegments(ruleEntity, matcherType) {
if (matcherType === void 0) { matcherType = constants_3.IN_SEGMENT; }
var _a = ruleEntity, _b = _a.conditions, conditions = _b === void 0 ? [] : _b, 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 === constants_3.STANDARD_SEGMENT && matcherType === constants_3.IN_SEGMENT) || (type === constants_3.RULE_BASED_SEGMENT && matcherType === constants_3.IN_RULE_BASED_SEGMENT)) {
segments.add(name);
}
});
}
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;
}
exports.parseSegments = parseSegments;
/**
* If there are defined filters and one feature flag doesn't match with them, its status is changed to 'ARCHIVE' to avoid storing it
* If there are set filter defined, names filter is ignored
*
* @param featureFlag - feature flag to be evaluated
* @param filters - splitFiltersValidation bySet | byName
*/
function matchFilters(featureFlag, filters) {
var _a = filters.groupedFilters, setsFilter = _a.bySet, namesFilter = _a.byName, prefixFilter = _a.byPrefix;
if (setsFilter.length > 0)
return featureFlag.sets && featureFlag.sets.some(function (featureFlagSet) { return setsFilter.indexOf(featureFlagSet) > -1; });
var namesFilterConfigured = namesFilter.length > 0;
var prefixFilterConfigured = prefixFilter.length > 0;
if (!namesFilterConfigured && !prefixFilterConfigured)
return true;
var matchNames = namesFilterConfigured && namesFilter.indexOf(featureFlag.name) > -1;
var matchPrefix = prefixFilterConfigured && prefixFilter.some(function (prefix) { return (0, lang_1.startsWith)(featureFlag.name, prefix); });
return matchNames || matchPrefix;
}
/**
* Given the list of splits from /splitChanges endpoint, it returns the mutations,
* i.e., an object with added splits, removed splits and used segments.
* Exported for testing purposes.
*/
function computeMutation(rules, segments, filters) {
return rules.reduce(function (accum, ruleEntity) {
if (ruleEntity.status === 'ACTIVE' && (!filters || matchFilters(ruleEntity, filters))) {
accum.added.push(ruleEntity);
parseSegments(ruleEntity).forEach(function (segmentName) {
segments.add(segmentName);
});
}
else {
accum.removed.push(ruleEntity);
}
return accum;
}, { added: [], removed: [] });
}
exports.computeMutation = computeMutation;
/**
* factory of SplitChanges updater, a task that:
* - fetches split changes using `splitChangesFetcher`
* - updates `splitsCache`
* - uses `splitsEventEmitter` to emit events related to split data updates
*
* @param log - Logger instance
* @param splitChangesFetcher - Fetcher of `/splitChanges`
* @param splits - Splits storage, with sync or async methods
* @param segments - Segments storage, with sync or async methods
* @param splitsEventEmitter - 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` we the updater do in case of failure or timeout. Default 0, i.e., no retries.
*/
function splitChangesUpdaterFactory(log, splitChangesFetcher, storage, splitFiltersValidation, splitsEventEmitter, requestTimeoutBeforeReady, retriesOnFailureBeforeReady, isClientSide) {
if (requestTimeoutBeforeReady === void 0) { requestTimeoutBeforeReady = 0; }
if (retriesOnFailureBeforeReady === void 0) { retriesOnFailureBeforeReady = 0; }
var splits = storage.splits, rbSegments = storage.rbSegments, segments = storage.segments;
var startingUp = true;
/** timeout decorator for `splitChangesFetcher` promise */
function _promiseDecorator(promise) {
if (startingUp && requestTimeoutBeforeReady)
promise = (0, timeout_1.timeout)(requestTimeoutBeforeReady, promise);
return promise;
}
/**
* SplitChanges updater returns a promise that resolves with a `false` boolean value if it fails to fetch splits 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 splitChangesUpdater(noCache, till, instantUpdate) {
/**
* @param since - current changeNumber at splitsCache
* @param retry - current number of retry attempts
*/
function _splitChangesUpdater(sinces, retry) {
if (retry === void 0) { retry = 0; }
var since = sinces[0], rbSince = sinces[1];
log.debug(constants_2.SYNC_SPLITS_FETCH, sinces);
return Promise.resolve(instantUpdate ?
instantUpdate.type === constants_4.SPLIT_UPDATE ?
// IFFU edge case: a change to a flag that adds an IN_RULE_BASED_SEGMENT matcher that is not present yet
Promise.resolve(rbSegments.contains(parseSegments(instantUpdate.payload, constants_3.IN_RULE_BASED_SEGMENT))).then(function (contains) {
return contains ?
{ ff: { d: [instantUpdate.payload], t: instantUpdate.changeNumber } } :
splitChangesFetcher(since, noCache, till, rbSince, _promiseDecorator);
}) :
{ rbs: { d: [instantUpdate.payload], t: instantUpdate.changeNumber } } :
splitChangesFetcher(since, noCache, till, rbSince, _promiseDecorator))
.then(function (splitChanges) {
startingUp = false;
var usedSegments = new Set();
var ffUpdate = false;
if (splitChanges.ff) {
var _a = computeMutation(splitChanges.ff.d, usedSegments, splitFiltersValidation), added = _a.added, removed = _a.removed;
log.debug(constants_2.SYNC_SPLITS_UPDATE, [added.length, removed.length]);
ffUpdate = splits.update(added, removed, splitChanges.ff.t);
}
var rbsUpdate = false;
if (splitChanges.rbs) {
var _b = computeMutation(splitChanges.rbs.d, usedSegments), added = _b.added, removed = _b.removed;
log.debug(constants_2.SYNC_RBS_UPDATE, [added.length, removed.length]);
rbsUpdate = rbSegments.update(added, removed, splitChanges.rbs.t);
}
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((0, sets_1.setToArray)(usedSegments))
]).then(function (_a) {
var ffChanged = _a[0], rbsChanged = _a[1];
if (splitsEventEmitter) {
// To emit SDK_SPLITS_ARRIVED for server-side SDK, we must check that all registered segments have been fetched
return Promise.resolve(!splitsEventEmitter.splitsArrived || ((ffChanged || rbsChanged) && (isClientSide || checkAllSegmentsExist(segments))))
.catch(function () { return false; } /** noop. just to handle a possible `checkAllSegmentsExist` rejection, before emitting SDK event */)
.then(function (emitSplitsArrivedEvent) {
// emit SDK events
if (emitSplitsArrivedEvent)
splitsEventEmitter.emit(constants_1.SDK_SPLITS_ARRIVED);
return true;
});
}
return true;
});
})
.catch(function (error) {
log.warn(constants_2.SYNC_SPLITS_FETCH_FAILS, [error]);
if (startingUp && retriesOnFailureBeforeReady > retry) {
retry += 1;
log.info(constants_2.SYNC_SPLITS_FETCH_RETRY, [retry, error]);
return _splitChangesUpdater(sinces, retry);
}
else {
startingUp = false;
}
return false;
});
}
// `getChangeNumber` never rejects or throws error
return Promise.all([splits.getChangeNumber(), rbSegments.getChangeNumber()]).then(_splitChangesUpdater);
};
}
exports.splitChangesUpdaterFactory = splitChangesUpdaterFactory;