configcat-common
Version:
ConfigCat is a configuration as a service that lets you manage your features and configurations without actually deploying new code.
369 lines (368 loc) • 20.5 kB
JavaScript
import { __awaiter, __generator } from "tslib";
import { ExternalConfigCache, InMemoryConfigCache } from "./ConfigCatCache";
import { FetchError, FetchResult, FetchStatus } from "./ConfigFetcher";
import { RedirectMode } from "./ConfigJson";
import { Config, ProjectConfig } from "./ProjectConfig";
import { isPromiseLike } from "./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 !== 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 internal cache. */
export 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 || (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 __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.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 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 __awaiter(_this, void 0, void 0, function () {
var fetchResult, shouldUpdateCache;
return __generator(this, function (_a) {
switch (_a.label) {
case 0: return [4 /*yield*/, this.fetchAsync(latestConfig)];
case 1:
fetchResult = _a.sent();
shouldUpdateCache = fetchResult.status === FetchStatus.Fetched
|| fetchResult.status === 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 === 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 Config({}));
};
ConfigServiceBase.prototype.fetchAsync = 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.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 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*/, FetchResult.error(lastConfig, errorMessage, configOrError)];
}
options.logger.debug("ConfigServiceBase.fetchAsync(): 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.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*/, FetchResult.error(lastConfig, errorMessage)];
}
options.logger.debug("ConfigServiceBase.fetchAsync(): 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(options.sdkKey).toString();
options.logger.debug("ConfigServiceBase.fetchAsync(): 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.fetchAsync(): 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.fetchAsync(): 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.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 InMemoryConfigCache) {
return cache.get(this.cacheKey);
}
if (this.pendingCacheSyncUp) {
return this.pendingCacheSyncUp;
}
var syncResult = cache.get(this.cacheKey);
if (!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 __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 };