UNPKG

@splitsoftware/splitio-commons

Version:
198 lines (197 loc) 11.3 kB
"use strict"; Object.defineProperty(exports, "__esModule", { value: true }); exports.definitionChangesUpdaterFactory = 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"); /** * 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, 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 === 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); } }); } 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; } exports.parseSegments = parseSegments; /** * 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 (0, lang_1.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. */ 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() }); } exports.computeMutation = computeMutation; 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. */ 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 = (0, timeout_1.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(constants_2.SYNC_FETCH, [definitionChangesFetcher.type, since, rbSince]); return Promise.resolve(instantUpdate ? instantUpdate.type === constants_4.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, constants_3.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(constants_2.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(constants_2.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((0, sets_1.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' ? constants_1.CONFIGS_UPDATE : constants_1.FLAGS_UPDATE : constants_1.SEGMENTS_UPDATE, names: updatedDefinitions.length > 0 ? updatedDefinitions : [] }; definitionsEventEmitter.emit(constants_1.SDK_DEFINITIONS_ARRIVED, metadata); } return true; }); } return true; }); }) .catch(function (error) { if (startingUp && retriesOnFailureBeforeReady > retry) { retry += 1; log.warn(constants_2.SYNC_FETCH_RETRY, [definitionChangesFetcher.type, retry, error]); return _definitionChangesUpdater(sinces, retry); } else { startingUp = false; log.warn(constants_2.SYNC_FETCH_FAILS, [definitionChangesFetcher.type, error]); } return false; }); } // `getChangeNumber` never rejects or throws error return Promise.all([definitions.getChangeNumber(), rbSegments.getChangeNumber()]).then(_definitionChangesUpdater); }; } exports.definitionChangesUpdaterFactory = definitionChangesUpdaterFactory;