@splitsoftware/splitio-commons
Version:
Split JavaScript SDK common components
241 lines (240 loc) • 11.9 kB
JavaScript
import { __extends, __spreadArray } from "tslib";
import { isFiniteNumber, isNaNNumber } from '../../utils/lang';
import { LOG_PREFIX } from './constants';
import { AbstractDefinitionsCacheAsync } from '../AbstractDefinitionsCacheAsync';
import { returnDifference } from '../../utils/lang/sets';
/**
* Discard errors for an answer of multiple operations.
*/
function processPipelineAnswer(results) {
return results ? results.reduce(function (accum, errValuePair) {
if (errValuePair[0] === null)
accum.push(errValuePair[1]);
return accum;
}, []) : [];
}
/**
* IDefinitionsCacheAsync implementation that stores definitions in Redis.
* Supported by Node.js
*/
var DefinitionsCacheInRedis = /** @class */ (function (_super) {
__extends(DefinitionsCacheInRedis, _super);
function DefinitionsCacheInRedis(log, keys, redis, splitFiltersValidation) {
var _this = _super.call(this) || this;
_this.log = log;
_this.redis = redis;
_this.keys = keys;
_this.setsFilter = splitFiltersValidation ? splitFiltersValidation.groupedFilters.bySet : [];
// There is no need to listen for redis 'error' event, because in that case ioredis calls will be rejected and handled by redis storage adapters.
// But it is done just to avoid getting the ioredis message `Unhandled error event`.
_this.redis.on('error', function (e) {
_this.redisError = e;
});
_this.redis.on('connect', function () {
_this.redisError = undefined;
});
return _this;
}
DefinitionsCacheInRedis.prototype._decrementCounts = function (definition) {
var _this = this;
var ttKey = this.keys.buildTrafficTypeKey(definition.trafficTypeName);
return this.redis.decr(ttKey).then(function (count) {
if (count === 0)
return _this.redis.del(ttKey);
});
};
DefinitionsCacheInRedis.prototype._incrementCounts = function (definition) {
var ttKey = this.keys.buildTrafficTypeKey(definition.trafficTypeName);
return this.redis.incr(ttKey);
};
DefinitionsCacheInRedis.prototype._updateSets = function (definitionName, setsOfRemovedDefinition, setsOfAddedDefinition) {
var _this = this;
var removeFromSets = returnDifference(setsOfRemovedDefinition, setsOfAddedDefinition);
var addToSets = returnDifference(setsOfAddedDefinition, setsOfRemovedDefinition);
if (this.setsFilter.length > 0) {
addToSets = addToSets.filter(function (set) {
return _this.setsFilter.some(function (filterSet) { return filterSet === set; });
});
}
var items = [definitionName];
return Promise.all(__spreadArray(__spreadArray([], removeFromSets.map(function (setName) { return _this.redis.srem(_this.keys.buildSetKey(setName), items); }), true), addToSets.map(function (setName) { return _this.redis.sadd(_this.keys.buildSetKey(setName), items); }), true));
};
/**
* Add a given definition.
* The returned promise is resolved when the operation success
* or rejected if it fails (e.g., redis operation fails)
*/
DefinitionsCacheInRedis.prototype.add = function (definition) {
var _this = this;
var name = definition.name;
var definitionKey = this.keys.buildDefinitionKey(name);
return this.redis.get(definitionKey).then(function (definitionFromStorage) {
// handling parsing error
var parsedPreviousDefinition, stringifiedNewDefinition;
try {
parsedPreviousDefinition = definitionFromStorage ? JSON.parse(definitionFromStorage) : undefined;
stringifiedNewDefinition = JSON.stringify(definition);
}
catch (e) {
throw new Error('Error parsing feature flag definition: ' + e);
}
return _this.redis.set(definitionKey, stringifiedNewDefinition).then(function () {
// avoid unnecessary increment/decrement operations
if (parsedPreviousDefinition && parsedPreviousDefinition.trafficTypeName === definition.trafficTypeName)
return;
// update traffic type counts
return _this._incrementCounts(definition).then(function () {
if (parsedPreviousDefinition)
return _this._decrementCounts(parsedPreviousDefinition);
});
}).then(function () { return _this._updateSets(name, parsedPreviousDefinition && parsedPreviousDefinition.sets, definition.sets); });
}).then(function () { return true; });
};
/**
* Remove a given definition.
* The returned promise is resolved when the operation success, with true or false indicating if the definition existed (and was removed) or not.
* or rejected if it fails (e.g., redis operation fails).
*/
DefinitionsCacheInRedis.prototype.remove = function (name) {
var _this = this;
return this.get(name).then(function (definition) {
if (definition) {
return _this._decrementCounts(definition).then(function () { return _this._updateSets(name, definition.sets); });
}
}).then(function () {
return _this.redis.del(_this.keys.buildDefinitionKey(name)).then(function (status) { return status === 1; });
});
};
/**
* Get definition or null if it's not defined.
* Returned promise is rejected if redis operation fails.
*/
DefinitionsCacheInRedis.prototype.get = function (name) {
if (this.redisError) {
this.log.error(LOG_PREFIX + this.redisError);
return Promise.reject(this.redisError);
}
return this.redis.get(this.keys.buildDefinitionKey(name))
.then(function (maybeDefinition) { return maybeDefinition && JSON.parse(maybeDefinition); });
};
/**
* Set till number.
* The returned promise is resolved when the operation success,
* or rejected if it fails.
*/
DefinitionsCacheInRedis.prototype.setChangeNumber = function (changeNumber) {
return this.redis.set(this.keys.buildDefinitionsTillKey(), changeNumber + '').then(function (status) { return status === 'OK'; });
};
/**
* Get till number or -1 if it's not defined.
* The returned promise is resolved with the changeNumber or -1 if it doesn't exist or a redis operation fails.
* The promise will never be rejected.
*/
DefinitionsCacheInRedis.prototype.getChangeNumber = function () {
var _this = this;
return this.redis.get(this.keys.buildDefinitionsTillKey()).then(function (value) {
var i = parseInt(value, 10);
return isNaNNumber(i) ? -1 : i;
}).catch(function (e) {
_this.log.error(LOG_PREFIX + 'Could not retrieve changeNumber from storage. Error: ' + e);
return -1;
});
};
/**
* Get list of all definitions.
* The returned promise is resolved with the list of definitions,
* or rejected if redis operation fails.
*/
// @TODO we need to benchmark which is the maximun number of commands we could pipeline without kill redis performance.
DefinitionsCacheInRedis.prototype.getAll = function () {
var _this = this;
return this.redis.keys(this.keys.searchPatternForDefinitionKeys())
.then(function (listOfKeys) { return _this.redis.pipeline(listOfKeys.map(function (k) { return ['get', k]; })).exec(); })
.then(processPipelineAnswer)
.then(function (definitions) { return definitions.map(function (definition) {
return JSON.parse(definition);
}); });
};
/**
* Get list of definition names.
* The returned promise is resolved with the list of names,
* or rejected if redis operation fails.
*/
DefinitionsCacheInRedis.prototype.getNames = function () {
var _this = this;
return this.redis.keys(this.keys.searchPatternForDefinitionKeys()).then(function (listOfKeys) { return listOfKeys.map(_this.keys.extractKey); });
};
/**
* Get list of definition names related to a given list of set names.
* The returned promise is resolved with the list of names per set,
* or rejected if the pipelined redis operation fails (e.g., timeout).
*/
DefinitionsCacheInRedis.prototype.getNamesBySets = function (sets) {
var _this = this;
return this.redis.pipeline(sets.map(function (set) { return ['smembers', _this.keys.buildSetKey(set)]; })).exec()
.then(function (results) { return results ? results.map(function (_a, index) {
var e = _a[0], value = _a[1];
if (e === null)
return value;
_this.log.error(LOG_PREFIX + "Could not read result from get members of set ".concat(sets[index], " due to an error: ").concat(e));
}) : []; })
.then(function (namesBySets) { return namesBySets.map(function (namesBySet) { return new Set(namesBySet); }); });
};
/**
* Check traffic type existence.
* The returned promise is resolved with a boolean indicating whether the TT exist or not.
* In case of redis operation failure, the promise resolves with a true value, assuming that the TT might exist.
* It will never be rejected.
*/
DefinitionsCacheInRedis.prototype.trafficTypeExists = function (trafficType) {
var _this = this;
// If there is a number there should be > 0, otherwise the TT is considered as not existent.
return this.redis.get(this.keys.buildTrafficTypeKey(trafficType))
.then(function (ttCount) {
if (ttCount === null)
return false; // if entry doesn't exist, means that TT doesn't exist
ttCount = parseInt(ttCount, 10);
if (!isFiniteNumber(ttCount) || ttCount < 0) {
_this.log.info(LOG_PREFIX + "Could not validate traffic type existence of ".concat(trafficType, " due to data corruption of some sorts."));
return false;
}
return ttCount > 0;
})
.catch(function (e) {
_this.log.error(LOG_PREFIX + "Could not validate traffic type existence of ".concat(trafficType, " due to an error: ").concat(e, "."));
// If there is an error, bypass the validation so the event can get tracked.
return true;
});
};
// @TODO remove or implement. It is not being used.
DefinitionsCacheInRedis.prototype.clear = function () {
return Promise.resolve();
};
/**
* Fetches multiple definitions.
* Returned promise is rejected if redis operation fails.
*/
DefinitionsCacheInRedis.prototype.getMany = function (names) {
var _a;
var _this = this;
if (this.redisError) {
this.log.error(LOG_PREFIX + this.redisError);
return Promise.reject(this.redisError);
}
var keys = names.map(function (name) { return _this.keys.buildDefinitionKey(name); });
return (_a = this.redis).mget.apply(_a, keys).then(function (stringifiedDefinitions) {
var definitions = {};
names.forEach(function (name, idx) {
var definition = stringifiedDefinitions[idx];
definitions[name] = definition && JSON.parse(definition);
});
return Promise.resolve(definitions);
})
.catch(function (e) {
_this.log.error(LOG_PREFIX + "Could not grab feature flags due to an error: ".concat(e, "."));
return Promise.reject(e);
});
};
return DefinitionsCacheInRedis;
}(AbstractDefinitionsCacheAsync));
export { DefinitionsCacheInRedis };