featurehub-javascript-client-sdk
Version:
FeatureHub client/browser SDK
301 lines • 14.6 kB
JavaScript
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
;