/**
* @summary rotations
* @version 3.0.0
* @since 2.0.0
* @author Arian Khosravi <arian.khosravi@aofl.com>
*/
import {CacheManager, cacheTypeEnumerate} from '@aofl/cache-manager';
/**
* @memberof module:@aofl/rotations
* @private
* @type {Number}
*/
const EXPIRE_90_DAYS = 7776000000;
/**
* @memberof module:@aofl/rotations
* @private
* @type {RegExp}
*/
const TRAILING_SLASH_REGEX = /\/$/i;
/**
* @memberof module:@aofl/rotations
* @private
* @generator
* @function arrayIterator
* @yields {*} next next item in array
*/
const arrayIterator = function* arrayIterator(arr) {
yield* arr;
};
/**
* Produces an updated route config based on a rotation config
*
* @memberof module:@aofl/rotations
*/
class Rotations {
/**
* @param {String} cacheNamespace
* @param {Object} routesConfig
* @param {Object} rotationConfig
* @param {Object} rotationConditions
* @param {Object} publicPath
*/
constructor(cacheNamespace, routesConfig, rotationConfig, rotationConditions, publicPath = '/', expires = EXPIRE_90_DAYS) {
this.routesConfig = routesConfig;
this.rotationConfig = rotationConfig;
this.rotationConditions = rotationConditions;
this.cache = new CacheManager(cacheNamespace, cacheTypeEnumerate.LOCAL, expires);
this.qualification = {};
this.weightRanges = {};
this.qualifiedVersions = {};
this.PUBLIC_PATH = publicPath.replace(TRAILING_SLASH_REGEX, ''); // eslint-disable-line
this.PUBLIC_PATH_REGEX = new RegExp(`^${this.PUBLIC_PATH}`);
}
/**
* @private
* @param {String} conditionId
*/
async qualifies(conditionId) {
if (typeof this.qualification[conditionId] !== 'undefined') {
return this.qualification[conditionId];
}
const conditionName = this.rotationConfig.conditions[conditionId];
if (typeof this.rotationConditions[conditionName] === 'function') {
const qualifies = Promise.resolve(await this.rotationConditions[conditionName]());
this.qualification[conditionId] = qualifies;
return qualifies;
}
return false;
}
/**
*
* @private
* @param {String} qualificationOrder
* @return {Promise}
*/
getQualifyingId(qualificationOrder) {
const qualificationIterator = arrayIterator(qualificationOrder);
const qualify = async () => {
const nextQ = qualificationIterator.next();
if (nextQ.done) {
throw new Error('No matching conditions');
}
const conditionId = nextQ.value;
const qualifies = await this.qualifies(conditionId);
if (qualifies) {
return conditionId;
}
return qualify();
};
return qualify();
}
/**
* @param {String} qualificationId
* @private
* @return {Array}
*/
getWeightRange(qualificationId) {
const weights = this.rotationConfig.weights[qualificationId];
if (typeof weights === 'undefined') {
throw new Error('Invalid weights for qualifying rotation');
}
const weightRanges = this.weightRanges[qualificationId];
if (typeof weightRanges !== 'undefined') {
return weightRanges;
}
const range = [];
for (const key in weights) {
/* istanbul ignore next */
if (!Object.hasOwnProperty.call(weights, key)) continue;
let i = 0;
while (i++ < weights[key]) {
range.push(key);
}
}
this.weightRanges[qualificationId] = range;
return range;
}
/**
* @param {String} qualificationId
* @private
* @return {String}
*/
getVersion(qualificationId) {
if (typeof this.qualifiedVersions[qualificationId] !== 'undefined') {
return this.qualifiedVersions[qualificationId];
}
const weights = this.getWeightRange(qualificationId);
const version = weights[Math.floor(Math.random() * Math.floor(weights.length))];
this.qualifiedVersions[qualificationId] = version;
return version;
}
/**
* @param {String} rotation
* @param {String} path
* @private
* @return {Object}
*/
findRotationRoute(rotation, path) {
const routes = this.routesConfig[rotation];
if (typeof routes === 'undefined') {
throw new Error('Rotation not found');
}
for (let i = 0; i < routes.length; i++) {
if (routes[i].path === path) {
return routes[i];
}
}
throw new Error('Rotation route not found');
}
/**
* @param {String} path
* @private
* @return {Object}
*/
getCachedRotation(path) {
const cachedResult = this.cache.getItem(path);
if (cachedResult === null) {
return null;
}
const qualifyingId = cachedResult.qualifyingId;
const version = cachedResult.version;
const weights = this.rotationConfig.weights[qualifyingId];
const rotation = this.rotationConfig.versions[version];
if (typeof weights !== 'undefined' && typeof rotation !== 'undefined' &&
typeof this.routesConfig[rotation] !== 'undefined') {
try {
this.findRotationRoute(rotation, path);
return {
qualifyingId,
version
};
} catch (e) {}
}
return null;
}
/**
*
* @param {String} mainRoutes
* @return {Promise} resolves to a route configuration Array of route objects
*/
async getRoutes(mainRoutes = 'routes') {
const routes = [];
const routesIterator = arrayIterator(this.routesConfig[mainRoutes]);
const qualifyRoutes = async () => {
const next = routesIterator.next();
if (next.done) {
return routes;
}
const route = next.value;
const routePath = route.path.replace(this.PUBLIC_PATH_REGEX, '').replace(TRAILING_SLASH_REGEX, '');
const qualificationOrder = this.rotationConfig.qualification_order[routePath] ||
this.rotationConfig.qualification_order[routePath + '/'];
try {
/* istanbul ignore next */
if (typeof qualificationOrder === 'undefined') {
throw new Error('No qualification order for giver route');
}
let qualifyingId = 0;
let version = '';
const cachedRotation = this.getCachedRotation(route.path);
if (cachedRotation !== null) {
qualifyingId = cachedRotation.qualifyingId;
version = cachedRotation.version;
} else {
qualifyingId = await this.getQualifyingId(qualificationOrder);
version = this.getVersion(qualifyingId);
}
const rotation = this.rotationConfig.versions[version];
if (typeof rotation === 'undefined') {
throw new Error('Version does not exist');
}
const rotationInfo = {qualifyingId, version};
const matchedRoute = this.findRotationRoute(rotation, route.path);
matchedRoute.rotationInfo = rotationInfo;
routes.push(matchedRoute);
if (cachedRotation === null) {
this.cache.setItem(route.path, {qualifyingId, version});
}
} catch (e) { // no qualifying rotation
route.rotationInfo = {version: this.rotationConfig.baseline_version};
routes.push(route);
this.cache.removeItem(route.path);
}
await qualifyRoutes();
};
await qualifyRoutes();
return routes;
}
}
export {
Rotations
};