@devcycle/nodejs-server-sdk
Version:
The DevCycle NodeJS Server SDK used for feature management.
237 lines • 9.91 kB
JavaScript
"use strict";
Object.defineProperty(exports, "__esModule", { value: true });
exports.EnvironmentConfigManager = void 0;
const types_1 = require("@devcycle/types");
const sse_connection_1 = require("../../../../sse-connection/src");
const CDNConfigSource_1 = require("./CDNConfigSource");
const request_1 = require("./request");
class EnvironmentConfigManager {
constructor(logger, sdkKey, setConfigBuffer, setInterval, clearInterval, trackSDKConfigEvent, { configPollingIntervalMS = 10000, sseConfigPollingIntervalMS = 10 * 60 * 1000, // 10 minutes
configPollingTimeoutMS = 5000, configCDNURI = 'https://config-cdn.devcycle.com', clientMode = false, disableRealTimeUpdates = false, }, configSource) {
this.logger = logger;
this.sdkKey = sdkKey;
this.setConfigBuffer = setConfigBuffer;
this.setInterval = setInterval;
this.clearInterval = clearInterval;
this.trackSDKConfigEvent = trackSDKConfigEvent;
this._hasConfig = false;
this.clientMode = clientMode;
this.enableRealtimeUpdates = !disableRealTimeUpdates;
this.configPollingIntervalMS =
configPollingIntervalMS >= 1000 ? configPollingIntervalMS : 1000;
this.sseConfigPollingIntervalMS =
sseConfigPollingIntervalMS <= 60 * 1000
? 10 * 60 * 1000
: sseConfigPollingIntervalMS;
this.requestTimeoutMS =
configPollingTimeoutMS >= this.configPollingIntervalMS
? this.configPollingIntervalMS
: configPollingTimeoutMS;
this.configSource =
configSource !== null && configSource !== void 0 ? configSource : new CDNConfigSource_1.CDNConfigSource(configCDNURI, logger, this.requestTimeoutMS);
this.fetchConfigPromise = this._fetchConfig()
.then(() => {
this.logger.debug('DevCycle initial config loaded');
})
.finally(() => {
this.startPolling(this.configPollingIntervalMS);
this.startSSE();
});
}
startSSE() {
if (!this.enableRealtimeUpdates)
return;
if (!this.configSSE) {
this.logger.warn('No SSE configuration found');
return;
}
if (this.sseConnection) {
return;
}
const url = new URL(this.configSSE.path, this.configSSE.hostname).toString();
this.logger.debug(`Starting SSE connection to ${url}`);
this.sseConnection = new sse_connection_1.SSEConnection(url, this.logger, {
onMessage: this.onSSEMessage.bind(this),
onOpen: () => {
this.logger.debug('SSE connection opened');
// Set config polling interval to 10 minutes
this.startPolling(this.sseConfigPollingIntervalMS);
},
onConnectionError: () => {
this.logger.debug('SSE connection error, switching to polling');
// reset polling interval to default
this.startPolling(this.configPollingIntervalMS);
this.stopSSE();
},
});
}
onSSEMessage(message) {
this.logger.debug(`SSE message: ${message}`);
try {
const parsedMessage = JSON.parse(message);
const messageData = JSON.parse(parsedMessage.data);
if (!messageData)
return;
const { type, etag, lastModified } = messageData;
if (!(!type || type === 'refetchConfig')) {
return;
}
if (this.configEtag && etag === this.configEtag) {
return;
}
if (this.isLastModifiedHeaderOld(lastModified)) {
this.logger.debug('Skipping SSE message, config last modified is newer. ');
return;
}
this._fetchConfig(lastModified)
.then(() => {
this.logger.debug('Config re-fetched from SSE message');
})
.catch((e) => {
this.logger.warn(`Failed to re-fetch config from SSE Message: ${e}`);
});
}
catch (e) {
this.logger.debug(`SSE Message Error: Unparseable message. Error: ${e}, message: ${message}`);
}
}
stopSSE() {
if (this.sseConnection) {
this.sseConnection.close();
this.sseConnection = undefined;
}
}
startPolling(pollingInterval) {
if (this.intervalTimeout) {
if (pollingInterval === this.currentPollingInterval) {
return;
}
// clear existing polling interval
this.stopPolling();
}
this.intervalTimeout = this.setInterval(async () => {
try {
await this._fetchConfig();
}
catch (ex) {
this.logger.error(ex.message);
}
}, pollingInterval);
this.currentPollingInterval = pollingInterval;
}
get hasConfig() {
return this._hasConfig;
}
get configEtag() {
return this.configSource.configEtag;
}
stopPolling() {
this.clearInterval(this.intervalTimeout);
this.intervalTimeout = null;
}
cleanup() {
this.stopPolling();
this.stopSSE();
}
async _fetchConfig(sseLastModified) {
const url = this.configSource.getConfigURL(this.sdkKey, this.clientMode ? 'bootstrap' : 'server', false);
let projectConfig = null;
let retrievalMetadata;
const startTime = Date.now();
let responseTimeMS = 0;
const currentEtag = this.configSource.configEtag;
const currentLastModified = this.configSource.configLastModified;
const logError = (error) => {
const errMsg = `Request to get config failed for url: ${url}, ` +
`response message: ${error.message}, response data: ${projectConfig}`;
if (this._hasConfig) {
this.logger.warn(errMsg);
}
else {
this.logger.error(errMsg);
}
};
const trackEvent = (err) => {
var _a, _b;
if (projectConfig || err) {
this.trackSDKConfigEvent(url, responseTimeMS, retrievalMetadata, err, currentEtag, currentLastModified, (_b = (_a = this.sseConnection) === null || _a === void 0 ? void 0 : _a.isConnected()) !== null && _b !== void 0 ? _b : false);
}
};
try {
this.logger.debug(`Requesting new config for ${url}, etag: ${this.configSource.configEtag}` +
`, last-modified: ${this.configSource.configLastModified}`);
({ config: projectConfig, metaData: retrievalMetadata } =
await this.configSource.getConfig(this.sdkKey, this.clientMode ? 'bootstrap' : 'server', false, sseLastModified));
responseTimeMS = Date.now() - startTime;
// if no errors occurred, the projectConfig is either new or null (meaning cached version is used)
// either way, trigger the SSE config handler to see if we need to reconnect
this.handleSSEConfig(projectConfig !== null && projectConfig !== void 0 ? projectConfig : undefined);
}
catch (ex) {
if (this.hasConfig) {
// TODO currently event queue in WASM requires a valid config
// switch this to hit the events API directly
trackEvent(ex);
}
logError(ex);
if (ex instanceof types_1.UserError) {
this.cleanup();
throw ex;
}
else if (this._hasConfig) {
this.logger.warn(`Failed to download config, using cached version. url: ${url}.`);
}
}
if (projectConfig) {
try {
this.setConfigBuffer(`${this.sdkKey}${this.clientMode ? '_client' : ''}`, JSON.stringify(projectConfig));
this._hasConfig = true;
return;
}
catch (e) {
logError(new Error('Invalid config JSON.'));
}
finally {
trackEvent();
}
}
if (!this._hasConfig) {
throw new Error('Failed to download DevCycle config.');
}
}
isLastModifiedHeaderOld(lastModifiedHeader) {
const lastModifiedHeaderDate = lastModifiedHeader
? new Date(lastModifiedHeader)
: null;
const configLastModifiedDate = this.configSource.configLastModified
? new Date(this.configSource.configLastModified)
: null;
return ((0, request_1.isValidDate)(configLastModifiedDate) &&
(0, request_1.isValidDate)(lastModifiedHeaderDate) &&
lastModifiedHeaderDate <= configLastModifiedDate);
}
handleSSEConfig(configBody) {
var _a, _b;
if (this.enableRealtimeUpdates) {
const originalConfigSSE = this.configSSE;
if (configBody) {
this.configSSE = configBody.sse;
}
// Reconnect SSE if not first config fetch, and the SSE config has changed
if (this.hasConfig &&
(!originalConfigSSE ||
!this.sseConnection ||
originalConfigSSE.hostname !== ((_a = this.configSSE) === null || _a === void 0 ? void 0 : _a.hostname) ||
originalConfigSSE.path !== ((_b = this.configSSE) === null || _b === void 0 ? void 0 : _b.path))) {
this.stopSSE();
this.startSSE();
}
}
else {
this.configSSE = undefined;
this.stopSSE();
}
}
}
exports.EnvironmentConfigManager = EnvironmentConfigManager;
//# sourceMappingURL=index.js.map