UNPKG

@amplitude/experiment-core

Version:

Amplitude Experiment evaluation JavaScript implementation.

1,129 lines (1,114 loc) 39.4 kB
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 };