UNPKG

@launchdarkly/js-server-sdk-common

Version:
517 lines 29.1 kB
"use strict"; Object.defineProperty(exports, "__esModule", { value: true }); /* eslint-disable class-methods-use-this */ const js_sdk_common_1 = require("@launchdarkly/js-sdk-common"); const api_1 = require("./api"); const BigSegmentsManager_1 = require("./BigSegmentsManager"); const createStreamListeners_1 = require("./data_sources/createStreamListeners"); const DataSourceUpdates_1 = require("./data_sources/DataSourceUpdates"); const PollingProcessor_1 = require("./data_sources/PollingProcessor"); const Requestor_1 = require("./data_sources/Requestor"); const StreamingProcessor_1 = require("./data_sources/StreamingProcessor"); const createDiagnosticsInitConfig_1 = require("./diagnostics/createDiagnosticsInitConfig"); const collection_1 = require("./evaluation/collection"); const EvalResult_1 = require("./evaluation/EvalResult"); const Evaluator_1 = require("./evaluation/Evaluator"); const ContextDeduplicator_1 = require("./events/ContextDeduplicator"); const EventFactory_1 = require("./events/EventFactory"); const isExperiment_1 = require("./events/isExperiment"); const FlagsStateBuilder_1 = require("./FlagsStateBuilder"); const HookRunner_1 = require("./hooks/HookRunner"); const MigrationOpEventConversion_1 = require("./MigrationOpEventConversion"); const MigrationOpTracker_1 = require("./MigrationOpTracker"); const Configuration_1 = require("./options/Configuration"); const VersionedDataKinds_1 = require("./store/VersionedDataKinds"); const { ClientMessages, ErrorKinds, NullEventProcessor } = js_sdk_common_1.internal; var InitState; (function (InitState) { InitState[InitState["Initializing"] = 0] = "Initializing"; InitState[InitState["Initialized"] = 1] = "Initialized"; InitState[InitState["Failed"] = 2] = "Failed"; })(InitState || (InitState = {})); const HIGH_TIMEOUT_THRESHOLD = 60; const BOOL_VARIATION_METHOD_NAME = 'LDClient.boolVariation'; const NUMBER_VARIATION_METHOD_NAME = 'LDClient.numberVariation'; const STRING_VARIATION_METHOD_NAME = 'LDClient.stringVariation'; const JSON_VARIATION_METHOD_NAME = 'LDClient.jsonVariation'; const VARIATION_METHOD_NAME = 'LDClient.variation'; const MIGRATION_VARIATION_METHOD_NAME = 'LDClient.migrationVariation'; const BOOL_VARIATION_DETAIL_METHOD_NAME = 'LDClient.boolVariationDetail'; const NUMBER_VARIATION_DETAIL_METHOD_NAME = 'LDClient.numberVariationDetail'; const STRING_VARIATION_DETAIL_METHOD_NAME = 'LDClient.stringVariationDetail'; const JSON_VARIATION_DETAIL_METHOD_NAME = 'LDClient.jsonVariationDetail'; const VARIATION_METHOD_DETAIL_NAME = 'LDClient.variationDetail'; /** * @ignore */ class LDClientImpl { get logger() { return this._logger; } constructor(_sdkKey, _platform, options, callbacks, internalOptions) { var _a, _b, _c, _d, _e; this._sdkKey = _sdkKey; this._platform = _platform; this._initState = InitState.Initializing; this._eventFactoryDefault = new EventFactory_1.default(false); this._eventFactoryWithReasons = new EventFactory_1.default(true); this._onError = callbacks.onError; this._onFailed = callbacks.onFailed; this._onReady = callbacks.onReady; const { onUpdate, hasEventListeners } = callbacks; const config = new Configuration_1.default(options, internalOptions); this._hookRunner = new HookRunner_1.default(config.logger, config.hooks || []); if (!_sdkKey && !config.offline) { throw new Error('You must configure the client with an SDK key'); } this._config = config; this._logger = config.logger; const baseHeaders = (0, js_sdk_common_1.defaultHeaders)(_sdkKey, _platform.info, config.tags); const clientContext = new js_sdk_common_1.ClientContext(_sdkKey, config, _platform); const featureStore = config.featureStoreFactory(clientContext); const dataSourceUpdates = new DataSourceUpdates_1.default(featureStore, hasEventListeners, onUpdate); if (config.sendEvents && !config.offline && !config.diagnosticOptOut) { this._diagnosticsManager = new js_sdk_common_1.internal.DiagnosticsManager(_sdkKey, _platform, (0, createDiagnosticsInitConfig_1.default)(config, _platform, featureStore)); } if (!config.sendEvents || config.offline) { this._eventProcessor = new NullEventProcessor(); } else { this._eventProcessor = new js_sdk_common_1.internal.EventProcessor(config, clientContext, baseHeaders, new ContextDeduplicator_1.default(config), this._diagnosticsManager); } this._featureStore = featureStore; const manager = new BigSegmentsManager_1.default((_b = (_a = config.bigSegments) === null || _a === void 0 ? void 0 : _a.store) === null || _b === void 0 ? void 0 : _b.call(_a, clientContext), (_c = config.bigSegments) !== null && _c !== void 0 ? _c : {}, config.logger, this._platform.crypto); this._bigSegmentsManager = manager; this.bigSegmentStatusProviderInternal = manager.statusProvider; const queries = { getFlag(key, cb) { featureStore.get(VersionedDataKinds_1.default.Features, key, (item) => cb(item)); }, getSegment(key, cb) { featureStore.get(VersionedDataKinds_1.default.Segments, key, (item) => cb(item)); }, getBigSegmentsMembership(userKey) { return manager.getUserMembership(userKey); }, }; this._evaluator = new Evaluator_1.default(this._platform, queries); const listeners = (0, createStreamListeners_1.createStreamListeners)(dataSourceUpdates, this._logger, { put: () => this._initSuccess(), }); const makeDefaultProcessor = () => config.stream ? new StreamingProcessor_1.default(clientContext, '/all', [], listeners, baseHeaders, this._diagnosticsManager, (e) => this._dataSourceErrorHandler(e), this._config.streamInitialReconnectDelay) : new PollingProcessor_1.default(config, new Requestor_1.default(config, this._platform.requests, baseHeaders), dataSourceUpdates, () => this._initSuccess(), (e) => this._dataSourceErrorHandler(e)); if (!(config.offline || config.useLdd)) { this._updateProcessor = (_e = (_d = config.updateProcessorFactory) === null || _d === void 0 ? void 0 : _d.call(config, clientContext, dataSourceUpdates, () => this._initSuccess(), (e) => this._dataSourceErrorHandler(e))) !== null && _e !== void 0 ? _e : makeDefaultProcessor(); } if (this._updateProcessor) { this._updateProcessor.start(); } else { // Deferring the start callback should allow client construction to complete before we start // emitting events. Allowing the client an opportunity to register events. setTimeout(() => this._initSuccess(), 0); } } initialized() { return this._initState === InitState.Initialized; } waitForInitialization(options) { // An initialization promise is only created if someone is going to use that promise. // If we always created an initialization promise, and there was no call waitForInitialization // by the time the promise was rejected, then that would result in an unhandled promise // rejection. var _a, _b; // If there is no update processor, then there is functionally no initialization // so it is fine not to wait. if ((options === null || options === void 0 ? void 0 : options.timeout) === undefined && this._updateProcessor !== undefined) { (_a = this._logger) === null || _a === void 0 ? void 0 : _a.warn('The waitForInitialization function was called without a timeout specified.' + ' In a future version a default timeout will be applied.'); } if ((options === null || options === void 0 ? void 0 : options.timeout) !== undefined && (options === null || options === void 0 ? void 0 : options.timeout) > HIGH_TIMEOUT_THRESHOLD && this._updateProcessor !== undefined) { (_b = this._logger) === null || _b === void 0 ? void 0 : _b.warn('The waitForInitialization function was called with a timeout greater than ' + `${HIGH_TIMEOUT_THRESHOLD} seconds. We recommend a timeout of less than ` + `${HIGH_TIMEOUT_THRESHOLD} seconds.`); } // Initialization promise was created by a previous call to waitForInitialization. if (this._initializedPromise) { // This promise may already be resolved/rejected, but it doesn't hurt to wrap it in a timeout. return this._clientWithTimeout(this._initializedPromise, options === null || options === void 0 ? void 0 : options.timeout, this._logger); } // Initialization completed before waitForInitialization was called, so we have completed // and there was no promise. So we make a resolved promise and return it. if (this._initState === InitState.Initialized) { this._initializedPromise = Promise.resolve(this); // Already initialized, no need to timeout. return this._initializedPromise; } // Initialization failed before waitForInitialization was called, so we have completed // and there was no promise. So we make a rejected promise and return it. if (this._initState === InitState.Failed) { // Already failed, no need to timeout. this._initializedPromise = Promise.reject(this._rejectionReason); return this._initializedPromise; } if (!this._initializedPromise) { this._initializedPromise = new Promise((resolve, reject) => { this._initResolve = resolve; this._initReject = reject; }); } return this._clientWithTimeout(this._initializedPromise, options === null || options === void 0 ? void 0 : options.timeout, this._logger); } variation(key, context, defaultValue, callback) { return this._hookRunner .withEvaluationSeries(key, context, defaultValue, VARIATION_METHOD_NAME, () => new Promise((resolve) => { this._evaluateIfPossible(key, context, defaultValue, this._eventFactoryDefault, (res) => { resolve(res.detail); }); })) .then((detail) => { callback === null || callback === void 0 ? void 0 : callback(null, detail.value); return detail.value; }); } variationDetail(key, context, defaultValue, callback) { return this._hookRunner.withEvaluationSeries(key, context, defaultValue, VARIATION_METHOD_DETAIL_NAME, () => new Promise((resolve) => { this._evaluateIfPossible(key, context, defaultValue, this._eventFactoryWithReasons, (res) => { resolve(res.detail); callback === null || callback === void 0 ? void 0 : callback(null, res.detail); }); })); } _typedEval(key, context, defaultValue, eventFactory, methodName, typeChecker) { return this._hookRunner.withEvaluationSeries(key, context, defaultValue, methodName, () => new Promise((resolve) => { this._evaluateIfPossible(key, context, defaultValue, eventFactory, (res) => { const typedRes = { value: res.detail.value, reason: res.detail.reason, variationIndex: res.detail.variationIndex, }; resolve(typedRes); }, typeChecker); })); } async boolVariation(key, context, defaultValue) { return (await this._typedEval(key, context, defaultValue, this._eventFactoryDefault, BOOL_VARIATION_METHOD_NAME, (value) => [js_sdk_common_1.TypeValidators.Boolean.is(value), js_sdk_common_1.TypeValidators.Boolean.getType()])).value; } async numberVariation(key, context, defaultValue) { return (await this._typedEval(key, context, defaultValue, this._eventFactoryDefault, NUMBER_VARIATION_METHOD_NAME, (value) => [js_sdk_common_1.TypeValidators.Number.is(value), js_sdk_common_1.TypeValidators.Number.getType()])).value; } async stringVariation(key, context, defaultValue) { return (await this._typedEval(key, context, defaultValue, this._eventFactoryDefault, STRING_VARIATION_METHOD_NAME, (value) => [js_sdk_common_1.TypeValidators.String.is(value), js_sdk_common_1.TypeValidators.String.getType()])).value; } jsonVariation(key, context, defaultValue) { return this._hookRunner .withEvaluationSeries(key, context, defaultValue, JSON_VARIATION_METHOD_NAME, () => new Promise((resolve) => { this._evaluateIfPossible(key, context, defaultValue, this._eventFactoryDefault, (res) => { resolve(res.detail); }); })) .then((detail) => detail.value); } boolVariationDetail(key, context, defaultValue) { return this._typedEval(key, context, defaultValue, this._eventFactoryWithReasons, BOOL_VARIATION_DETAIL_METHOD_NAME, (value) => [js_sdk_common_1.TypeValidators.Boolean.is(value), js_sdk_common_1.TypeValidators.Boolean.getType()]); } numberVariationDetail(key, context, defaultValue) { return this._typedEval(key, context, defaultValue, this._eventFactoryWithReasons, NUMBER_VARIATION_DETAIL_METHOD_NAME, (value) => [js_sdk_common_1.TypeValidators.Number.is(value), js_sdk_common_1.TypeValidators.Number.getType()]); } stringVariationDetail(key, context, defaultValue) { return this._typedEval(key, context, defaultValue, this._eventFactoryWithReasons, STRING_VARIATION_DETAIL_METHOD_NAME, (value) => [js_sdk_common_1.TypeValidators.String.is(value), js_sdk_common_1.TypeValidators.String.getType()]); } jsonVariationDetail(key, context, defaultValue) { return this._hookRunner.withEvaluationSeries(key, context, defaultValue, JSON_VARIATION_DETAIL_METHOD_NAME, () => new Promise((resolve) => { this._evaluateIfPossible(key, context, defaultValue, this._eventFactoryWithReasons, (res) => { resolve(res.detail); }); })); } async _migrationVariationInternal(key, context, defaultValue) { var _a; const res = await new Promise((resolve) => { this._evaluateIfPossible(key, context, defaultValue, this._eventFactoryWithReasons, ({ detail }, flag) => { if (!(0, api_1.IsMigrationStage)(detail.value)) { const error = new Error(`Unrecognized MigrationState for "${key}"; returning default value.`); this._onError(error); const reason = { kind: 'ERROR', errorKind: ErrorKinds.WrongType, }; resolve({ detail: { value: defaultValue, reason, }, flag, }); return; } resolve({ detail, flag }); }); }); const { detail, flag } = res; const checkRatio = (_a = flag === null || flag === void 0 ? void 0 : flag.migration) === null || _a === void 0 ? void 0 : _a.checkRatio; const samplingRatio = flag === null || flag === void 0 ? void 0 : flag.samplingRatio; return { detail, migration: { value: detail.value, tracker: new MigrationOpTracker_1.default(key, context, defaultValue, detail.value, detail.reason, checkRatio, // Can be null for compatibility reasons. detail.variationIndex === null ? undefined : detail.variationIndex, flag === null || flag === void 0 ? void 0 : flag.version, samplingRatio, this._logger), }, }; } async migrationVariation(key, context, defaultValue) { const res = await this._hookRunner.withEvaluationSeriesExtraDetail(key, context, defaultValue, MIGRATION_VARIATION_METHOD_NAME, () => this._migrationVariationInternal(key, context, defaultValue)); return res.migration; } allFlagsState(context, options, callback) { var _a, _b, _c; if (this._config.offline) { (_a = this._logger) === null || _a === void 0 ? void 0 : _a.info('allFlagsState() called in offline mode. Returning empty state.'); const allFlagState = new FlagsStateBuilder_1.default(false, false).build(); callback === null || callback === void 0 ? void 0 : callback(null, allFlagState); return Promise.resolve(allFlagState); } const evalContext = js_sdk_common_1.Context.fromLDContext(context); if (!evalContext.valid) { (_b = this._logger) === null || _b === void 0 ? void 0 : _b.info(`${(_c = evalContext.message) !== null && _c !== void 0 ? _c : 'Invalid context.'}. Returning empty state.`); return Promise.resolve(new FlagsStateBuilder_1.default(false, false).build()); } return new Promise((resolve) => { const doEval = (valid) => this._featureStore.all(VersionedDataKinds_1.default.Features, (allFlags) => { const builder = new FlagsStateBuilder_1.default(valid, !!(options === null || options === void 0 ? void 0 : options.withReasons)); const clientOnly = !!(options === null || options === void 0 ? void 0 : options.clientSideOnly); const detailsOnlyIfTracked = !!(options === null || options === void 0 ? void 0 : options.detailsOnlyForTrackedFlags); (0, collection_1.allAsync)(Object.values(allFlags), (storeItem, iterCb) => { var _a; const flag = storeItem; if (clientOnly && !((_a = flag.clientSideAvailability) === null || _a === void 0 ? void 0 : _a.usingEnvironmentId)) { iterCb(true); return; } this._evaluator.evaluateCb(flag, evalContext, (res) => { var _a; if (res.isError) { this._onError(new Error(`Error for feature flag "${flag.key}" while evaluating all flags: ${res.message}`)); } const requireExperimentData = (0, isExperiment_1.default)(flag, res.detail.reason); builder.addFlag(flag, res.detail.value, (_a = res.detail.variationIndex) !== null && _a !== void 0 ? _a : undefined, res.detail.reason, flag.trackEvents || requireExperimentData, requireExperimentData, detailsOnlyIfTracked, res.prerequisites); iterCb(true); }); }, () => { const res = builder.build(); callback === null || callback === void 0 ? void 0 : callback(null, res); resolve(res); }); }); if (!this.initialized()) { this._featureStore.initialized((storeInitialized) => { var _a, _b; let valid = true; if (storeInitialized) { (_a = this._logger) === null || _a === void 0 ? void 0 : _a.warn('Called allFlagsState before client initialization; using last known' + ' values from data store'); } else { (_b = this._logger) === null || _b === void 0 ? void 0 : _b.warn('Called allFlagsState before client initialization. Data store not available; ' + 'returning empty state'); valid = false; } doEval(valid); }); } else { doEval(true); } }); } secureModeHash(context) { const checkedContext = js_sdk_common_1.Context.fromLDContext(context); const key = checkedContext.valid ? checkedContext.canonicalKey : undefined; if (!this._platform.crypto.createHmac) { // This represents an error in platform implementation. throw new Error('Platform must implement createHmac'); } const hmac = this._platform.crypto.createHmac('sha256', this._sdkKey); if (key === undefined) { throw new js_sdk_common_1.LDClientError('Could not generate secure mode hash for invalid context'); } hmac.update(key); return hmac.digest('hex'); } close() { var _a; this._eventProcessor.close(); (_a = this._updateProcessor) === null || _a === void 0 ? void 0 : _a.close(); this._featureStore.close(); this._bigSegmentsManager.close(); } isOffline() { return this._config.offline; } track(key, context, data, metricValue) { var _a, _b; const checkedContext = js_sdk_common_1.Context.fromLDContext(context); if (!checkedContext.valid) { (_a = this._logger) === null || _a === void 0 ? void 0 : _a.warn(ClientMessages.MissingContextKeyNoEvent); return; } // 0 is valid, so do not truthy check the metric value if (metricValue !== undefined && !js_sdk_common_1.TypeValidators.Number.is(metricValue)) { (_b = this._logger) === null || _b === void 0 ? void 0 : _b.warn(ClientMessages.invalidMetricValue(typeof metricValue)); } this._eventProcessor.sendEvent(this._eventFactoryDefault.customEvent(key, checkedContext, data, metricValue)); } trackMigration(event) { const converted = (0, MigrationOpEventConversion_1.default)(event); if (!converted) { return; } this._eventProcessor.sendEvent(converted); } identify(context) { var _a; const checkedContext = js_sdk_common_1.Context.fromLDContext(context); if (!checkedContext.valid) { (_a = this._logger) === null || _a === void 0 ? void 0 : _a.warn(ClientMessages.MissingContextKeyNoEvent); return; } this._eventProcessor.sendEvent(this._eventFactoryDefault.identifyEvent(checkedContext)); } async flush(callback) { try { await this._eventProcessor.flush(); } catch (err) { return callback === null || callback === void 0 ? void 0 : callback(err, false); } return callback === null || callback === void 0 ? void 0 : callback(null, true); } addHook(hook) { this._hookRunner.addHook(hook); } _variationInternal(flagKey, context, defaultValue, eventFactory, cb, typeChecker) { var _a, _b; if (this._config.offline) { (_a = this._logger) === null || _a === void 0 ? void 0 : _a.info('Variation called in offline mode. Returning default value.'); cb(EvalResult_1.default.forError(ErrorKinds.ClientNotReady, undefined, defaultValue)); return; } const evalContext = js_sdk_common_1.Context.fromLDContext(context); if (!evalContext.valid) { this._onError(new js_sdk_common_1.LDClientError(`${(_b = evalContext.message) !== null && _b !== void 0 ? _b : 'Context not valid;'} returning default value.`)); cb(EvalResult_1.default.forError(ErrorKinds.UserNotSpecified, undefined, defaultValue)); return; } this._featureStore.get(VersionedDataKinds_1.default.Features, flagKey, (item) => { const flag = item; if (!flag) { const error = new js_sdk_common_1.LDClientError(`Unknown feature flag "${flagKey}"; returning default value`); this._onError(error); const result = EvalResult_1.default.forError(ErrorKinds.FlagNotFound, undefined, defaultValue); this._eventProcessor.sendEvent(this._eventFactoryDefault.unknownFlagEvent(flagKey, defaultValue, evalContext)); cb(result); return; } this._evaluator.evaluateCb(flag, evalContext, (evalRes) => { var _a; if (evalRes.detail.variationIndex === undefined || evalRes.detail.variationIndex === null) { (_a = this._logger) === null || _a === void 0 ? void 0 : _a.debug('Result value is null in variation'); evalRes.setDefault(defaultValue); } if (typeChecker) { const [matched, type] = typeChecker(evalRes.detail.value); if (!matched) { const errorRes = EvalResult_1.default.forError(ErrorKinds.WrongType, `Did not receive expected type (${type}) evaluating feature flag "${flagKey}"`, defaultValue); this._sendEvalEvent(errorRes, eventFactory, flag, evalContext, defaultValue); cb(errorRes, flag); return; } } this._sendEvalEvent(evalRes, eventFactory, flag, evalContext, defaultValue); cb(evalRes, flag); }, eventFactory); }); } _sendEvalEvent(evalRes, eventFactory, flag, evalContext, defaultValue) { var _a; (_a = evalRes.events) === null || _a === void 0 ? void 0 : _a.forEach((event) => { this._eventProcessor.sendEvent(Object.assign({}, event)); }); this._eventProcessor.sendEvent(eventFactory.evalEventServer(flag, evalContext, evalRes.detail, defaultValue, undefined)); } _evaluateIfPossible(flagKey, context, defaultValue, eventFactory, cb, typeChecker) { if (!this.initialized()) { this._featureStore.initialized((storeInitialized) => { var _a, _b; if (storeInitialized) { (_a = this._logger) === null || _a === void 0 ? void 0 : _a.warn('Variation called before LaunchDarkly client initialization completed' + " (did you wait for the 'ready' event?) - using last known values from feature store"); this._variationInternal(flagKey, context, defaultValue, eventFactory, cb, typeChecker); return; } (_b = this._logger) === null || _b === void 0 ? void 0 : _b.warn('Variation called before LaunchDarkly client initialization completed (did you wait for the' + "'ready' event?) - using default value"); cb(EvalResult_1.default.forError(ErrorKinds.ClientNotReady, undefined, defaultValue)); }); return; } this._variationInternal(flagKey, context, defaultValue, eventFactory, cb, typeChecker); } _dataSourceErrorHandler(e) { var _a; const error = e.code === 401 ? new Error('Authentication failed. Double check your SDK key.') : e; this._onError(error); this._onFailed(error); if (!this.initialized()) { this._initState = InitState.Failed; this._rejectionReason = error; (_a = this._initReject) === null || _a === void 0 ? void 0 : _a.call(this, error); } } _initSuccess() { var _a; if (!this.initialized()) { this._initState = InitState.Initialized; (_a = this._initResolve) === null || _a === void 0 ? void 0 : _a.call(this, this); this._onReady(); } } /** * Apply a timeout promise to a base promise. This is for use with waitForInitialization. * Currently it returns a LDClient. In the future it should return a status. * * The client isn't always the expected type of the consumer. It returns an LDClient interface * which is less capable than, for example, the node client interface. * * @param basePromise The promise to race against a timeout. * @param timeout The timeout in seconds. * @param logger A logger to log when the timeout expires. * @returns */ _clientWithTimeout(basePromise, timeout, logger) { if (timeout) { const cancelableTimeout = (0, js_sdk_common_1.cancelableTimedPromise)(timeout, 'waitForInitialization'); return Promise.race([ basePromise.then(() => this), cancelableTimeout.promise.then(() => this), ]) .catch((reason) => { if (reason instanceof js_sdk_common_1.LDTimeoutError) { logger === null || logger === void 0 ? void 0 : logger.error(reason.message); } throw reason; }) .finally(() => cancelableTimeout.cancel()); } return basePromise; } } exports.default = LDClientImpl; //# sourceMappingURL=LDClientImpl.js.map