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
JavaScript
"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