UNPKG

configcat-common

Version:

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

323 lines (322 loc) 18 kB
import { __awaiter, __generator } from "tslib"; import { FetchError, FetchResult, FetchStatus } from "./ConfigFetcher"; import { RedirectMode } from "./ConfigJson"; import { Config, ProjectConfig } from "./ProjectConfig"; /** Contains the result of an `IConfigCatClient.forceRefresh` or `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 !== 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; }()); export { RefreshResult }; /** Specifies the possible states of the local cache. */ export var ClientCacheState; (function (ClientCacheState) { ClientCacheState[ClientCacheState["NoFlagData"] = 0] = "NoFlagData"; ClientCacheState[ClientCacheState["HasLocalOverrideFlagDataOnly"] = 1] = "HasLocalOverrideFlagDataOnly"; ClientCacheState[ClientCacheState["HasCachedFlagDataOnly"] = 2] = "HasCachedFlagDataOnly"; ClientCacheState[ClientCacheState["HasUpToDateFlagData"] = 3] = "HasUpToDateFlagData"; })(ClientCacheState || (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.pendingFetch = 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 __awaiter(this, void 0, void 0, function () { var latestConfig, _a, fetchResult, config, errorMessage; return __generator(this, function (_b) { switch (_b.label) { case 0: return [4 /*yield*/, this.options.cache.get(this.cacheKey)]; 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: errorMessage = this.options.logger.configServiceCannotInitiateHttpCalls().toString(); return [2 /*return*/, [RefreshResult.failure(errorMessage), latestConfig]]; } }); }); }; ConfigServiceBase.prototype.refreshConfigCoreAsync = function (latestConfig) { return __awaiter(this, void 0, void 0, function () { var fetchResult, configChanged, success; return __generator(this, function (_a) { switch (_a.label) { case 0: return [4 /*yield*/, this.fetchAsync(latestConfig)]; case 1: fetchResult = _a.sent(); configChanged = false; success = fetchResult.status === FetchStatus.Fetched; if (!(success || fetchResult.config.timestamp > latestConfig.timestamp && (!fetchResult.config.isEmpty || latestConfig.isEmpty))) return [3 /*break*/, 3]; return [4 /*yield*/, this.options.cache.set(this.cacheKey, fetchResult.config)]; case 2: _a.sent(); configChanged = success && !ProjectConfig.equals(fetchResult.config, latestConfig); latestConfig = fetchResult.config; _a.label = 3; case 3: this.onConfigFetched(fetchResult.config); if (configChanged) { this.onConfigChanged(fetchResult.config); } return [2 /*return*/, [fetchResult, latestConfig]]; } }); }); }; 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 Config({})); }; ConfigServiceBase.prototype.fetchAsync = function (lastConfig) { var _this = this; var _a; return (_a = this.pendingFetch) !== null && _a !== void 0 ? _a : (this.pendingFetch = (function () { return __awaiter(_this, void 0, void 0, function () { return __generator(this, function (_a) { switch (_a.label) { case 0: _a.trys.push([0, , 2, 3]); return [4 /*yield*/, this.fetchLogicAsync(lastConfig)]; case 1: return [2 /*return*/, _a.sent()]; case 2: this.pendingFetch = null; return [7 /*endfinally*/]; case 3: return [2 /*return*/]; } }); }); })()); }; ConfigServiceBase.prototype.fetchLogicAsync = function (lastConfig) { var _a; return __awaiter(this, void 0, void 0, function () { var options, errorMessage, _b, response, configOrError, err_1; return __generator(this, function (_c) { switch (_c.label) { case 0: options = this.options; options.logger.debug("ConfigServiceBase.fetchLogicAsync() - 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 Config)) { errorMessage = options.logger.fetchReceived200WithInvalidBody(configOrError).toString(); options.logger.debug("ConfigServiceBase.fetchLogicAsync(): " + response.statusCode + " " + response.reasonPhrase + " was received but the HTTP response content was invalid. Returning null."); return [2 /*return*/, FetchResult.error(lastConfig, errorMessage, configOrError)]; } options.logger.debug("ConfigServiceBase.fetchLogicAsync(): fetch was successful. Returning new config."); return [2 /*return*/, FetchResult.success(new ProjectConfig(response.body, configOrError, ProjectConfig.generateTimestamp(), response.eTag))]; case 304: // Not Modified if (!lastConfig) { errorMessage = options.logger.fetchReceived304WhenLocalCacheIsEmpty(response.statusCode, response.reasonPhrase).toString(); options.logger.debug("ConfigServiceBase.fetchLogicAsync(): " + response.statusCode + " " + response.reasonPhrase + " was received when no config is cached locally. Returning null."); return [2 /*return*/, FetchResult.error(lastConfig, errorMessage)]; } options.logger.debug("ConfigServiceBase.fetchLogicAsync(): content was not modified. Returning last config with updated timestamp."); return [2 /*return*/, FetchResult.notModified(lastConfig.with(ProjectConfig.generateTimestamp()))]; case 403: // Forbidden case 404: // Not Found errorMessage = options.logger.fetchFailedDueToInvalidSdkKey().toString(); options.logger.debug("ConfigServiceBase.fetchLogicAsync(): fetch was unsuccessful. Returning last config (if any) with updated timestamp."); return [2 /*return*/, FetchResult.error(lastConfig.with(ProjectConfig.generateTimestamp()), errorMessage)]; default: errorMessage = options.logger.fetchFailedDueToUnexpectedHttpResponse(response.statusCode, response.reasonPhrase).toString(); options.logger.debug("ConfigServiceBase.fetchLogicAsync(): fetch was unsuccessful. Returning null."); return [2 /*return*/, FetchResult.error(lastConfig, errorMessage)]; } return [3 /*break*/, 4]; case 3: err_1 = _c.sent(); errorMessage = (err_1 instanceof FetchError && err_1.cause === "timeout" ? options.logger.fetchFailedDueToRequestTimeout(err_1.args[0], err_1) : options.logger.fetchFailedDueToUnexpectedError(err_1)).toString(); options.logger.debug("ConfigServiceBase.fetchLogicAsync(): fetch was unsuccessful. Returning null."); return [2 /*return*/, FetchResult.error(lastConfig, errorMessage, err_1)]; case 4: return [2 /*return*/]; } }); }); }; ConfigServiceBase.prototype.fetchRequestAsync = function (lastETag, maxRetryCount) { if (maxRetryCount === void 0) { maxRetryCount = 2; } return __awaiter(this, void 0, void 0, function () { var options, retryNumber, response, config, preferences, baseUrl, redirect; return __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 = 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 !== RedirectMode.Force) { options.logger.debug("ConfigServiceBase.fetchRequestAsync(): options.baseUrlOverriden && redirect !== 2."); return [2 /*return*/, [response, config]]; } options.baseUrl = baseUrl; if (redirect === RedirectMode.No) { return [2 /*return*/, [response, config]]; } if (redirect === 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.setOnlineCore = function () { }; ConfigServiceBase.prototype.setOnline = function () { if (this.status === ConfigServiceStatus.Offline) { this.setOnlineCore(); this.status = ConfigServiceStatus.Online; this.options.logger.configServiceStatusChanged(ConfigServiceStatus[this.status]); } else if (this.disposed) { this.options.logger.configServiceMethodHasNoEffectDueToDisposedClient("setOnline"); } }; ConfigServiceBase.prototype.setOfflineCore = function () { }; ConfigServiceBase.prototype.setOffline = function () { if (this.status === ConfigServiceStatus.Online) { this.setOfflineCore(); this.status = ConfigServiceStatus.Offline; this.options.logger.configServiceStatusChanged(ConfigServiceStatus[this.status]); } else if (this.disposed) { this.options.logger.configServiceMethodHasNoEffectDueToDisposedClient("setOffline"); } }; ConfigServiceBase.prototype.syncUpWithCache = function () { return this.options.cache.get(this.cacheKey); }; ConfigServiceBase.prototype.getReadyPromise = function (state, waitForReadyAsync) { return __awaiter(this, void 0, void 0, function () { var cacheState; return __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; }()); export { ConfigServiceBase };