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
JavaScript
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 };