UNPKG

featurehub-javascript-client-sdk

Version:
301 lines 14.6 kB
"use strict"; var __importDefault = (this && this.__importDefault) || function (mod) { return (mod && mod.__esModule) ? mod : { "default": mod }; }; Object.defineProperty(exports, "__esModule", { value: true }); exports.ApplyFeature = exports.MatcherRegistry = exports.Applied = exports.Murmur3PercentageCalculator = void 0; const models_1 = require("./models"); const semver_compare_1 = __importDefault(require("semver-compare")); const netmask_1 = require("netmask"); const murmurhash_1 = require("murmurhash"); class Murmur3PercentageCalculator { constructor() { this.MAX_PERCENTAGE = 1000000; } determineClientPercentage(percentageText, featureId) { const result = (0, murmurhash_1.v3)(percentageText + featureId, 0); return Math.floor(result / Math.pow(2, 32) * this.MAX_PERCENTAGE); } } exports.Murmur3PercentageCalculator = Murmur3PercentageCalculator; class Applied { constructor(matched, value) { this.matched = matched; this.value = value; } } exports.Applied = Applied; class FallthroughMatcher { match(suppliedValue, attr) { return false; } } class BooleanMatcher { match(suppliedValue, attr) { const val = 'true' === suppliedValue; const values = attr.values || []; if (attr.conditional === models_1.RolloutStrategyAttributeConditional.Equals) { return val === (values[0].toString() === 'true'); } if (attr.conditional === models_1.RolloutStrategyAttributeConditional.NotEquals) { return val !== (values[0].toString() === 'true'); } return false; } } class StringMatcher { match(suppliedValue, attr) { const vals = this.attrToStringValues(attr); switch (attr.conditional) { case models_1.RolloutStrategyAttributeConditional.Equals: return vals.findIndex((v) => v === suppliedValue) >= 0; case models_1.RolloutStrategyAttributeConditional.EndsWith: return vals.findIndex((v) => suppliedValue.endsWith(v)) >= 0; case models_1.RolloutStrategyAttributeConditional.StartsWith: return vals.findIndex((v) => suppliedValue.startsWith(v)) >= 0; case models_1.RolloutStrategyAttributeConditional.Greater: return vals.findIndex((v) => suppliedValue > v) >= 0; case models_1.RolloutStrategyAttributeConditional.GreaterEquals: return vals.findIndex((v) => suppliedValue >= v) >= 0; case models_1.RolloutStrategyAttributeConditional.Less: return vals.findIndex((v) => suppliedValue < v) >= 0; case models_1.RolloutStrategyAttributeConditional.LessEquals: return vals.findIndex((v) => suppliedValue <= v) >= 0; case models_1.RolloutStrategyAttributeConditional.NotEquals: return vals.findIndex((v) => v === suppliedValue) === -1; case models_1.RolloutStrategyAttributeConditional.Includes: return vals.findIndex((v) => suppliedValue.includes(v)) >= 0; case models_1.RolloutStrategyAttributeConditional.Excludes: return vals.findIndex((v) => suppliedValue.includes(v)) === -1; case models_1.RolloutStrategyAttributeConditional.Regex: return vals.findIndex((v) => suppliedValue.match(v)) >= 0; } return false; } attrToStringValues(attr) { return (attr.values || []).filter((v) => v != null).map((v) => v.toString()); } } class DateMatcher extends StringMatcher { match(suppliedValue, attr) { try { const parsedDate = new Date(suppliedValue); if (parsedDate == null) { return false; } return super.match(parsedDate.toISOString().substring(0, 10), attr); } catch (_) { return false; } } attrToStringValues(attr) { return (attr.values || []).filter((v) => v != null) .map((v) => (v instanceof Date) ? v.toISOString().substring(0, 10) : v.toString()); } } class DateTimeMatcher extends StringMatcher { match(suppliedValue, attr) { try { const parsedDate = new Date(suppliedValue); if (parsedDate == null) { return false; } return super.match(parsedDate.toISOString().substring(0, 19) + 'Z', attr); } catch (_) { return false; } } attrToStringValues(attr) { return (attr.values || []).filter((v) => v != null) .map((v) => (v instanceof Date) ? (v.toISOString().substring(0, 19) + 'Z') : v.toString()); } } class NumberMatcher { match(suppliedValue, attr) { try { const isFloat = suppliedValue.indexOf('.') >= 0; const num = isFloat ? parseFloat(suppliedValue) : parseInt(suppliedValue, 10); const conv = (v) => isFloat ? parseFloat(v) : parseInt(v, 10); const vals = (attr.values || []).filter((v) => v != null).map((v) => v.toString()); switch (attr.conditional) { case models_1.RolloutStrategyAttributeConditional.Equals: return vals.findIndex((v) => conv(v) === num) >= 0; case models_1.RolloutStrategyAttributeConditional.EndsWith: return vals.findIndex((v) => suppliedValue.endsWith(v)) >= 0; case models_1.RolloutStrategyAttributeConditional.StartsWith: return vals.findIndex((v) => suppliedValue.startsWith(v)) >= 0; case models_1.RolloutStrategyAttributeConditional.Greater: return vals.findIndex((v) => num > conv(v)) >= 0; case models_1.RolloutStrategyAttributeConditional.GreaterEquals: return vals.findIndex((v) => num >= conv(v)) >= 0; case models_1.RolloutStrategyAttributeConditional.Less: return vals.findIndex((v) => num < conv(v)) >= 0; case models_1.RolloutStrategyAttributeConditional.LessEquals: return vals.findIndex((v) => num <= conv(v)) >= 0; case models_1.RolloutStrategyAttributeConditional.NotEquals: return vals.findIndex((v) => conv(v) === num) === -1; case models_1.RolloutStrategyAttributeConditional.Includes: return vals.findIndex((v) => suppliedValue.includes(v)) >= 0; case models_1.RolloutStrategyAttributeConditional.Excludes: return vals.findIndex((v) => suppliedValue.includes(v)) === -1; case models_1.RolloutStrategyAttributeConditional.Regex: return vals.findIndex((v) => suppliedValue.match(v)) >= 0; } } catch (_) { return false; } return false; } } class SemanticVersionMatcher { match(suppliedValue, attr) { const vals = (attr.values || []).filter((v) => v != null).map((v) => v.toString()); switch (attr.conditional) { case models_1.RolloutStrategyAttributeConditional.Includes: case models_1.RolloutStrategyAttributeConditional.Equals: return vals.findIndex((v) => (0, semver_compare_1.default)(suppliedValue, v) === 0) >= 0; case models_1.RolloutStrategyAttributeConditional.EndsWith: break; case models_1.RolloutStrategyAttributeConditional.StartsWith: break; case models_1.RolloutStrategyAttributeConditional.Greater: return vals.findIndex((v) => (0, semver_compare_1.default)(suppliedValue, v) > 0) >= 0; case models_1.RolloutStrategyAttributeConditional.GreaterEquals: return vals.findIndex((v) => (0, semver_compare_1.default)(suppliedValue, v) >= 0) >= 0; case models_1.RolloutStrategyAttributeConditional.Less: return vals.findIndex((v) => (0, semver_compare_1.default)(suppliedValue, v) < 0) >= 0; case models_1.RolloutStrategyAttributeConditional.LessEquals: return vals.findIndex((v) => (0, semver_compare_1.default)(suppliedValue, v) <= 0) >= 0; case models_1.RolloutStrategyAttributeConditional.NotEquals: case models_1.RolloutStrategyAttributeConditional.Excludes: return vals.findIndex((v) => (0, semver_compare_1.default)(suppliedValue, v) !== 0) >= 0; case models_1.RolloutStrategyAttributeConditional.Regex: break; } return false; } } class IPNetworkMatcher { match(ip, attr) { const vals = (attr.values || []).filter((v) => v != null); switch (attr.conditional) { case models_1.RolloutStrategyAttributeConditional.Equals: case models_1.RolloutStrategyAttributeConditional.Includes: return vals.findIndex((v) => new netmask_1.Netmask(v).contains(ip)) >= 0; case models_1.RolloutStrategyAttributeConditional.NotEquals: case models_1.RolloutStrategyAttributeConditional.Excludes: return vals.findIndex((v) => new netmask_1.Netmask(v).contains(ip)) === -1; } return false; } } class MatcherRegistry { findMatcher(attr) { switch (attr === null || attr === void 0 ? void 0 : attr.type) { case models_1.RolloutStrategyFieldType.String: return new StringMatcher(); case models_1.RolloutStrategyFieldType.SemanticVersion: return new SemanticVersionMatcher(); case models_1.RolloutStrategyFieldType.Number: return new NumberMatcher(); case models_1.RolloutStrategyFieldType.Date: return new DateMatcher(); case models_1.RolloutStrategyFieldType.Datetime: return new DateTimeMatcher(); case models_1.RolloutStrategyFieldType.Boolean: return new BooleanMatcher(); case models_1.RolloutStrategyFieldType.IpAddress: return new IPNetworkMatcher(); } return new FallthroughMatcher(); } } exports.MatcherRegistry = MatcherRegistry; class ApplyFeature { constructor(percentageCalculator, matcherRepository) { this._percentageCalculator = percentageCalculator || new Murmur3PercentageCalculator(); this._matcherRepository = matcherRepository || new MatcherRegistry(); } apply(strategies = [], key, featureValueId, context) { var _a, _b; if (context !== undefined && strategies.length) { let percentage = null; let percentageKey = null; const basePercentage = new Map(); const defaultPercentageKey = context.defaultPercentageKey(); for (const rsi of strategies) { if (rsi.percentage !== 0 && (defaultPercentageKey || ((_a = rsi.percentageAttributes) === null || _a === void 0 ? void 0 : _a.length))) { const newPercentageKey = ApplyFeature.determinePercentageKey(context, rsi.percentageAttributes); if (!basePercentage.has(newPercentageKey)) { basePercentage.set(newPercentageKey, 0); } const basePercentageVal = basePercentage.get(newPercentageKey); if (percentage === null || newPercentageKey !== percentageKey) { percentageKey = newPercentageKey; percentage = this._percentageCalculator.determineClientPercentage(percentageKey, featureValueId); } const useBasePercentage = (rsi.attributes === undefined || rsi.attributes.length === 0) ? basePercentageVal : 0; if (percentage <= (useBasePercentage + rsi.percentage)) { if (rsi.attributes != null && rsi.attributes.length) { if (this.matchAttribute(context, rsi)) { return new Applied(true, rsi.value); } } else { return new Applied(true, rsi.value); } } if ((_b = rsi.attributes) === null || _b === void 0 ? void 0 : _b.length) { basePercentage.set(percentageKey, basePercentage.get(percentageKey) + rsi.percentage); } } if ((rsi.percentage === 0 || rsi.percentage === undefined) && rsi.attributes !== undefined && rsi.attributes.length > 0 && this.matchAttribute(context, rsi)) { return new Applied(true, rsi.value); } } } return new Applied(false, null); } static determinePercentageKey(context, percentageAttributes) { if (!percentageAttributes || percentageAttributes.length === 0) { return context.defaultPercentageKey(); } return percentageAttributes.map((pa) => context.getAttr(pa, '<none>')).join('$'); } matchAttribute(context, rsi) { for (const attr of (rsi.attributes || [])) { let suppliedValues = context.getAttrs(attr.fieldName); if (suppliedValues.length == 0 && attr.fieldName.toLowerCase() === 'now') { switch (attr.type) { case models_1.RolloutStrategyFieldType.Date: suppliedValues = [new Date().toISOString().substring(0, 10)]; break; case models_1.RolloutStrategyFieldType.Datetime: suppliedValues = [new Date().toISOString()]; break; } } if (attr.values == null && suppliedValues.length == 0) { if (attr.conditional !== models_1.RolloutStrategyAttributeConditional.Equals) { return false; } continue; } if (attr.values == null || suppliedValues.length == 0) { return false; } const match = suppliedValues.find(sv => this._matcherRepository.findMatcher(attr).match(sv, attr)); if (!match) { return false; } } return true; } } exports.ApplyFeature = ApplyFeature; //# sourceMappingURL=strategy_matcher.js.map