@amplitude/experiment-core
Version:
Amplitude Experiment evaluation JavaScript implementation.
1,129 lines (1,114 loc) • 39.4 kB
JavaScript
const EvaluationOperator = {
IS: 'is',
IS_NOT: 'is not',
CONTAINS: 'contains',
DOES_NOT_CONTAIN: 'does not contain',
LESS_THAN: 'less',
LESS_THAN_EQUALS: 'less or equal',
GREATER_THAN: 'greater',
GREATER_THAN_EQUALS: 'greater or equal',
VERSION_LESS_THAN: 'version less',
VERSION_LESS_THAN_EQUALS: 'version less or equal',
VERSION_GREATER_THAN: 'version greater',
VERSION_GREATER_THAN_EQUALS: 'version greater or equal',
SET_IS: 'set is',
SET_IS_NOT: 'set is not',
SET_CONTAINS: 'set contains',
SET_DOES_NOT_CONTAIN: 'set does not contain',
SET_CONTAINS_ANY: 'set contains any',
SET_DOES_NOT_CONTAIN_ANY: 'set does not contain any',
REGEX_MATCH: 'regex match',
REGEX_DOES_NOT_MATCH: 'regex does not match',
};
const stringToUtf8ByteArray = (str) => {
const out = [];
let p = 0;
for (let i = 0; i < str.length; i++) {
let c = str.charCodeAt(i);
if (c < 128) {
out[p++] = c;
}
else if (c < 2048) {
out[p++] = (c >> 6) | 192;
out[p++] = (c & 63) | 128;
}
else if ((c & 0xfc00) == 0xd800 &&
i + 1 < str.length &&
(str.charCodeAt(i + 1) & 0xfc00) == 0xdc00) {
// Surrogate Pair
c = 0x10000 + ((c & 0x03ff) << 10) + (str.charCodeAt(++i) & 0x03ff);
out[p++] = (c >> 18) | 240;
out[p++] = ((c >> 12) & 63) | 128;
out[p++] = ((c >> 6) & 63) | 128;
out[p++] = (c & 63) | 128;
}
else {
out[p++] = (c >> 12) | 224;
out[p++] = ((c >> 6) & 63) | 128;
out[p++] = (c & 63) | 128;
}
}
return Uint8Array.from(out);
};
const C1_32 = -0x3361d2af;
const C2_32 = 0x1b873593;
const R1_32 = 15;
const R2_32 = 13;
const M_32 = 5;
const N_32 = -0x19ab949c;
const hash32x86 = (input, seed = 0) => {
const data = stringToUtf8ByteArray(input);
const length = data.length;
const nBlocks = length >> 2;
let hash = seed;
// body
for (let i = 0; i < nBlocks; i++) {
const index = i << 2;
const k = readIntLe(data, index);
hash = mix32(k, hash);
}
// tail
const index = nBlocks << 2;
let k1 = 0;
switch (length - index) {
case 3:
k1 ^= data[index + 2] << 16;
k1 ^= data[index + 1] << 8;
k1 ^= data[index];
k1 = Math.imul(k1, C1_32);
k1 = rotateLeft(k1, R1_32);
k1 = Math.imul(k1, C2_32);
hash ^= k1;
break;
case 2:
k1 ^= data[index + 1] << 8;
k1 ^= data[index];
k1 = Math.imul(k1, C1_32);
k1 = rotateLeft(k1, R1_32);
k1 = Math.imul(k1, C2_32);
hash ^= k1;
break;
case 1:
k1 ^= data[index];
k1 = Math.imul(k1, C1_32);
k1 = rotateLeft(k1, R1_32);
k1 = Math.imul(k1, C2_32);
hash ^= k1;
break;
}
hash ^= length;
return fmix32(hash) >>> 0;
};
const mix32 = (k, hash) => {
let kResult = k;
let hashResult = hash;
kResult = Math.imul(kResult, C1_32);
kResult = rotateLeft(kResult, R1_32);
kResult = Math.imul(kResult, C2_32);
hashResult ^= kResult;
hashResult = rotateLeft(hashResult, R2_32);
hashResult = Math.imul(hashResult, M_32);
return (hashResult + N_32) | 0;
};
const fmix32 = (hash) => {
let hashResult = hash;
hashResult ^= hashResult >>> 16;
hashResult = Math.imul(hashResult, -0x7a143595);
hashResult ^= hashResult >>> 13;
hashResult = Math.imul(hashResult, -0x3d4d51cb);
hashResult ^= hashResult >>> 16;
return hashResult;
};
const rotateLeft = (x, n, width = 32) => {
if (n > width)
n = n % width;
const mask = (0xffffffff << (width - n)) >>> 0;
const r = (((x & mask) >>> 0) >>> (width - n)) >>> 0;
return ((x << n) | r) >>> 0;
};
const readIntLe = (data, index = 0) => {
const n = (data[index] << 24) |
(data[index + 1] << 16) |
(data[index + 2] << 8) |
data[index + 3];
return reverseBytes(n);
};
const reverseBytes = (n) => {
return (((n & -0x1000000) >>> 24) |
((n & 0x00ff0000) >>> 8) |
((n & 0x0000ff00) << 8) |
((n & 0x000000ff) << 24));
};
const select = (selectable, selector) => {
if (!selector || selector.length === 0) {
return undefined;
}
for (const selectorElement of selector) {
if (!selectorElement || !selectable || typeof selectable !== 'object') {
return undefined;
}
selectable = selectable[selectorElement];
}
if (selectable === undefined || selectable === null) {
return undefined;
}
else {
return selectable;
}
};
// major and minor should be non-negative numbers separated by a dot
const MAJOR_MINOR_REGEX = '(\\d+)\\.(\\d+)';
// patch should be a non-negative number
const PATCH_REGEX = '(\\d+)';
// prerelease is optional. If provided, it should be a hyphen followed by a
// series of dot separated identifiers where an identifer can contain anything in [-0-9a-zA-Z]
const PRERELEASE_REGEX = '(-(([-\\w]+\\.?)*))?';
// version pattern should be major.minor(.patchAndPreRelease) where .patchAndPreRelease is optional
const VERSION_PATTERN = `^${MAJOR_MINOR_REGEX}(\\.${PATCH_REGEX}${PRERELEASE_REGEX})?$`;
class SemanticVersion {
constructor(major, minor, patch, preRelease = undefined) {
this.major = major;
this.minor = minor;
this.patch = patch;
this.preRelease = preRelease;
}
static parse(version) {
if (!version) {
return undefined;
}
const matchGroup = new RegExp(VERSION_PATTERN).exec(version);
if (!matchGroup) {
return undefined;
}
const major = Number(matchGroup[1]);
const minor = Number(matchGroup[2]);
if (isNaN(major) || isNaN(minor)) {
return undefined;
}
const patch = Number(matchGroup[4]) || 0;
const preRelease = matchGroup[5] || undefined;
return new SemanticVersion(major, minor, patch, preRelease);
}
compareTo(other) {
if (this.major > other.major)
return 1;
if (this.major < other.major)
return -1;
if (this.minor > other.minor)
return 1;
if (this.minor < other.minor)
return -1;
if (this.patch > other.patch)
return 1;
if (this.patch < other.patch)
return -1;
if (this.preRelease && !other.preRelease)
return -1;
if (!this.preRelease && other.preRelease)
return 1;
if (this.preRelease && other.preRelease) {
if (this.preRelease > other.preRelease)
return 1;
if (this.preRelease < other.preRelease)
return -1;
return 0;
}
return 0;
}
}
class EvaluationEngine {
evaluate(context, flags) {
const results = {};
const target = {
context: context,
result: results,
};
for (const flag of flags) {
// Evaluate flag and update results.
const variant = this.evaluateFlag(target, flag);
if (variant) {
results[flag.key] = variant;
}
}
return results;
}
evaluateFlag(target, flag) {
let result;
for (const segment of flag.segments) {
result = this.evaluateSegment(target, flag, segment);
if (result) {
// Merge all metadata into the result
const metadata = Object.assign(Object.assign(Object.assign({}, flag.metadata), segment.metadata), result.metadata);
result = Object.assign(Object.assign({}, result), { metadata: metadata });
break;
}
}
return result;
}
evaluateSegment(target, flag, segment) {
if (!segment.conditions) {
// Null conditions always match
const variantKey = this.bucket(target, segment);
if (variantKey !== undefined) {
return flag.variants[variantKey];
}
else {
return undefined;
}
}
const match = this.evaluateConditions(target, segment.conditions);
// On match, bucket the user.
if (match) {
const variantKey = this.bucket(target, segment);
if (variantKey !== undefined) {
return flag.variants[variantKey];
}
else {
return undefined;
}
}
return undefined;
}
evaluateConditions(target, conditions) {
// Outer list logic is "or" (||)
for (const innerConditions of conditions) {
let match = true;
for (const condition of innerConditions) {
match = this.matchCondition(target, condition);
if (!match) {
break;
}
}
if (match) {
return true;
}
}
return false;
}
matchCondition(target, condition) {
const propValue = select(target, condition.selector);
// We need special matching for null properties and set type prop values
// and operators. All other values are matched as strings, since the
// filter values are always strings.
if (propValue === undefined || propValue === null) {
return this.matchNull(condition.op, condition.values);
}
else if (this.isSetOperator(condition.op)) {
const propValueStringList = this.coerceStringArray(propValue);
if (!propValueStringList) {
return false;
}
return this.matchSet(propValueStringList, condition.op, condition.values);
}
else {
const propValueString = this.coerceString(propValue);
if (propValueString !== undefined) {
return this.matchString(propValueString, condition.op, condition.values);
}
else {
return false;
}
}
}
getHash(key) {
return hash32x86(key);
}
bucket(target, segment) {
if (!segment.bucket) {
// A null bucket means the segment is fully rolled out. Select the
// default variant.
return segment.variant;
}
// Select the bucketing value.
const bucketingValue = this.coerceString(select(target, segment.bucket.selector));
if (!bucketingValue || bucketingValue.length === 0) {
// A null or empty bucketing value cannot be bucketed. Select the
// default variant.
return segment.variant;
}
// Salt and has the value, and compute the allocation and distribution
// values.
const keyToHash = `${segment.bucket.salt}/${bucketingValue}`;
const hash = this.getHash(keyToHash);
const allocationValue = hash % 100;
const distributionValue = Math.floor(hash / 100);
for (const allocation of segment.bucket.allocations) {
const allocationStart = allocation.range[0];
const allocationEnd = allocation.range[1];
if (allocationValue >= allocationStart &&
allocationValue < allocationEnd) {
for (const distribution of allocation.distributions) {
const distributionStart = distribution.range[0];
const distributionEnd = distribution.range[1];
if (distributionValue >= distributionStart &&
distributionValue < distributionEnd) {
return distribution.variant;
}
}
}
}
return segment.variant;
}
matchNull(op, filterValues) {
const containsNone = this.containsNone(filterValues);
switch (op) {
case EvaluationOperator.IS:
case EvaluationOperator.CONTAINS:
case EvaluationOperator.LESS_THAN:
case EvaluationOperator.LESS_THAN_EQUALS:
case EvaluationOperator.GREATER_THAN:
case EvaluationOperator.GREATER_THAN_EQUALS:
case EvaluationOperator.VERSION_LESS_THAN:
case EvaluationOperator.VERSION_LESS_THAN_EQUALS:
case EvaluationOperator.VERSION_GREATER_THAN:
case EvaluationOperator.VERSION_GREATER_THAN_EQUALS:
case EvaluationOperator.SET_IS:
case EvaluationOperator.SET_CONTAINS:
case EvaluationOperator.SET_CONTAINS_ANY:
return containsNone;
case EvaluationOperator.IS_NOT:
case EvaluationOperator.DOES_NOT_CONTAIN:
case EvaluationOperator.SET_DOES_NOT_CONTAIN:
case EvaluationOperator.SET_DOES_NOT_CONTAIN_ANY:
return !containsNone;
default:
return false;
}
}
matchSet(propValues, op, filterValues) {
switch (op) {
case EvaluationOperator.SET_IS:
return this.setEquals(propValues, filterValues);
case EvaluationOperator.SET_IS_NOT:
return !this.setEquals(propValues, filterValues);
case EvaluationOperator.SET_CONTAINS:
return this.matchesSetContainsAll(propValues, filterValues);
case EvaluationOperator.SET_DOES_NOT_CONTAIN:
return !this.matchesSetContainsAll(propValues, filterValues);
case EvaluationOperator.SET_CONTAINS_ANY:
return this.matchesSetContainsAny(propValues, filterValues);
case EvaluationOperator.SET_DOES_NOT_CONTAIN_ANY:
return !this.matchesSetContainsAny(propValues, filterValues);
default:
return false;
}
}
matchString(propValue, op, filterValues) {
switch (op) {
case EvaluationOperator.IS:
return this.matchesIs(propValue, filterValues);
case EvaluationOperator.IS_NOT:
return !this.matchesIs(propValue, filterValues);
case EvaluationOperator.CONTAINS:
return this.matchesContains(propValue, filterValues);
case EvaluationOperator.DOES_NOT_CONTAIN:
return !this.matchesContains(propValue, filterValues);
case EvaluationOperator.LESS_THAN:
case EvaluationOperator.LESS_THAN_EQUALS:
case EvaluationOperator.GREATER_THAN:
case EvaluationOperator.GREATER_THAN_EQUALS:
return this.matchesComparable(propValue, op, filterValues, (value) => this.parseNumber(value), this.comparator);
case EvaluationOperator.VERSION_LESS_THAN:
case EvaluationOperator.VERSION_LESS_THAN_EQUALS:
case EvaluationOperator.VERSION_GREATER_THAN:
case EvaluationOperator.VERSION_GREATER_THAN_EQUALS:
return this.matchesComparable(propValue, op, filterValues, (value) => SemanticVersion.parse(value), this.versionComparator);
case EvaluationOperator.REGEX_MATCH:
return this.matchesRegex(propValue, filterValues);
case EvaluationOperator.REGEX_DOES_NOT_MATCH:
return !this.matchesRegex(propValue, filterValues);
default:
return false;
}
}
matchesIs(propValue, filterValues) {
if (this.containsBooleans(filterValues)) {
const lower = propValue.toLowerCase();
if (lower === 'true' || lower === 'false') {
return filterValues.some((value) => value.toLowerCase() === lower);
}
}
return filterValues.some((value) => propValue === value);
}
matchesContains(propValue, filterValues) {
for (const filterValue of filterValues) {
if (propValue.toLowerCase().includes(filterValue.toLowerCase())) {
return true;
}
}
return false;
}
matchesComparable(propValue, op, filterValues, typeTransformer, typeComparator) {
const propValueTransformed = typeTransformer(propValue);
const filterValuesTransformed = filterValues
.map((filterValue) => {
return typeTransformer(filterValue);
})
.filter((filterValue) => {
return filterValue !== undefined;
});
if (propValueTransformed === undefined ||
filterValuesTransformed.length === 0) {
return filterValues.some((filterValue) => {
return this.comparator(propValue, op, filterValue);
});
}
else {
return filterValuesTransformed.some((filterValueTransformed) => {
return typeComparator(propValueTransformed, op, filterValueTransformed);
});
}
}
comparator(propValue, op, filterValue) {
switch (op) {
case EvaluationOperator.LESS_THAN:
case EvaluationOperator.VERSION_LESS_THAN:
return propValue < filterValue;
case EvaluationOperator.LESS_THAN_EQUALS:
case EvaluationOperator.VERSION_LESS_THAN_EQUALS:
return propValue <= filterValue;
case EvaluationOperator.GREATER_THAN:
case EvaluationOperator.VERSION_GREATER_THAN:
return propValue > filterValue;
case EvaluationOperator.GREATER_THAN_EQUALS:
case EvaluationOperator.VERSION_GREATER_THAN_EQUALS:
return propValue >= filterValue;
default:
return false;
}
}
versionComparator(propValue, op, filterValue) {
const compareTo = propValue.compareTo(filterValue);
switch (op) {
case EvaluationOperator.LESS_THAN:
case EvaluationOperator.VERSION_LESS_THAN:
return compareTo < 0;
case EvaluationOperator.LESS_THAN_EQUALS:
case EvaluationOperator.VERSION_LESS_THAN_EQUALS:
return compareTo <= 0;
case EvaluationOperator.GREATER_THAN:
case EvaluationOperator.VERSION_GREATER_THAN:
return compareTo > 0;
case EvaluationOperator.GREATER_THAN_EQUALS:
case EvaluationOperator.VERSION_GREATER_THAN_EQUALS:
return compareTo >= 0;
default:
return false;
}
}
matchesRegex(propValue, filterValues) {
return filterValues.some((filterValue) => Boolean(new RegExp(filterValue).exec(propValue)));
}
containsNone(filterValues) {
return filterValues.some((filterValue) => {
return filterValue === '(none)';
});
}
containsBooleans(filterValues) {
return filterValues.some((filterValue) => {
switch (filterValue.toLowerCase()) {
case 'true':
case 'false':
return true;
default:
return false;
}
});
}
parseNumber(value) {
var _a;
return (_a = Number(value)) !== null && _a !== void 0 ? _a : undefined;
}
coerceString(value) {
if (value === undefined || value === null) {
return undefined;
}
if (typeof value === 'object') {
return JSON.stringify(value);
}
return String(value);
}
coerceStringArray(value) {
if (Array.isArray(value)) {
const anyArray = value;
return anyArray
.map((e) => this.coerceString(e))
.filter(Boolean);
}
const stringValue = String(value);
try {
const parsedValue = JSON.parse(stringValue);
if (Array.isArray(parsedValue)) {
const anyArray = value;
return anyArray
.map((e) => this.coerceString(e))
.filter(Boolean);
}
else {
const s = this.coerceString(stringValue);
return s ? [s] : undefined;
}
}
catch (_a) {
const s = this.coerceString(stringValue);
return s ? [s] : undefined;
}
}
isSetOperator(op) {
switch (op) {
case EvaluationOperator.SET_IS:
case EvaluationOperator.SET_IS_NOT:
case EvaluationOperator.SET_CONTAINS:
case EvaluationOperator.SET_DOES_NOT_CONTAIN:
case EvaluationOperator.SET_CONTAINS_ANY:
case EvaluationOperator.SET_DOES_NOT_CONTAIN_ANY:
return true;
default:
return false;
}
}
setEquals(xa, ya) {
const xs = new Set(xa);
const ys = new Set(ya);
return xs.size === ys.size && [...ys].every((y) => xs.has(y));
}
matchesSetContainsAll(propValues, filterValues) {
if (propValues.length < filterValues.length) {
return false;
}
for (const filterValue of filterValues) {
if (!this.matchesIs(filterValue, propValues)) {
return false;
}
}
return true;
}
matchesSetContainsAny(propValues, filterValues) {
for (const filterValue of filterValues) {
if (this.matchesIs(filterValue, propValues)) {
return true;
}
}
return false;
}
}
const topologicalSort = (flags, flagKeys) => {
const available = Object.assign({}, flags);
const result = [];
const startingKeys = flagKeys || Object.keys(available);
for (const flagKey of startingKeys) {
const traversal = parentTraversal(flagKey, available);
if (traversal) {
result.push(...traversal);
}
}
return result;
};
const parentTraversal = (flagKey, available, path = []) => {
const flag = available[flagKey];
if (!flag) {
return undefined;
}
else if (!flag.dependencies || flag.dependencies.length === 0) {
delete available[flag.key];
return [flag];
}
path.push(flag.key);
const result = [];
for (const parentKey of flag.dependencies) {
if (path.some((p) => p === parentKey)) {
throw Error(`Detected a cycle between flags ${path}`);
}
const traversal = parentTraversal(parentKey, available, path);
if (traversal) {
result.push(...traversal);
}
}
result.push(flag);
path.pop();
delete available[flag.key];
return result;
};
function __awaiter(thisArg, _arguments, P, generator) {
function adopt(value) {
return value instanceof P ? value : new P(function (resolve) {
resolve(value);
});
}
return new (P || (P = Promise))(function (resolve, reject) {
function fulfilled(value) {
try {
step(generator.next(value));
} catch (e) {
reject(e);
}
}
function rejected(value) {
try {
step(generator["throw"](value));
} catch (e) {
reject(e);
}
}
function step(result) {
result.done ? resolve(result.value) : adopt(result.value).then(fulfilled, rejected);
}
step((generator = generator.apply(thisArg, _arguments || [])).next());
});
}
typeof SuppressedError === "function" ? SuppressedError : function (error, suppressed, message) {
var e = new Error(message);
return e.name = "SuppressedError", e.error = error, e.suppressed = suppressed, e;
};
/**
* base64.ts
*
* Licensed under the BSD 3-Clause License.
* http://opensource.org/licenses/BSD-3-Clause
*
* References:
* http://en.wikipedia.org/wiki/Base64
*
* @author Dan Kogai (https://github.com/dankogai)
*/
var version = '3.7.7';
/**
* @deprecated use lowercase `version`.
*/
var VERSION = version;
var _hasBuffer = typeof Buffer === 'function';
var _TD = typeof TextDecoder === 'function' ? new TextDecoder() : undefined;
var _TE = typeof TextEncoder === 'function' ? new TextEncoder() : undefined;
var b64ch = 'ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/=';
var b64chs = Array.prototype.slice.call(b64ch);
var b64tab = function (a) {
var tab = {};
a.forEach(function (c, i) {
return tab[c] = i;
});
return tab;
}(b64chs);
var b64re = /^(?:[A-Za-z\d+\/]{4})*?(?:[A-Za-z\d+\/]{2}(?:==)?|[A-Za-z\d+\/]{3}=?)?$/;
var _fromCC = String.fromCharCode.bind(String);
var _U8Afrom = typeof Uint8Array.from === 'function' ? Uint8Array.from.bind(Uint8Array) : function (it) {
return new Uint8Array(Array.prototype.slice.call(it, 0));
};
var _mkUriSafe = function _mkUriSafe(src) {
return src.replace(/=/g, '').replace(/[+\/]/g, function (m0) {
return m0 == '+' ? '-' : '_';
});
};
var _tidyB64 = function _tidyB64(s) {
return s.replace(/[^A-Za-z0-9\+\/]/g, '');
};
/**
* polyfill version of `btoa`
*/
var btoaPolyfill = function btoaPolyfill(bin) {
// console.log('polyfilled');
var u32,
c0,
c1,
c2,
asc = '';
var pad = bin.length % 3;
for (var i = 0; i < bin.length;) {
if ((c0 = bin.charCodeAt(i++)) > 255 || (c1 = bin.charCodeAt(i++)) > 255 || (c2 = bin.charCodeAt(i++)) > 255) throw new TypeError('invalid character found');
u32 = c0 << 16 | c1 << 8 | c2;
asc += b64chs[u32 >> 18 & 63] + b64chs[u32 >> 12 & 63] + b64chs[u32 >> 6 & 63] + b64chs[u32 & 63];
}
return pad ? asc.slice(0, pad - 3) + "===".substring(pad) : asc;
};
/**
* does what `window.btoa` of web browsers do.
* @param {String} bin binary string
* @returns {string} Base64-encoded string
*/
var _btoa = typeof btoa === 'function' ? function (bin) {
return btoa(bin);
} : _hasBuffer ? function (bin) {
return Buffer.from(bin, 'binary').toString('base64');
} : btoaPolyfill;
var _fromUint8Array = _hasBuffer ? function (u8a) {
return Buffer.from(u8a).toString('base64');
} : function (u8a) {
// cf. https://stackoverflow.com/questions/12710001/how-to-convert-uint8-array-to-base64-encoded-string/12713326#12713326
var maxargs = 0x1000;
var strs = [];
for (var i = 0, l = u8a.length; i < l; i += maxargs) {
strs.push(_fromCC.apply(null, u8a.subarray(i, i + maxargs)));
}
return _btoa(strs.join(''));
};
/**
* converts a Uint8Array to a Base64 string.
* @param {boolean} [urlsafe] URL-and-filename-safe a la RFC4648 §5
* @returns {string} Base64 string
*/
var fromUint8Array = function fromUint8Array(u8a) {
var urlsafe = arguments.length > 1 && arguments[1] !== undefined ? arguments[1] : false;
return urlsafe ? _mkUriSafe(_fromUint8Array(u8a)) : _fromUint8Array(u8a);
};
// This trick is found broken https://github.com/dankogai/js-base64/issues/130
// const utob = (src: string) => unescape(encodeURIComponent(src));
// reverting good old fationed regexp
var cb_utob = function cb_utob(c) {
if (c.length < 2) {
var cc = c.charCodeAt(0);
return cc < 0x80 ? c : cc < 0x800 ? _fromCC(0xc0 | cc >>> 6) + _fromCC(0x80 | cc & 0x3f) : _fromCC(0xe0 | cc >>> 12 & 0x0f) + _fromCC(0x80 | cc >>> 6 & 0x3f) + _fromCC(0x80 | cc & 0x3f);
} else {
var cc = 0x10000 + (c.charCodeAt(0) - 0xD800) * 0x400 + (c.charCodeAt(1) - 0xDC00);
return _fromCC(0xf0 | cc >>> 18 & 0x07) + _fromCC(0x80 | cc >>> 12 & 0x3f) + _fromCC(0x80 | cc >>> 6 & 0x3f) + _fromCC(0x80 | cc & 0x3f);
}
};
var re_utob = /[\uD800-\uDBFF][\uDC00-\uDFFFF]|[^\x00-\x7F]/g;
/**
* @deprecated should have been internal use only.
* @param {string} src UTF-8 string
* @returns {string} UTF-16 string
*/
var utob = function utob(u) {
return u.replace(re_utob, cb_utob);
};
//
var _encode = _hasBuffer ? function (s) {
return Buffer.from(s, 'utf8').toString('base64');
} : _TE ? function (s) {
return _fromUint8Array(_TE.encode(s));
} : function (s) {
return _btoa(utob(s));
};
/**
* converts a UTF-8-encoded string to a Base64 string.
* @param {boolean} [urlsafe] if `true` make the result URL-safe
* @returns {string} Base64 string
*/
var encode = function encode(src) {
var urlsafe = arguments.length > 1 && arguments[1] !== undefined ? arguments[1] : false;
return urlsafe ? _mkUriSafe(_encode(src)) : _encode(src);
};
/**
* converts a UTF-8-encoded string to URL-safe Base64 RFC4648 §5.
* @returns {string} Base64 string
*/
var encodeURI = function encodeURI(src) {
return encode(src, true);
};
// This trick is found broken https://github.com/dankogai/js-base64/issues/130
// const btou = (src: string) => decodeURIComponent(escape(src));
// reverting good old fationed regexp
var re_btou = /[\xC0-\xDF][\x80-\xBF]|[\xE0-\xEF][\x80-\xBF]{2}|[\xF0-\xF7][\x80-\xBF]{3}/g;
var cb_btou = function cb_btou(cccc) {
switch (cccc.length) {
case 4:
var cp = (0x07 & cccc.charCodeAt(0)) << 18 | (0x3f & cccc.charCodeAt(1)) << 12 | (0x3f & cccc.charCodeAt(2)) << 6 | 0x3f & cccc.charCodeAt(3),
offset = cp - 0x10000;
return _fromCC((offset >>> 10) + 0xD800) + _fromCC((offset & 0x3FF) + 0xDC00);
case 3:
return _fromCC((0x0f & cccc.charCodeAt(0)) << 12 | (0x3f & cccc.charCodeAt(1)) << 6 | 0x3f & cccc.charCodeAt(2));
default:
return _fromCC((0x1f & cccc.charCodeAt(0)) << 6 | 0x3f & cccc.charCodeAt(1));
}
};
/**
* @deprecated should have been internal use only.
* @param {string} src UTF-16 string
* @returns {string} UTF-8 string
*/
var btou = function btou(b) {
return b.replace(re_btou, cb_btou);
};
/**
* polyfill version of `atob`
*/
var atobPolyfill = function atobPolyfill(asc) {
// console.log('polyfilled');
asc = asc.replace(/\s+/g, '');
if (!b64re.test(asc)) throw new TypeError('malformed base64.');
asc += '=='.slice(2 - (asc.length & 3));
var u24,
bin = '',
r1,
r2;
for (var i = 0; i < asc.length;) {
u24 = b64tab[asc.charAt(i++)] << 18 | b64tab[asc.charAt(i++)] << 12 | (r1 = b64tab[asc.charAt(i++)]) << 6 | (r2 = b64tab[asc.charAt(i++)]);
bin += r1 === 64 ? _fromCC(u24 >> 16 & 255) : r2 === 64 ? _fromCC(u24 >> 16 & 255, u24 >> 8 & 255) : _fromCC(u24 >> 16 & 255, u24 >> 8 & 255, u24 & 255);
}
return bin;
};
/**
* does what `window.atob` of web browsers do.
* @param {String} asc Base64-encoded string
* @returns {string} binary string
*/
var _atob = typeof atob === 'function' ? function (asc) {
return atob(_tidyB64(asc));
} : _hasBuffer ? function (asc) {
return Buffer.from(asc, 'base64').toString('binary');
} : atobPolyfill;
//
var _toUint8Array = _hasBuffer ? function (a) {
return _U8Afrom(Buffer.from(a, 'base64'));
} : function (a) {
return _U8Afrom(_atob(a).split('').map(function (c) {
return c.charCodeAt(0);
}));
};
/**
* converts a Base64 string to a Uint8Array.
*/
var toUint8Array = function toUint8Array(a) {
return _toUint8Array(_unURI(a));
};
//
var _decode = _hasBuffer ? function (a) {
return Buffer.from(a, 'base64').toString('utf8');
} : _TD ? function (a) {
return _TD.decode(_toUint8Array(a));
} : function (a) {
return btou(_atob(a));
};
var _unURI = function _unURI(a) {
return _tidyB64(a.replace(/[-_]/g, function (m0) {
return m0 == '-' ? '+' : '/';
}));
};
/**
* converts a Base64 string to a UTF-8 string.
* @param {String} src Base64 string. Both normal and URL-safe are supported
* @returns {string} UTF-8 string
*/
var decode = function decode(src) {
return _decode(_unURI(src));
};
/**
* check if a value is a valid Base64 string
* @param {String} src a value to check
*/
var isValid = function isValid(src) {
if (typeof src !== 'string') return false;
var s = src.replace(/\s+/g, '').replace(/={0,2}$/, '');
return !/[^\s0-9a-zA-Z\+/]/.test(s) || !/[^\s0-9a-zA-Z\-_]/.test(s);
};
//
var _noEnum = function _noEnum(v) {
return {
value: v,
enumerable: false,
writable: true,
configurable: true
};
};
/**
* extend String.prototype with relevant methods
*/
var extendString = function extendString() {
var _add = function _add(name, body) {
return Object.defineProperty(String.prototype, name, _noEnum(body));
};
_add('fromBase64', function () {
return decode(this);
});
_add('toBase64', function (urlsafe) {
return encode(this, urlsafe);
});
_add('toBase64URI', function () {
return encode(this, true);
});
_add('toBase64URL', function () {
return encode(this, true);
});
_add('toUint8Array', function () {
return toUint8Array(this);
});
};
/**
* extend Uint8Array.prototype with relevant methods
*/
var extendUint8Array = function extendUint8Array() {
var _add = function _add(name, body) {
return Object.defineProperty(Uint8Array.prototype, name, _noEnum(body));
};
_add('toBase64', function (urlsafe) {
return fromUint8Array(this, urlsafe);
});
_add('toBase64URI', function () {
return fromUint8Array(this, true);
});
_add('toBase64URL', function () {
return fromUint8Array(this, true);
});
};
/**
* extend Builtin prototypes with relevant methods
*/
var extendBuiltins = function extendBuiltins() {
extendString();
extendUint8Array();
};
var gBase64 = {
version: version,
VERSION: VERSION,
atob: _atob,
atobPolyfill: atobPolyfill,
btoa: _btoa,
btoaPolyfill: btoaPolyfill,
fromBase64: decode,
toBase64: encode,
encode: encode,
encodeURI: encodeURI,
encodeURL: encodeURI,
utob: utob,
btou: btou,
decode: decode,
isValid: isValid,
fromUint8Array: fromUint8Array,
toUint8Array: toUint8Array,
extendString: extendString,
extendUint8Array: extendUint8Array,
extendBuiltins: extendBuiltins
};
class FetchError extends Error {
constructor(statusCode, message) {
super(message);
this.statusCode = statusCode;
Object.setPrototypeOf(this, FetchError.prototype);
}
}
class TimeoutError extends Error {
constructor(message) {
super(message);
Object.setPrototypeOf(this, TimeoutError.prototype);
}
}
class SdkEvaluationApi {
constructor(deploymentKey, serverUrl, httpClient) {
this.deploymentKey = deploymentKey;
this.serverUrl = serverUrl;
this.httpClient = httpClient;
}
getVariants(user, options) {
return __awaiter(this, void 0, void 0, function* () {
const userJsonBase64 = gBase64.encodeURL(JSON.stringify(user));
const headers = {
Authorization: `Api-Key ${this.deploymentKey}`,
'X-Amp-Exp-User': userJsonBase64,
};
if (options === null || options === void 0 ? void 0 : options.flagKeys) {
headers['X-Amp-Exp-Flag-Keys'] = gBase64.encodeURL(JSON.stringify(options.flagKeys));
}
if (options === null || options === void 0 ? void 0 : options.trackingOption) {
headers['X-Amp-Exp-Track'] = options.trackingOption;
}
const url = new URL(`${this.serverUrl}/sdk/v2/vardata?v=0`);
if (options === null || options === void 0 ? void 0 : options.evaluationMode) {
url.searchParams.append('eval_mode', options === null || options === void 0 ? void 0 : options.evaluationMode);
}
if (options === null || options === void 0 ? void 0 : options.deliveryMethod) {
url.searchParams.append('delivery_method', options === null || options === void 0 ? void 0 : options.deliveryMethod);
}
const response = yield this.httpClient.request({
requestUrl: url.toString(),
method: 'GET',
headers: headers,
timeoutMillis: options === null || options === void 0 ? void 0 : options.timeoutMillis,
});
if (response.status != 200) {
throw new FetchError(response.status, `Fetch error response: status=${response.status}`);
}
return JSON.parse(response.body);
});
}
}
class SdkFlagApi {
constructor(deploymentKey, serverUrl, httpClient) {
this.deploymentKey = deploymentKey;
this.serverUrl = serverUrl;
this.httpClient = httpClient;
}
getFlags(options) {
return __awaiter(this, void 0, void 0, function* () {
const headers = {
Authorization: `Api-Key ${this.deploymentKey}`,
};
if ((options === null || options === void 0 ? void 0 : options.libraryName) && (options === null || options === void 0 ? void 0 : options.libraryVersion)) {
headers['X-Amp-Exp-Library'] = `${options.libraryName}/${options.libraryVersion}`;
}
if (options === null || options === void 0 ? void 0 : options.user) {
headers['X-Amp-Exp-User'] = gBase64.encodeURL(JSON.stringify(options.user));
}
const response = yield this.httpClient.request({
requestUrl: `${this.serverUrl}/sdk/v2/flags` +
((options === null || options === void 0 ? void 0 : options.deliveryMethod)
? `?delivery_method=${options.deliveryMethod}`
: ''),
method: 'GET',
headers: headers,
timeoutMillis: options === null || options === void 0 ? void 0 : options.timeoutMillis,
});
if (response.status != 200) {
throw Error(`Flags error response: status=${response.status}`);
}
const flagsArray = JSON.parse(response.body);
return flagsArray.reduce((map, flag) => {
map[flag.key] = flag;
return map;
}, {});
});
}
}
const safeGlobal = typeof globalThis !== 'undefined' ? globalThis : global || self;
const getGlobalScope = () => {
if (typeof globalThis !== 'undefined') {
return globalThis;
}
if (typeof window !== 'undefined') {
return window;
}
if (typeof self !== 'undefined') {
return self;
}
if (typeof global !== 'undefined') {
return global;
}
return undefined;
};
const isLocalStorageAvailable = () => {
const globalScope = getGlobalScope();
if (globalScope) {
try {
const testKey = 'EXP_test';
globalScope.localStorage.setItem(testKey, testKey);
globalScope.localStorage.removeItem(testKey);
return true;
}
catch (e) {
return false;
}
}
return false;
};
class Poller {
constructor(action, ms) {
this.poller = undefined;
this.action = action;
this.ms = ms;
}
start() {
if (this.poller) {
return;
}
this.poller = safeGlobal.setInterval(this.action, this.ms);
void this.action();
}
stop() {
if (!this.poller) {
return;
}
// eslint-disable-next-line @typescript-eslint/ban-ts-comment
// @ts-ignore
safeGlobal.clearInterval(this.poller);
this.poller = undefined;
}
}
export { EvaluationEngine, EvaluationOperator, FetchError, Poller, SdkEvaluationApi, SdkFlagApi, TimeoutError, getGlobalScope, isLocalStorageAvailable, safeGlobal, topologicalSort };