UNPKG

configcat-common

Version:

ConfigCat is a configuration as a service that lets you manage your features and configurations without actually deploying new code.

372 lines (371 loc) 21.1 kB
"use strict"; Object.defineProperty(exports, "__esModule", { value: true }); exports.ConfigServiceBase = exports.ClientCacheState = exports.RefreshResult = void 0; var tslib_1 = require("tslib"); var ConfigCatCache_1 = require("./ConfigCatCache"); var ConfigFetcher_1 = require("./ConfigFetcher"); var ConfigJson_1 = require("./ConfigJson"); var ProjectConfig_1 = require("./ProjectConfig"); var Utils_1 = require("./Utils"); /** Contains the result of an `IConfigCatClient.forceRefreshAsync` operation. */ var RefreshResult = /** @class */ (function () { function RefreshResult( /** Error message in case the operation failed, otherwise `null`. */ errorMessage, /** The exception object related to the error in case the operation failed (if any). */ errorException) { this.errorMessage = errorMessage; this.errorException = errorException; } Object.defineProperty(RefreshResult.prototype, "isSuccess", { /** Indicates whether the operation was successful or not. */ get: function () { return this.errorMessage === null; }, enumerable: false, configurable: true }); RefreshResult.from = function (fetchResult) { return fetchResult.status !== ConfigFetcher_1.FetchStatus.Errored ? RefreshResult.success() : RefreshResult.failure(fetchResult.errorMessage, fetchResult.errorException); }; /** Creates an instance of the `RefreshResult` class which indicates that the operation was successful. */ RefreshResult.success = function () { return new RefreshResult(null); }; /** Creates an instance of the `RefreshResult` class which indicates that the operation failed. */ RefreshResult.failure = function (errorMessage, errorException) { return new RefreshResult(errorMessage, errorException); }; return RefreshResult; }()); exports.RefreshResult = RefreshResult; /** Specifies the possible states of the internal cache. */ var ClientCacheState; (function (ClientCacheState) { /** No config data is available in the internal cache. */ ClientCacheState[ClientCacheState["NoFlagData"] = 0] = "NoFlagData"; /** Only config data provided by local flag override is available in the internal cache. */ ClientCacheState[ClientCacheState["HasLocalOverrideFlagDataOnly"] = 1] = "HasLocalOverrideFlagDataOnly"; /** Only expired config data obtained from the external cache or the ConfigCat CDN is available in the internal cache. */ ClientCacheState[ClientCacheState["HasCachedFlagDataOnly"] = 2] = "HasCachedFlagDataOnly"; /** Up-to-date config data obtained from the external cache or the ConfigCat CDN is available in the internal cache. */ ClientCacheState[ClientCacheState["HasUpToDateFlagData"] = 3] = "HasUpToDateFlagData"; })(ClientCacheState = exports.ClientCacheState || (exports.ClientCacheState = {})); var ConfigServiceStatus; (function (ConfigServiceStatus) { ConfigServiceStatus[ConfigServiceStatus["Online"] = 0] = "Online"; ConfigServiceStatus[ConfigServiceStatus["Offline"] = 1] = "Offline"; ConfigServiceStatus[ConfigServiceStatus["Disposed"] = 2] = "Disposed"; })(ConfigServiceStatus || (ConfigServiceStatus = {})); var ConfigServiceBase = /** @class */ (function () { function ConfigServiceBase(configFetcher, options) { this.configFetcher = configFetcher; this.options = options; this.pendingCacheSyncUp = null; this.pendingConfigRefresh = null; this.cacheKey = options.getCacheKey(); this.configFetcher = configFetcher; this.options = options; this.status = options.offline ? ConfigServiceStatus.Offline : ConfigServiceStatus.Online; } ConfigServiceBase.prototype.dispose = function () { this.status = ConfigServiceStatus.Disposed; }; Object.defineProperty(ConfigServiceBase.prototype, "disposed", { get: function () { return this.status === ConfigServiceStatus.Disposed; }, enumerable: false, configurable: true }); ConfigServiceBase.prototype.refreshConfigAsync = function () { return tslib_1.__awaiter(this, void 0, void 0, function () { var latestConfig, _a, fetchResult, config, errorMessage; return tslib_1.__generator(this, function (_b) { switch (_b.label) { case 0: return [4 /*yield*/, this.syncUpWithCache()]; case 1: latestConfig = _b.sent(); if (!!this.isOffline) return [3 /*break*/, 3]; return [4 /*yield*/, this.refreshConfigCoreAsync(latestConfig)]; case 2: _a = _b.sent(), fetchResult = _a[0], config = _a[1]; return [2 /*return*/, [RefreshResult.from(fetchResult), config]]; case 3: if (this.options.cache instanceof ConfigCatCache_1.ExternalConfigCache) { return [2 /*return*/, [RefreshResult.success(), latestConfig]]; } else { errorMessage = this.options.logger.configServiceCannotInitiateHttpCalls().toString(); return [2 /*return*/, [RefreshResult.failure(errorMessage), latestConfig]]; } _b.label = 4; case 4: return [2 /*return*/]; } }); }); }; ConfigServiceBase.prototype.refreshConfigCoreAsync = function (latestConfig) { var _this = this; if (this.pendingConfigRefresh) { // NOTE: Joiners may obtain more up-to-date config data from the external cache than the `latestConfig` // that was used to initiate the fetch operation. However, we ignore this possibility because we consider // the fetch operation result a more authentic source of truth. Although this may lead to overwriting // the cache with stale data, we expect this to be a temporary effect, which corrects itself eventually. return this.pendingConfigRefresh; } var configRefreshPromise = (function (latestConfig) { return tslib_1.__awaiter(_this, void 0, void 0, function () { var fetchResult, shouldUpdateCache; return tslib_1.__generator(this, function (_a) { switch (_a.label) { case 0: return [4 /*yield*/, this.fetchAsync(latestConfig)]; case 1: fetchResult = _a.sent(); shouldUpdateCache = fetchResult.status === ConfigFetcher_1.FetchStatus.Fetched || fetchResult.status === ConfigFetcher_1.FetchStatus.NotModified || fetchResult.config.timestamp > latestConfig.timestamp // is not transient error? && (!fetchResult.config.isEmpty || this.options.cache.getInMemory().isEmpty); if (!shouldUpdateCache) return [3 /*break*/, 3]; // NOTE: `ExternalConfigCache.set` makes sure that the external cache is not overwritten with empty // config data under any circumstances. return [4 /*yield*/, this.options.cache.set(this.cacheKey, fetchResult.config)]; case 2: // NOTE: `ExternalConfigCache.set` makes sure that the external cache is not overwritten with empty // config data under any circumstances. _a.sent(); latestConfig = fetchResult.config; _a.label = 3; case 3: this.onConfigFetched(fetchResult.config); if (fetchResult.status === ConfigFetcher_1.FetchStatus.Fetched) { this.onConfigChanged(fetchResult.config); } return [2 /*return*/, [fetchResult, latestConfig]]; } }); }); })(latestConfig); this.pendingConfigRefresh = configRefreshPromise; try { configRefreshPromise.finally(function () { return _this.pendingConfigRefresh = null; }); } catch (err) { this.pendingConfigRefresh = null; throw err; } return configRefreshPromise; }; ConfigServiceBase.prototype.onConfigFetched = function (newConfig) { }; ConfigServiceBase.prototype.onConfigChanged = function (newConfig) { var _a; this.options.logger.debug("config changed"); this.options.hooks.emit("configChanged", (_a = newConfig.config) !== null && _a !== void 0 ? _a : new ProjectConfig_1.Config({})); }; ConfigServiceBase.prototype.fetchAsync = function (lastConfig) { var _a; return tslib_1.__awaiter(this, void 0, void 0, function () { var options, errorMessage, _b, response, configOrError, err_1; return tslib_1.__generator(this, function (_c) { switch (_c.label) { case 0: options = this.options; options.logger.debug("ConfigServiceBase.fetchAsync() - called."); _c.label = 1; case 1: _c.trys.push([1, 3, , 4]); return [4 /*yield*/, this.fetchRequestAsync((_a = lastConfig.httpETag) !== null && _a !== void 0 ? _a : null)]; case 2: _b = _c.sent(), response = _b[0], configOrError = _b[1]; switch (response.statusCode) { case 200: // OK if (!(configOrError instanceof ProjectConfig_1.Config)) { errorMessage = options.logger.fetchReceived200WithInvalidBody(configOrError).toString(); options.logger.debug("ConfigServiceBase.fetchAsync(): " + response.statusCode + " " + response.reasonPhrase + " was received but the HTTP response content was invalid. Returning null."); return [2 /*return*/, ConfigFetcher_1.FetchResult.error(lastConfig, errorMessage, configOrError)]; } options.logger.debug("ConfigServiceBase.fetchAsync(): fetch was successful. Returning new config."); return [2 /*return*/, ConfigFetcher_1.FetchResult.success(new ProjectConfig_1.ProjectConfig(response.body, configOrError, ProjectConfig_1.ProjectConfig.generateTimestamp(), response.eTag))]; case 304: // Not Modified if (lastConfig.isEmpty) { errorMessage = options.logger.fetchReceived304WhenLocalCacheIsEmpty(response.statusCode, response.reasonPhrase).toString(); options.logger.debug("ConfigServiceBase.fetchAsync(): " + response.statusCode + " " + response.reasonPhrase + " was received when no config is cached locally. Returning null."); return [2 /*return*/, ConfigFetcher_1.FetchResult.error(lastConfig, errorMessage)]; } options.logger.debug("ConfigServiceBase.fetchAsync(): content was not modified. Returning last config with updated timestamp."); return [2 /*return*/, ConfigFetcher_1.FetchResult.notModified(lastConfig.with(ProjectConfig_1.ProjectConfig.generateTimestamp()))]; case 403: // Forbidden case 404: // Not Found errorMessage = options.logger.fetchFailedDueToInvalidSdkKey(options.sdkKey).toString(); options.logger.debug("ConfigServiceBase.fetchAsync(): fetch was unsuccessful. Returning last config (if any) with updated timestamp."); return [2 /*return*/, ConfigFetcher_1.FetchResult.error(lastConfig.with(ProjectConfig_1.ProjectConfig.generateTimestamp()), errorMessage)]; default: errorMessage = options.logger.fetchFailedDueToUnexpectedHttpResponse(response.statusCode, response.reasonPhrase).toString(); options.logger.debug("ConfigServiceBase.fetchAsync(): fetch was unsuccessful. Returning null."); return [2 /*return*/, ConfigFetcher_1.FetchResult.error(lastConfig, errorMessage)]; } return [3 /*break*/, 4]; case 3: err_1 = _c.sent(); errorMessage = (err_1 instanceof ConfigFetcher_1.FetchError && err_1.cause === "timeout" ? options.logger.fetchFailedDueToRequestTimeout(err_1.args[0], err_1) : options.logger.fetchFailedDueToUnexpectedError(err_1)).toString(); options.logger.debug("ConfigServiceBase.fetchAsync(): fetch was unsuccessful. Returning null."); return [2 /*return*/, ConfigFetcher_1.FetchResult.error(lastConfig, errorMessage, err_1)]; case 4: return [2 /*return*/]; } }); }); }; ConfigServiceBase.prototype.fetchRequestAsync = function (lastETag, maxRetryCount) { if (maxRetryCount === void 0) { maxRetryCount = 2; } return tslib_1.__awaiter(this, void 0, void 0, function () { var options, retryNumber, response, config, preferences, baseUrl, redirect; return tslib_1.__generator(this, function (_a) { switch (_a.label) { case 0: options = this.options; options.logger.debug("ConfigServiceBase.fetchRequestAsync() - called."); retryNumber = 0; _a.label = 1; case 1: options.logger.debug("ConfigServiceBase.fetchRequestAsync(): calling fetchLogic()" + (retryNumber > 0 ? ", retry " + retryNumber + "/" + maxRetryCount : "")); return [4 /*yield*/, this.configFetcher.fetchLogic(options, lastETag)]; case 2: response = _a.sent(); if (response.statusCode !== 200) { return [2 /*return*/, [response]]; } if (!response.body) { options.logger.debug("ConfigServiceBase.fetchRequestAsync(): no response body."); return [2 /*return*/, [response, new Error("No response body.")]]; } config = void 0; try { config = ProjectConfig_1.Config.deserialize(response.body); } catch (err) { options.logger.debug("ConfigServiceBase.fetchRequestAsync(): invalid response body."); return [2 /*return*/, [response, err]]; } preferences = config.preferences; if (!preferences) { options.logger.debug("ConfigServiceBase.fetchRequestAsync(): preferences is empty."); return [2 /*return*/, [response, config]]; } baseUrl = preferences.baseUrl; // If the base_url is the same as the last called one, just return the response. if (!baseUrl || baseUrl === options.baseUrl) { options.logger.debug("ConfigServiceBase.fetchRequestAsync(): baseUrl OK."); return [2 /*return*/, [response, config]]; } redirect = preferences.redirectMode; // If the base_url is overridden, and the redirect parameter is not 2 (force), // the SDK should not redirect the calls and it just have to return the response. if (options.baseUrlOverriden && redirect !== ConfigJson_1.RedirectMode.Force) { options.logger.debug("ConfigServiceBase.fetchRequestAsync(): options.baseUrlOverriden && redirect !== 2."); return [2 /*return*/, [response, config]]; } options.baseUrl = baseUrl; if (redirect === ConfigJson_1.RedirectMode.No) { return [2 /*return*/, [response, config]]; } if (redirect === ConfigJson_1.RedirectMode.Should) { options.logger.dataGovernanceIsOutOfSync(); } if (retryNumber >= maxRetryCount) { options.logger.fetchFailedDueToRedirectLoop(); return [2 /*return*/, [response, config]]; } _a.label = 3; case 3: retryNumber++; return [3 /*break*/, 1]; case 4: return [2 /*return*/]; } }); }); }; Object.defineProperty(ConfigServiceBase.prototype, "isOfflineExactly", { get: function () { return this.status === ConfigServiceStatus.Offline; }, enumerable: false, configurable: true }); Object.defineProperty(ConfigServiceBase.prototype, "isOffline", { get: function () { return this.status !== ConfigServiceStatus.Online; }, enumerable: false, configurable: true }); ConfigServiceBase.prototype.goOnline = function () { }; ConfigServiceBase.prototype.setOnline = function () { if (this.status === ConfigServiceStatus.Offline) { this.goOnline(); this.status = ConfigServiceStatus.Online; this.options.logger.configServiceStatusChanged(ConfigServiceStatus[this.status]); } else if (this.disposed) { this.options.logger.configServiceMethodHasNoEffectDueToDisposedClient("setOnline"); } }; ConfigServiceBase.prototype.setOffline = function () { if (this.status === ConfigServiceStatus.Online) { this.status = ConfigServiceStatus.Offline; this.options.logger.configServiceStatusChanged(ConfigServiceStatus[this.status]); } else if (this.disposed) { this.options.logger.configServiceMethodHasNoEffectDueToDisposedClient("setOffline"); } }; ConfigServiceBase.prototype.syncUpWithCache = function () { var _this = this; var cache = this.options.cache; if (cache instanceof ConfigCatCache_1.InMemoryConfigCache) { return cache.get(this.cacheKey); } if (this.pendingCacheSyncUp) { return this.pendingCacheSyncUp; } var syncResult = cache.get(this.cacheKey); if (!Utils_1.isPromiseLike(syncResult)) { return this.onCacheSynced(syncResult); } var cacheSyncUpPromise = syncResult.then(function (syncResult) { return _this.onCacheSynced(syncResult); }); this.pendingCacheSyncUp = cacheSyncUpPromise; try { cacheSyncUpPromise.finally(function () { return _this.pendingCacheSyncUp = null; }); } catch (err) { this.pendingCacheSyncUp = null; throw err; } return cacheSyncUpPromise; }; ConfigServiceBase.prototype.onCacheSynced = function (syncResult) { if (!Array.isArray(syncResult)) { return syncResult; } var newConfig = syncResult[0]; if (!newConfig.isEmpty) { this.onConfigChanged(newConfig); } return newConfig; }; ConfigServiceBase.prototype.getReadyPromise = function (state, waitForReadyAsync) { return tslib_1.__awaiter(this, void 0, void 0, function () { var cacheState; return tslib_1.__generator(this, function (_a) { switch (_a.label) { case 0: return [4 /*yield*/, waitForReadyAsync(state)]; case 1: cacheState = _a.sent(); this.options.hooks.emit("clientReady", cacheState); return [2 /*return*/, cacheState]; } }); }); }; return ConfigServiceBase; }()); exports.ConfigServiceBase = ConfigServiceBase;