UNPKG

hypertune

Version:

[Hypertune](https://www.hypertune.com/) is the most flexible platform for feature flags, A/B testing, analytics and app configuration. Built with full end-to-end type-safety, Git-style version control and local, synchronous, in-memory flag evaluation. Opt

521 lines 22.4 kB
"use strict"; var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) { if (k2 === undefined) k2 = k; var desc = Object.getOwnPropertyDescriptor(m, k); if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) { desc = { enumerable: true, get: function() { return m[k]; } }; } Object.defineProperty(o, k2, desc); }) : (function(o, m, k, k2) { if (k2 === undefined) k2 = k; o[k2] = m[k]; })); var __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? (function(o, v) { Object.defineProperty(o, "default", { enumerable: true, value: v }); }) : function(o, v) { o["default"] = v; }); var __importStar = (this && this.__importStar) || (function () { var ownKeys = function(o) { ownKeys = Object.getOwnPropertyNames || function (o) { var ar = []; for (var k in o) if (Object.prototype.hasOwnProperty.call(o, k)) ar[ar.length] = k; return ar; }; return ownKeys(o); }; return function (mod) { if (mod && mod.__esModule) return mod; var result = {}; if (mod != null) for (var k = ownKeys(mod), i = 0; i < k.length; i++) if (k[i] !== "default") __createBinding(result, mod, k[i]); __setModuleDefault(result, mod); return result; }; })(); var __importDefault = (this && this.__importDefault) || function (mod) { return (mod && mod.__esModule) ? mod : { "default": mod }; }; Object.defineProperty(exports, "__esModule", { value: true }); /* eslint-disable no-underscore-dangle */ const shared_1 = require("../shared"); const getMetadata_1 = __importDefault(require("../shared/helpers/getMetadata")); const merge_1 = __importDefault(require("./merge")); const throwIfObjectValueIsInvalid_1 = __importDefault(require("../shared/helpers/throwIfObjectValueIsInvalid")); const getLocalLogArguments_1 = __importDefault(require("../shared/helpers/getLocalLogArguments")); const getNodeCacheKey_1 = __importDefault(require("./getNodeCacheKey")); const getNodePath_1 = __importStar(require("./getNodePath")); class Node { constructor(props) { // This should be overridden by subclasses as the constructor name may be // minified and mangled by bundlers this.typeName = this.constructor.name; this.props = props; } updateIfNeeded() { const { context, parent, step } = this.props; if (!context || !context.initData) { return; } // If we're on the latest commit hash, we don't need to update if (this.props.initDataHash === context.initData.hash) { return; } // If we don't have a parent, we're the source node if (!parent) { this.props = Object.assign(Object.assign({}, this.props), { initDataHash: context.initData.hash, expression: context.initData.reducedExpression }); return; } // Update from the parent if (!step) { throw new Error("Node update error: Missing step."); } switch (step.type) { case "GetFieldStep": { this.props = parent.getFieldNodeProps(step.fieldName, { fieldArguments: step.fieldArguments, }); break; } case "GetItemStep": { const newProps = parent.getItemNodeProps({ fallbackLength: step.fallbackLength, })[step.index]; if (!newProps) { throw new Error("Node update error: No item props."); } this.props = newProps; break; } default: { const neverStep = step; throw new Error(`Unexpected step: ${JSON.stringify(neverStep)}`); } } } getFieldNode(fieldName, { fieldArguments = {} } = {}) { return new Node(this.getFieldNodeProps(fieldName, { fieldArguments })); } /** * @deprecated This method will be removed in the next major SDK version. */ getField(fieldName, fieldArguments) { return this.getFieldNodeProps(fieldName, { fieldArguments }); } getFieldNodeProps(fieldName, { fieldArguments = {} } = {}) { var _a; const step = { type: "GetFieldStep", fieldName, fieldArguments }; const { context } = this.props; const initDataHash = (_a = context === null || context === void 0 ? void 0 : context.initData) === null || _a === void 0 ? void 0 : _a.hash; if (!initDataHash || !context.getFieldCache) { // No caching if the sdk hasn't been initialized or there is no cache. return this.createProps(step, this.getReducedFieldExpression(fieldName, fieldArguments)); } const cacheKey = (0, getNodeCacheKey_1.default)(initDataHash, (0, getNodePath_1.default)(/* parent */ this, step), /* suffix */ ""); const cachedReducedFieldExpression = context.getFieldCache.get(cacheKey); if (cachedReducedFieldExpression) { return this.createProps(step, cachedReducedFieldExpression); } const reducedFieldExpression = this.getReducedFieldExpression(fieldName, fieldArguments); if (reducedFieldExpression) { context.getFieldCache.set(cacheKey, reducedFieldExpression); } return this.createProps(step, reducedFieldExpression); } // @internal getReducedFieldExpression(fieldName, fieldArguments) { try { (0, shared_1.prefixError)(() => (0, throwIfObjectValueIsInvalid_1.default)(fieldArguments), "Invalid field arguments: "); this.updateIfNeeded(); const { expression } = this.props; if (!expression) { this.log(shared_1.LogLevel.Debug, `Using fallback for field "${fieldName}" as expression is null. This is expected before initialization.`); return null; } if (expression.type !== "ObjectExpression") { throw new Error(`Cannot get field "${fieldName}" as expression type is "${expression.type}".`); } const context = (0, shared_1.nullThrows)(this.props.context, `Cannot get field "${fieldName}" as context is null.`); const selection = { [fieldName]: { fieldArguments, fieldQuery: null }, }; const { objectTypeName } = expression; const fragment = { type: "InlineFragment", objectTypeName, selection, }; const fieldQuery = { [objectTypeName]: fragment, }; const reducedObjectExpression = context.reduce(fieldQuery, expression); if (reducedObjectExpression.type !== "ObjectExpression") { throw new Error(`Cannot get field "${fieldName}" as reduced expression type is "${expression.type}".`); } const reducedFieldExpression = (0, shared_1.nullThrows)(reducedObjectExpression.fields[fieldName], `Object expression does not contain field "${fieldName}".`); reducedFieldExpression.logs = (0, shared_1.mergeLogs)(reducedObjectExpression.logs, (0, shared_1.getExpressionEvaluationCountLogs)(reducedObjectExpression), reducedFieldExpression.logs); return reducedFieldExpression; } catch (error) { this.log(shared_1.LogLevel.Error, `Error getting field "${fieldName}" with arguments ${JSON.stringify(fieldArguments)}: ${(0, shared_1.asError)(error).message}`, (0, getMetadata_1.default)(error)); return null; } } getItemNodes({ fallbackLength = 0, } = {}) { return this.getItemNodeProps({ fallbackLength }).map((props) => new Node(props)); } /** * @deprecated This method will be removed in the next major SDK version. */ _getItems(fallbackLength) { return this.getItemNodeProps({ fallbackLength }); } getItemNodeProps({ fallbackLength = 0, } = {}) { var _a; const { context, parent, step } = this.props; const initDataHash = (_a = context === null || context === void 0 ? void 0 : context.initData) === null || _a === void 0 ? void 0 : _a.hash; if (!initDataHash || !context.getItemsCache) { // No caching if the sdk hasn't been initialized or there is no cache. return this.createPropsArray(this._getItemExpressions(), fallbackLength); } const cacheKey = (0, getNodeCacheKey_1.default)(initDataHash, (0, getNodePath_1.default)(parent, step), /* suffix */ ""); const cachedItemExpressions = context.getItemsCache.get(cacheKey); if (cachedItemExpressions) { return this.createPropsArray(cachedItemExpressions, fallbackLength); } const itemExpressions = this._getItemExpressions(); if (itemExpressions) { context.getItemsCache.set(cacheKey, itemExpressions); } return this.createPropsArray(itemExpressions, fallbackLength); } // @internal _getItemExpressions() { try { this.updateIfNeeded(); const { expression } = this.props; if (!expression) { this.log(shared_1.LogLevel.Debug, "Using fallback for array items as expression is null. This is expected before initialization."); return null; } if (expression.type !== "ListExpression") { throw new Error(`Cannot get items as expression type is "${expression.type}". ${shared_1.breakingSchemaChangesError}`); } const listLogs = (0, shared_1.mergeLogs)(expression.logs, (0, shared_1.getExpressionEvaluationCountLogs)(expression)); const result = expression.items.map((item, index) => { const itemExpression = (0, shared_1.nullThrows)(item, `List expression has null item at index ${index}.`); itemExpression.logs = (0, shared_1.mergeLogs)(listLogs, itemExpression.logs); return itemExpression; }); return result; } catch (error) { this.log(shared_1.LogLevel.Error, `Error getting items: ${(0, shared_1.asError)(error).message}`, (0, getMetadata_1.default)(error)); return null; } } getFieldValue(fieldName, { fallback, query = null, fieldArguments = {}, }) { return this.getFieldNode(fieldName, { fieldArguments }).getValue({ fallback, query, }); } /** * @deprecated This method will be removed in the next major SDK version. */ evaluate(query, fallback) { return this.getValue({ query, fallback }); } getValue({ query = null, fallback, }) { var _a; const valueAndLogs = this.getValueAndLogsWithCache(query); const { value: valueWithoutOverride, logs: reductionLogs, path, args, shouldLogEvaluation, } = valueAndLogs; const isFallback = valueWithoutOverride === null; const value = (0, merge_1.default)(isFallback ? fallback : valueWithoutOverride, this.getNodeOverride()); this.logReductionLogs(Object.assign(Object.assign({}, reductionLogs), { evaluationList: shouldLogEvaluation ? [ ...((_a = reductionLogs.evaluationList) !== null && _a !== void 0 ? _a : []), { path, value, args, isFallback }, ] : reductionLogs.evaluationList })); return value; } getNodeOverride() { var _a, _b, _c; const { context, parent, step } = this.props; if (!context) { return undefined; } if (context.override === null || context.override === undefined) { // Short-circuit if no override in context return undefined; } if (!parent) { // We're the Query node return (_a = context.override) !== null && _a !== void 0 ? _a : undefined; } if (!step) { return undefined; } const parentOverride = parent.getNodeOverride(); if (parentOverride === null || parentOverride === undefined) { return undefined; } if (step.type === "GetFieldStep") { return (_b = parentOverride[step.fieldName]) !== null && _b !== void 0 ? _b : undefined; } return (_c = parentOverride[step.index]) !== null && _c !== void 0 ? _c : undefined; } // @internal getValueAndLogsWithCache(query) { var _a; const { context, parent, step } = this.props; const initDataHash = (_a = context === null || context === void 0 ? void 0 : context.initData) === null || _a === void 0 ? void 0 : _a.hash; if (!initDataHash || !context.evaluateCache) { // No caching if the sdk hasn't been initialized or there is no cache. return this.getValueAndLogs(query); } const cacheKey = (0, getNodeCacheKey_1.default)(initDataHash, (0, getNodePath_1.default)(parent, step), /* suffix */ JSON.stringify(query)); const cachedValueAndLogs = context.evaluateCache.get(cacheKey); if (cachedValueAndLogs) { return cachedValueAndLogs; } const valueAndLogs = this.getValueAndLogs(query); if (valueAndLogs.value !== null) { context.evaluateCache.set(cacheKey, valueAndLogs); } return valueAndLogs; } // @internal getValueAndLogs(query) { const { path, args } = (0, getNodePath_1.getJsonNodePathAndArgs)(this.props.parent, this.props.step); try { this.updateIfNeeded(); const { expression } = this.props; if (!expression) { return { path, args, value: null, logs: { messageList: [ { level: shared_1.LogLevel.Debug, message: `Using fallback while evaluating as expression is null. This is expected before initialization.`, metadata: {}, }, ], }, shouldLogEvaluation: true, }; } const context = (0, shared_1.nullThrows)(this.props.context, "Cannot evaluate as context is null."); const reducedExpression = context.reduce(query, expression); const { value, logs, shouldLogEvaluation } = (0, shared_1.prefixError)(() => (0, shared_1.evaluate)(reducedExpression), "Evaluation error: "); return { value, logs, path, args, shouldLogEvaluation }; } catch (error) { return { path, args, value: null, logs: { messageList: [ { level: shared_1.LogLevel.Error, message: `Error getting value and logs: ${(0, shared_1.asError)(error).message}`, metadata: (0, getMetadata_1.default)(error), }, ], }, shouldLogEvaluation: true, }; } } // @internal createProps(step, expression) { const { context, logger, initDataHash } = this.props; return { step, parent: this, context, expression, logger, initDataHash }; } // @internal createPropsArray(itemExpressions, fallbackLength) { return (itemExpressions || Array(fallbackLength).fill(null)).map((expression, index) => this.createProps({ type: "GetItemStep", index, fallbackLength }, expression)); } _logUnexpectedTypeError() { if (!this.props.expression) { this.log(shared_1.LogLevel.Debug, `Unexpected expression type as expression is null but this is expected before initialization.`); return; } this.log(shared_1.LogLevel.Error, "Unexpected expression type."); } logUnexpectedValueError(value) { this.log(shared_1.LogLevel.Error, `Evaluated to unexpected value: ${JSON.stringify(value)}`); } log(level, message, metadata = {}) { this.logReductionLogs({ messageList: [{ level, message, metadata }], }); } logReductionLogs(reductionLogs) { var _a, _b, _c; const { typeName } = this; const { parent, step, logger, expression, initDataHash } = this.props; const commitId = (_c = (_b = (_a = this.props.context) === null || _a === void 0 ? void 0 : _a.initData) === null || _b === void 0 ? void 0 : _b.commitId.toString()) !== null && _c !== void 0 ? _c : null; const nodePath = (0, getNodePath_1.default)(parent, step); if (!logger) { // eslint-disable-next-line no-console console.error(...(0, getLocalLogArguments_1.default)(`No logger for ${typeName}Node at ${nodePath} to log reduction logs`, { reductionLogs })); return; } logger.nodeLog({ commitId, initDataHash, nodeTypeName: typeName, nodePath, nodeExpression: expression, reductionLogs, }); } getStateHash() { const { context } = this.props; if (!context) { this.log(shared_1.LogLevel.Error, "No context so cannot get state hash."); return null; } return context.getStateHash(); } getInitResponse() { const { context } = this.props; if (!context || !context.initData) { this.log(shared_1.LogLevel.Error, "No context so cannot get init data."); return null; } return context.initData; } getHashResponse() { const { context } = this.props; if (!context || !context.initData) { this.log(shared_1.LogLevel.Error, "No context so cannot get hash data."); return null; } return (0, shared_1.formatHashData)(context.initData); } addUpdateListener(listener) { const { context } = this.props; if (!context) { this.log(shared_1.LogLevel.Error, "No context so cannot add update listener."); return; } context.addUpdateListener(listener); } removeUpdateListener(listener) { const { context } = this.props; if (!context) { this.log(shared_1.LogLevel.Error, "No context so cannot remove update listener."); return; } context.removeUpdateListener(listener); } /** * Initialize from the init data provider if needed */ initIfNeeded(traceId = (0, shared_1.uniqueId)(), retries = 0) { const { context } = this.props; if (!context) { this.log(shared_1.LogLevel.Error, "No context so cannot initialize from the data provider."); return Promise.resolve(); } return context.initIfNeeded(traceId, retries); } /** * Returns the timestamp of the last time the SDK was initialized from * the init data provider */ getLastInitDataRefreshTime() { const { context } = this.props; if (!context) { this.log(shared_1.LogLevel.Error, "No context so cannot get the last data provider init time."); return null; } return context.lastInitDataRefreshTime; } /** * @returns @deprecated use `getLastInitDataRefreshTime` instead */ getLastDataProviderInitTime() { return this.getLastInitDataRefreshTime(); } /** * Indicates whether the SDK is ready to evaluate flags and log events. */ isReady() { const { context } = this.props; if (!context) { return false; } return context.isReady(); } flushLogs(traceId = (0, shared_1.uniqueId)()) { const { context } = this.props; if (!context) { this.log(shared_1.LogLevel.Error, "No context so cannot flush logs."); return Promise.resolve(); } return context.logger.flush(traceId); } setOverride(override, traceId = (0, shared_1.uniqueId)()) { const { context } = this.props; if (!context) { this.log(shared_1.LogLevel.Error, "No context so cannot set override."); return; } context.setOverride(traceId, override); } dehydrate(query, variableValues) { const { context } = this.props; if (!context) { this.log(shared_1.LogLevel.Error, "No context so cannot dehydrate."); return null; } return context.dehydrate(query, variableValues); } hydrate(dehydratedState, traceId = (0, shared_1.uniqueId)()) { const { context } = this.props; if (!context) { this.log(shared_1.LogLevel.Error, "No context so cannot hydrate."); return; } context.hydrate(traceId, dehydratedState); } /** * Close flushes any remaining logs and stops all background processes * ensuring clean shutdown. */ close(traceId = (0, shared_1.uniqueId)()) { const { context } = this.props; if (!context) { this.log(shared_1.LogLevel.Error, "No context so cannot close."); return Promise.resolve(); } return context.close(traceId); } getFlagValues({ flagFallbacks, flagPaths, }) { return flagPaths.reduce((current, flag) => { current[flag] = this.getFlagValue(flag, flagFallbacks[flag]); return current; }, {}); } getFlagValue(flagPath, fallback) { return flagPath .split(".") .reduce((node, step) => node.getFieldNode(step, {}), this) .getValue({ fallback }); } getEncodedFlagValues({ flagFallbacks, flagPaths, }) { return btoa(JSON.stringify(this.getFlagValues({ flagFallbacks, flagPaths }))); } } exports.default = Node; //# sourceMappingURL=Node.js.map