UNPKG

@devcycle/nodejs-server-sdk

Version:

The DevCycle NodeJS Server SDK used for feature management.

237 lines 9.91 kB
"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