@devcycle/nodejs-server-sdk
Advanced tools
Comparing version 1.30.2 to 1.31.0
@@ -7,2 +7,3 @@ import { RequestInitWithRetry } from 'fetch-retry'; | ||
export declare const exponentialBackoff: RequestInitWithRetry['retryDelay']; | ||
export type RequestInitConfig = RequestInit | RequestInitWithRetry; | ||
export declare function handleResponse(res: Response): Promise<Response>; | ||
@@ -9,0 +10,0 @@ export declare function getWithTimeout(url: string, requestConfig: RequestInit | RequestInitWithRetry, timeout: number): Promise<Response>; |
@@ -119,3 +119,4 @@ "use strict"; | ||
const newConfig = { ...requestConfig }; | ||
newConfig.retryOn = retryOnRequestError(requestConfig.retries); | ||
newConfig.retryOn = | ||
newConfig.retryOn || retryOnRequestError(requestConfig.retries); | ||
newConfig.retryDelay = exports.exponentialBackoff; | ||
@@ -122,0 +123,0 @@ return [await getFetchWithRetry(), newConfig]; |
@@ -7,2 +7,3 @@ import { RequestInitWithRetry } from 'fetch-retry'; | ||
export declare const exponentialBackoff: RequestInitWithRetry['retryDelay']; | ||
export type RequestInitConfig = RequestInit | RequestInitWithRetry; | ||
export declare function handleResponse(res: Response): Promise<Response>; | ||
@@ -9,0 +10,0 @@ export declare function getWithTimeout(url: string, requestConfig: RequestInit | RequestInitWithRetry, timeout: number): Promise<Response>; |
@@ -119,3 +119,4 @@ "use strict"; | ||
const newConfig = { ...requestConfig }; | ||
newConfig.retryOn = retryOnRequestError(requestConfig.retries); | ||
newConfig.retryOn = | ||
newConfig.retryOn || retryOnRequestError(requestConfig.retries); | ||
newConfig.retryDelay = exports.exponentialBackoff; | ||
@@ -122,0 +123,0 @@ return [await getFetchWithRetry(), newConfig]; |
@@ -1,5 +0,6 @@ | ||
import { DVCLogger } from '@devcycle/types'; | ||
import { ConfigBody, DVCLogger } from '@devcycle/types'; | ||
import { ResponseError } from "../server-request/src"; | ||
type ConfigPollingOptions = { | ||
configPollingIntervalMS?: number; | ||
sseConfigPollingIntervalMS?: number; | ||
configPollingTimeoutMS?: number; | ||
@@ -9,2 +10,3 @@ configCDNURI?: string; | ||
clientMode?: boolean; | ||
enableBetaRealTimeUpdates?: boolean; | ||
}; | ||
@@ -14,3 +16,3 @@ type SetIntervalInterface = (handler: () => void, timeout?: number) => any; | ||
type SetConfigBufferInterface = (sdkKey: string, projectConfig: string) => void; | ||
type TrackSDKConfigEventInterface = (url: string, responseTimeMS: number, res?: Response, err?: ResponseError, reqEtag?: string, reqLastModified?: string) => void; | ||
type TrackSDKConfigEventInterface = (url: string, responseTimeMS: number, res?: Response, err?: ResponseError, reqEtag?: string, reqLastModified?: string, sseConnected?: boolean) => void; | ||
export declare class EnvironmentConfigManager { | ||
@@ -26,16 +28,27 @@ private readonly logger; | ||
configLastModified?: string; | ||
private readonly pollingIntervalMS; | ||
configSSE?: ConfigBody<string>['sse']; | ||
private currentPollingInterval; | ||
private readonly configPollingIntervalMS; | ||
private readonly sseConfigPollingIntervalMS; | ||
private readonly requestTimeoutMS; | ||
private readonly cdnURI; | ||
private readonly enableRealtimeUpdates; | ||
fetchConfigPromise: Promise<void>; | ||
private intervalTimeout?; | ||
private disablePolling; | ||
private clientMode; | ||
constructor(logger: DVCLogger, sdkKey: string, setConfigBuffer: SetConfigBufferInterface, setInterval: SetIntervalInterface, clearInterval: ClearIntervalInterface, trackSDKConfigEvent: TrackSDKConfigEventInterface, { configPollingIntervalMS, configPollingTimeoutMS, configCDNURI, cdnURI, clientMode, }: ConfigPollingOptions); | ||
private sseConnection?; | ||
constructor(logger: DVCLogger, sdkKey: string, setConfigBuffer: SetConfigBufferInterface, setInterval: SetIntervalInterface, clearInterval: ClearIntervalInterface, trackSDKConfigEvent: TrackSDKConfigEventInterface, { configPollingIntervalMS, sseConfigPollingIntervalMS, // 10 minutes | ||
configPollingTimeoutMS, configCDNURI, cdnURI, clientMode, enableBetaRealTimeUpdates, }: ConfigPollingOptions); | ||
private startSSE; | ||
private onSSEMessage; | ||
private stopSSE; | ||
private startPolling; | ||
get hasConfig(): boolean; | ||
stopPolling(): void; | ||
private stopPolling; | ||
cleanup(): void; | ||
getConfigURL(): string; | ||
_fetchConfig(): Promise<void>; | ||
_fetchConfig(sseLastModified?: string): Promise<void>; | ||
private isLastModifiedHeaderOld; | ||
private handleSSEConfig; | ||
} | ||
export {}; |
@@ -6,4 +6,6 @@ "use strict"; | ||
const server_request_1 = require("../server-request/src"); | ||
const sse_connection_1 = require("../sse-connection/src"); | ||
class EnvironmentConfigManager { | ||
constructor(logger, sdkKey, setConfigBuffer, setInterval, clearInterval, trackSDKConfigEvent, { configPollingIntervalMS = 10000, configPollingTimeoutMS = 5000, configCDNURI, cdnURI = 'https://config-cdn.devcycle.com', clientMode = false, }) { | ||
constructor(logger, sdkKey, setConfigBuffer, setInterval, clearInterval, trackSDKConfigEvent, { configPollingIntervalMS = 10000, sseConfigPollingIntervalMS = 10 * 60 * 1000, // 10 minutes | ||
configPollingTimeoutMS = 5000, configCDNURI, cdnURI = 'https://config-cdn.devcycle.com', clientMode = false, enableBetaRealTimeUpdates = false, }) { | ||
this.logger = logger; | ||
@@ -16,9 +18,13 @@ this.sdkKey = sdkKey; | ||
this._hasConfig = false; | ||
this.disablePolling = false; | ||
this.clientMode = clientMode; | ||
this.pollingIntervalMS = | ||
this.enableRealtimeUpdates = enableBetaRealTimeUpdates; | ||
this.configPollingIntervalMS = | ||
configPollingIntervalMS >= 1000 ? configPollingIntervalMS : 1000; | ||
this.sseConfigPollingIntervalMS = | ||
sseConfigPollingIntervalMS <= 60 * 1000 | ||
? 10 * 60 * 1000 | ||
: sseConfigPollingIntervalMS; | ||
this.requestTimeoutMS = | ||
configPollingTimeoutMS >= this.pollingIntervalMS | ||
? this.pollingIntervalMS | ||
configPollingTimeoutMS >= this.configPollingIntervalMS | ||
? this.configPollingIntervalMS | ||
: configPollingTimeoutMS; | ||
@@ -31,15 +37,87 @@ this.cdnURI = configCDNURI || cdnURI; | ||
.finally(() => { | ||
if (this.disablePolling) { | ||
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; | ||
} | ||
this.intervalTimeout = this.setInterval(async () => { | ||
try { | ||
await this._fetchConfig(); | ||
} | ||
catch (ex) { | ||
this.logger.error(ex.message); | ||
} | ||
}, this.pollingIntervalMS); | ||
}); | ||
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() { | ||
@@ -49,7 +127,8 @@ return this._hasConfig; | ||
stopPolling() { | ||
this.disablePolling = true; | ||
this.clearInterval(this.intervalTimeout); | ||
this.intervalTimeout = null; | ||
} | ||
cleanup() { | ||
this.stopPolling(); | ||
this.stopSSE(); | ||
} | ||
@@ -62,3 +141,3 @@ getConfigURL() { | ||
} | ||
async _fetchConfig() { | ||
async _fetchConfig(sseLastModified) { | ||
const url = this.getConfigURL(); | ||
@@ -70,4 +149,4 @@ let res; | ||
let responseTimeMS = 0; | ||
const reqEtag = this.configEtag; | ||
const reqLastModified = this.configLastModified; | ||
const currentEtag = this.configEtag; | ||
const currentLastModified = this.configLastModified; | ||
const logError = (error) => { | ||
@@ -84,4 +163,5 @@ const errMsg = `Request to get config failed for url: ${url}, ` + | ||
const trackEvent = (err) => { | ||
var _a, _b; | ||
if ((res && (res === null || res === void 0 ? void 0 : res.status) !== 304) || err) { | ||
this.trackSDKConfigEvent(url, responseTimeMS, res || undefined, err, reqEtag, reqLastModified); | ||
this.trackSDKConfigEvent(url, responseTimeMS, res || undefined, err, currentEtag, currentLastModified, (_b = (_a = this.sseConnection) === null || _a === void 0 ? void 0 : _a.isConnected()) !== null && _b !== void 0 ? _b : false); | ||
} | ||
@@ -92,3 +172,10 @@ }; | ||
`, last-modified: ${this.configLastModified}`); | ||
res = await (0, request_1.getEnvironmentConfig)(url, this.requestTimeoutMS, reqEtag, reqLastModified); | ||
res = await (0, request_1.getEnvironmentConfig)({ | ||
logger: this.logger, | ||
url, | ||
requestTimeout: this.requestTimeoutMS, | ||
currentEtag, | ||
currentLastModified, | ||
sseLastModified, | ||
}); | ||
responseTimeMS = Date.now() - startTime; | ||
@@ -112,8 +199,13 @@ projectConfig = await res.text(); | ||
else if ((res === null || res === void 0 ? void 0 : res.status) === 200 && projectConfig) { | ||
const lastModifiedHeader = res === null || res === void 0 ? void 0 : res.headers.get('last-modified'); | ||
if (this.isLastModifiedHeaderOld(lastModifiedHeader)) { | ||
this.logger.debug('Skipping saving config, existing last modified date is newer.'); | ||
return; | ||
} | ||
try { | ||
this.handleSSEConfig(projectConfig); | ||
this.setConfigBuffer(`${this.sdkKey}${this.clientMode ? '_client' : ''}`, projectConfig); | ||
this._hasConfig = true; | ||
this.configEtag = (res === null || res === void 0 ? void 0 : res.headers.get('etag')) || ''; | ||
this.configLastModified = | ||
(res === null || res === void 0 ? void 0 : res.headers.get('last-modified')) || ''; | ||
this.configLastModified = lastModifiedHeader || ''; | ||
return; | ||
@@ -133,3 +225,3 @@ } | ||
else if ((responseError === null || responseError === void 0 ? void 0 : responseError.status) === 403) { | ||
this.stopPolling(); | ||
this.cleanup(); | ||
throw new server_request_1.UserError(`Invalid SDK key provided: ${this.sdkKey}`); | ||
@@ -141,4 +233,36 @@ } | ||
} | ||
isLastModifiedHeaderOld(lastModifiedHeader) { | ||
const lastModifiedHeaderDate = lastModifiedHeader | ||
? new Date(lastModifiedHeader) | ||
: null; | ||
const configLastModifiedDate = this.configLastModified | ||
? new Date(this.configLastModified) | ||
: null; | ||
return ((0, request_1.isValidDate)(configLastModifiedDate) && | ||
(0, request_1.isValidDate)(lastModifiedHeaderDate) && | ||
lastModifiedHeaderDate <= configLastModifiedDate); | ||
} | ||
handleSSEConfig(projectConfig) { | ||
var _a, _b; | ||
if (this.enableRealtimeUpdates) { | ||
const configBody = JSON.parse(projectConfig); | ||
const originalConfigSSE = this.configSSE; | ||
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 |
@@ -1,1 +0,10 @@ | ||
export declare function getEnvironmentConfig(url: string, requestTimeout: number, etag?: string, lastModified?: string): Promise<Response>; | ||
import { DVCLogger } from '@devcycle/types'; | ||
export declare const isValidDate: (date: Date | null) => date is Date; | ||
export declare function getEnvironmentConfig({ logger, url, requestTimeout, currentEtag, currentLastModified, sseLastModified, }: { | ||
logger: DVCLogger; | ||
url: string; | ||
requestTimeout: number; | ||
currentEtag?: string; | ||
currentLastModified?: string; | ||
sseLastModified?: string; | ||
}): Promise<Response>; |
"use strict"; | ||
Object.defineProperty(exports, "__esModule", { value: true }); | ||
exports.getEnvironmentConfig = void 0; | ||
exports.getEnvironmentConfig = exports.isValidDate = void 0; | ||
const server_request_1 = require("../server-request/src"); | ||
async function getEnvironmentConfig(url, requestTimeout, etag, lastModified) { | ||
const isValidDate = (date) => date instanceof Date && !isNaN(date.getTime()); | ||
exports.isValidDate = isValidDate; | ||
async function getEnvironmentConfig({ logger, url, requestTimeout, currentEtag, currentLastModified, sseLastModified, }) { | ||
const headers = {}; | ||
if (etag) { | ||
headers['If-None-Match'] = etag; | ||
let retries = 1; | ||
let retryOn; | ||
const sseLastModifiedDate = sseLastModified | ||
? new Date(sseLastModified) | ||
: null; | ||
// Retry fetching config if the Last-Modified header is older than | ||
// the requiredLastModified from the SSE message | ||
if (sseLastModified && (0, exports.isValidDate)(sseLastModifiedDate)) { | ||
retries = 3; | ||
retryOn = (attempt, error, response) => { | ||
if (attempt >= retries) { | ||
return false; | ||
} | ||
else if (response && (response === null || response === void 0 ? void 0 : response.status) === 200) { | ||
const lastModifiedHeader = response.headers.get('Last-Modified'); | ||
const lastModifiedHeaderDate = lastModifiedHeader | ||
? new Date(lastModifiedHeader) | ||
: null; | ||
if ((0, exports.isValidDate)(lastModifiedHeaderDate) && | ||
lastModifiedHeaderDate < sseLastModifiedDate) { | ||
logger.debug(`Retry fetching config, last modified is old: ${lastModifiedHeader}` + | ||
`, sse last modified: ${sseLastModified}`); | ||
return true; | ||
} | ||
return false; | ||
} | ||
else if (response && (response === null || response === void 0 ? void 0 : response.status) < 500) { | ||
return false; | ||
} | ||
return true; | ||
}; | ||
} | ||
if (lastModified) { | ||
headers['If-Modified-Since'] = lastModified; | ||
if (currentEtag) { | ||
headers['If-None-Match'] = currentEtag; | ||
} | ||
if (currentLastModified) { | ||
headers['If-Modified-Since'] = currentLastModified; | ||
} | ||
return await (0, server_request_1.getWithTimeout)(url, { | ||
headers: headers, | ||
retries: 1, | ||
retries, | ||
retryOn, | ||
}, requestTimeout); | ||
@@ -17,0 +52,0 @@ } |
@@ -1,5 +0,6 @@ | ||
import { DVCLogger } from '@devcycle/types'; | ||
import { ConfigBody, DVCLogger } from '@devcycle/types'; | ||
import { ResponseError } from "../../../../server-request/src"; | ||
type ConfigPollingOptions = { | ||
configPollingIntervalMS?: number; | ||
sseConfigPollingIntervalMS?: number; | ||
configPollingTimeoutMS?: number; | ||
@@ -9,2 +10,3 @@ configCDNURI?: string; | ||
clientMode?: boolean; | ||
enableBetaRealTimeUpdates?: boolean; | ||
}; | ||
@@ -14,3 +16,3 @@ type SetIntervalInterface = (handler: () => void, timeout?: number) => any; | ||
type SetConfigBufferInterface = (sdkKey: string, projectConfig: string) => void; | ||
type TrackSDKConfigEventInterface = (url: string, responseTimeMS: number, res?: Response, err?: ResponseError, reqEtag?: string, reqLastModified?: string) => void; | ||
type TrackSDKConfigEventInterface = (url: string, responseTimeMS: number, res?: Response, err?: ResponseError, reqEtag?: string, reqLastModified?: string, sseConnected?: boolean) => void; | ||
export declare class EnvironmentConfigManager { | ||
@@ -26,16 +28,27 @@ private readonly logger; | ||
configLastModified?: string; | ||
private readonly pollingIntervalMS; | ||
configSSE?: ConfigBody<string>['sse']; | ||
private currentPollingInterval; | ||
private readonly configPollingIntervalMS; | ||
private readonly sseConfigPollingIntervalMS; | ||
private readonly requestTimeoutMS; | ||
private readonly cdnURI; | ||
private readonly enableRealtimeUpdates; | ||
fetchConfigPromise: Promise<void>; | ||
private intervalTimeout?; | ||
private disablePolling; | ||
private clientMode; | ||
constructor(logger: DVCLogger, sdkKey: string, setConfigBuffer: SetConfigBufferInterface, setInterval: SetIntervalInterface, clearInterval: ClearIntervalInterface, trackSDKConfigEvent: TrackSDKConfigEventInterface, { configPollingIntervalMS, configPollingTimeoutMS, configCDNURI, cdnURI, clientMode, }: ConfigPollingOptions); | ||
private sseConnection?; | ||
constructor(logger: DVCLogger, sdkKey: string, setConfigBuffer: SetConfigBufferInterface, setInterval: SetIntervalInterface, clearInterval: ClearIntervalInterface, trackSDKConfigEvent: TrackSDKConfigEventInterface, { configPollingIntervalMS, sseConfigPollingIntervalMS, // 10 minutes | ||
configPollingTimeoutMS, configCDNURI, cdnURI, clientMode, enableBetaRealTimeUpdates, }: ConfigPollingOptions); | ||
private startSSE; | ||
private onSSEMessage; | ||
private stopSSE; | ||
private startPolling; | ||
get hasConfig(): boolean; | ||
stopPolling(): void; | ||
private stopPolling; | ||
cleanup(): void; | ||
getConfigURL(): string; | ||
_fetchConfig(): Promise<void>; | ||
_fetchConfig(sseLastModified?: string): Promise<void>; | ||
private isLastModifiedHeaderOld; | ||
private handleSSEConfig; | ||
} | ||
export {}; |
@@ -6,4 +6,6 @@ "use strict"; | ||
const server_request_1 = require("../../../../server-request/src"); | ||
const sse_connection_1 = require("../../../../sse-connection/src"); | ||
class EnvironmentConfigManager { | ||
constructor(logger, sdkKey, setConfigBuffer, setInterval, clearInterval, trackSDKConfigEvent, { configPollingIntervalMS = 10000, configPollingTimeoutMS = 5000, configCDNURI, cdnURI = 'https://config-cdn.devcycle.com', clientMode = false, }) { | ||
constructor(logger, sdkKey, setConfigBuffer, setInterval, clearInterval, trackSDKConfigEvent, { configPollingIntervalMS = 10000, sseConfigPollingIntervalMS = 10 * 60 * 1000, // 10 minutes | ||
configPollingTimeoutMS = 5000, configCDNURI, cdnURI = 'https://config-cdn.devcycle.com', clientMode = false, enableBetaRealTimeUpdates = false, }) { | ||
this.logger = logger; | ||
@@ -16,9 +18,13 @@ this.sdkKey = sdkKey; | ||
this._hasConfig = false; | ||
this.disablePolling = false; | ||
this.clientMode = clientMode; | ||
this.pollingIntervalMS = | ||
this.enableRealtimeUpdates = enableBetaRealTimeUpdates; | ||
this.configPollingIntervalMS = | ||
configPollingIntervalMS >= 1000 ? configPollingIntervalMS : 1000; | ||
this.sseConfigPollingIntervalMS = | ||
sseConfigPollingIntervalMS <= 60 * 1000 | ||
? 10 * 60 * 1000 | ||
: sseConfigPollingIntervalMS; | ||
this.requestTimeoutMS = | ||
configPollingTimeoutMS >= this.pollingIntervalMS | ||
? this.pollingIntervalMS | ||
configPollingTimeoutMS >= this.configPollingIntervalMS | ||
? this.configPollingIntervalMS | ||
: configPollingTimeoutMS; | ||
@@ -31,15 +37,87 @@ this.cdnURI = configCDNURI || cdnURI; | ||
.finally(() => { | ||
if (this.disablePolling) { | ||
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; | ||
} | ||
this.intervalTimeout = this.setInterval(async () => { | ||
try { | ||
await this._fetchConfig(); | ||
} | ||
catch (ex) { | ||
this.logger.error(ex.message); | ||
} | ||
}, this.pollingIntervalMS); | ||
}); | ||
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() { | ||
@@ -49,7 +127,8 @@ return this._hasConfig; | ||
stopPolling() { | ||
this.disablePolling = true; | ||
this.clearInterval(this.intervalTimeout); | ||
this.intervalTimeout = null; | ||
} | ||
cleanup() { | ||
this.stopPolling(); | ||
this.stopSSE(); | ||
} | ||
@@ -62,3 +141,3 @@ getConfigURL() { | ||
} | ||
async _fetchConfig() { | ||
async _fetchConfig(sseLastModified) { | ||
const url = this.getConfigURL(); | ||
@@ -70,4 +149,4 @@ let res; | ||
let responseTimeMS = 0; | ||
const reqEtag = this.configEtag; | ||
const reqLastModified = this.configLastModified; | ||
const currentEtag = this.configEtag; | ||
const currentLastModified = this.configLastModified; | ||
const logError = (error) => { | ||
@@ -84,4 +163,5 @@ const errMsg = `Request to get config failed for url: ${url}, ` + | ||
const trackEvent = (err) => { | ||
var _a, _b; | ||
if ((res && (res === null || res === void 0 ? void 0 : res.status) !== 304) || err) { | ||
this.trackSDKConfigEvent(url, responseTimeMS, res || undefined, err, reqEtag, reqLastModified); | ||
this.trackSDKConfigEvent(url, responseTimeMS, res || undefined, err, currentEtag, currentLastModified, (_b = (_a = this.sseConnection) === null || _a === void 0 ? void 0 : _a.isConnected()) !== null && _b !== void 0 ? _b : false); | ||
} | ||
@@ -92,3 +172,10 @@ }; | ||
`, last-modified: ${this.configLastModified}`); | ||
res = await (0, request_1.getEnvironmentConfig)(url, this.requestTimeoutMS, reqEtag, reqLastModified); | ||
res = await (0, request_1.getEnvironmentConfig)({ | ||
logger: this.logger, | ||
url, | ||
requestTimeout: this.requestTimeoutMS, | ||
currentEtag, | ||
currentLastModified, | ||
sseLastModified, | ||
}); | ||
responseTimeMS = Date.now() - startTime; | ||
@@ -112,8 +199,13 @@ projectConfig = await res.text(); | ||
else if ((res === null || res === void 0 ? void 0 : res.status) === 200 && projectConfig) { | ||
const lastModifiedHeader = res === null || res === void 0 ? void 0 : res.headers.get('last-modified'); | ||
if (this.isLastModifiedHeaderOld(lastModifiedHeader)) { | ||
this.logger.debug('Skipping saving config, existing last modified date is newer.'); | ||
return; | ||
} | ||
try { | ||
this.handleSSEConfig(projectConfig); | ||
this.setConfigBuffer(`${this.sdkKey}${this.clientMode ? '_client' : ''}`, projectConfig); | ||
this._hasConfig = true; | ||
this.configEtag = (res === null || res === void 0 ? void 0 : res.headers.get('etag')) || ''; | ||
this.configLastModified = | ||
(res === null || res === void 0 ? void 0 : res.headers.get('last-modified')) || ''; | ||
this.configLastModified = lastModifiedHeader || ''; | ||
return; | ||
@@ -133,3 +225,3 @@ } | ||
else if ((responseError === null || responseError === void 0 ? void 0 : responseError.status) === 403) { | ||
this.stopPolling(); | ||
this.cleanup(); | ||
throw new server_request_1.UserError(`Invalid SDK key provided: ${this.sdkKey}`); | ||
@@ -141,4 +233,36 @@ } | ||
} | ||
isLastModifiedHeaderOld(lastModifiedHeader) { | ||
const lastModifiedHeaderDate = lastModifiedHeader | ||
? new Date(lastModifiedHeader) | ||
: null; | ||
const configLastModifiedDate = this.configLastModified | ||
? new Date(this.configLastModified) | ||
: null; | ||
return ((0, request_1.isValidDate)(configLastModifiedDate) && | ||
(0, request_1.isValidDate)(lastModifiedHeaderDate) && | ||
lastModifiedHeaderDate <= configLastModifiedDate); | ||
} | ||
handleSSEConfig(projectConfig) { | ||
var _a, _b; | ||
if (this.enableRealtimeUpdates) { | ||
const configBody = JSON.parse(projectConfig); | ||
const originalConfigSSE = this.configSSE; | ||
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 |
@@ -1,1 +0,10 @@ | ||
export declare function getEnvironmentConfig(url: string, requestTimeout: number, etag?: string, lastModified?: string): Promise<Response>; | ||
import { DVCLogger } from '@devcycle/types'; | ||
export declare const isValidDate: (date: Date | null) => date is Date; | ||
export declare function getEnvironmentConfig({ logger, url, requestTimeout, currentEtag, currentLastModified, sseLastModified, }: { | ||
logger: DVCLogger; | ||
url: string; | ||
requestTimeout: number; | ||
currentEtag?: string; | ||
currentLastModified?: string; | ||
sseLastModified?: string; | ||
}): Promise<Response>; |
"use strict"; | ||
Object.defineProperty(exports, "__esModule", { value: true }); | ||
exports.getEnvironmentConfig = void 0; | ||
exports.getEnvironmentConfig = exports.isValidDate = void 0; | ||
const server_request_1 = require("../../../../server-request/src"); | ||
async function getEnvironmentConfig(url, requestTimeout, etag, lastModified) { | ||
const isValidDate = (date) => date instanceof Date && !isNaN(date.getTime()); | ||
exports.isValidDate = isValidDate; | ||
async function getEnvironmentConfig({ logger, url, requestTimeout, currentEtag, currentLastModified, sseLastModified, }) { | ||
const headers = {}; | ||
if (etag) { | ||
headers['If-None-Match'] = etag; | ||
let retries = 1; | ||
let retryOn; | ||
const sseLastModifiedDate = sseLastModified | ||
? new Date(sseLastModified) | ||
: null; | ||
// Retry fetching config if the Last-Modified header is older than | ||
// the requiredLastModified from the SSE message | ||
if (sseLastModified && (0, exports.isValidDate)(sseLastModifiedDate)) { | ||
retries = 3; | ||
retryOn = (attempt, error, response) => { | ||
if (attempt >= retries) { | ||
return false; | ||
} | ||
else if (response && (response === null || response === void 0 ? void 0 : response.status) === 200) { | ||
const lastModifiedHeader = response.headers.get('Last-Modified'); | ||
const lastModifiedHeaderDate = lastModifiedHeader | ||
? new Date(lastModifiedHeader) | ||
: null; | ||
if ((0, exports.isValidDate)(lastModifiedHeaderDate) && | ||
lastModifiedHeaderDate < sseLastModifiedDate) { | ||
logger.debug(`Retry fetching config, last modified is old: ${lastModifiedHeader}` + | ||
`, sse last modified: ${sseLastModified}`); | ||
return true; | ||
} | ||
return false; | ||
} | ||
else if (response && (response === null || response === void 0 ? void 0 : response.status) < 500) { | ||
return false; | ||
} | ||
return true; | ||
}; | ||
} | ||
if (lastModified) { | ||
headers['If-Modified-Since'] = lastModified; | ||
if (currentEtag) { | ||
headers['If-None-Match'] = currentEtag; | ||
} | ||
if (currentLastModified) { | ||
headers['If-Modified-Since'] = currentLastModified; | ||
} | ||
return await (0, server_request_1.getWithTimeout)(url, { | ||
headers: headers, | ||
retries: 1, | ||
retries, | ||
retryOn, | ||
}, requestTimeout); | ||
@@ -17,0 +52,0 @@ } |
@@ -7,2 +7,3 @@ import { RequestInitWithRetry } from 'fetch-retry'; | ||
export declare const exponentialBackoff: RequestInitWithRetry['retryDelay']; | ||
export type RequestInitConfig = RequestInit | RequestInitWithRetry; | ||
export declare function handleResponse(res: Response): Promise<Response>; | ||
@@ -9,0 +10,0 @@ export declare function getWithTimeout(url: string, requestConfig: RequestInit | RequestInitWithRetry, timeout: number): Promise<Response>; |
@@ -119,3 +119,4 @@ "use strict"; | ||
const newConfig = { ...requestConfig }; | ||
newConfig.retryOn = retryOnRequestError(requestConfig.retries); | ||
newConfig.retryOn = | ||
newConfig.retryOn || retryOnRequestError(requestConfig.retries); | ||
newConfig.retryDelay = exports.exponentialBackoff; | ||
@@ -122,0 +123,0 @@ return [await getFetchWithRetry(), newConfig]; |
{ | ||
"name": "@devcycle/nodejs-server-sdk", | ||
"version": "1.30.2", | ||
"version": "1.31.0", | ||
"description": "The DevCycle NodeJS Server SDK used for feature management.", | ||
@@ -23,6 +23,7 @@ "license": "MIT", | ||
"dependencies": { | ||
"@devcycle/bucketing-assembly-script": "^1.22.1", | ||
"@devcycle/js-cloud-server-sdk": "^1.12.1", | ||
"@devcycle/types": "^1.13.1", | ||
"@devcycle/bucketing-assembly-script": "^1.23.0", | ||
"@devcycle/js-cloud-server-sdk": "^1.13.0", | ||
"@devcycle/types": "^1.14.0", | ||
"cross-fetch": "^4.0.0", | ||
"eventsource": "^2.0.2", | ||
"fetch-retry": "^5.0.6" | ||
@@ -29,0 +30,0 @@ }, |
@@ -7,2 +7,3 @@ import { RequestInitWithRetry } from 'fetch-retry'; | ||
export declare const exponentialBackoff: RequestInitWithRetry['retryDelay']; | ||
export type RequestInitConfig = RequestInit | RequestInitWithRetry; | ||
export declare function handleResponse(res: Response): Promise<Response>; | ||
@@ -9,0 +10,0 @@ export declare function getWithTimeout(url: string, requestConfig: RequestInit | RequestInitWithRetry, timeout: number): Promise<Response>; |
@@ -119,3 +119,4 @@ "use strict"; | ||
const newConfig = { ...requestConfig }; | ||
newConfig.retryOn = retryOnRequestError(requestConfig.retries); | ||
newConfig.retryOn = | ||
newConfig.retryOn || retryOnRequestError(requestConfig.retries); | ||
newConfig.retryDelay = exports.exponentialBackoff; | ||
@@ -122,0 +123,0 @@ return [await getFetchWithRetry(), newConfig]; |
@@ -209,3 +209,3 @@ "use strict"; | ||
} | ||
trackSDKConfigEvent(url, responseTimeMS, res, err, reqEtag, reqLastModified) { | ||
trackSDKConfigEvent(url, responseTimeMS, res, err, reqEtag, reqLastModified, sseConnected) { | ||
var _a, _b, _c, _d, _e; | ||
@@ -228,2 +228,3 @@ const populatedUser = (0, populatedUserHelpers_1.DVCPopulatedUserFromDevCycleUser)({ | ||
errMsg: (_e = err === null || err === void 0 ? void 0 : err.message) !== null && _e !== void 0 ? _e : undefined, | ||
sseConnected: sseConnected !== null && sseConnected !== void 0 ? sseConnected : undefined, | ||
}, | ||
@@ -230,0 +231,0 @@ }); |
Sorry, the diff of this file is not supported yet
Sorry, the diff of this file is not supported yet
Sorry, the diff of this file is not supported yet
Sorry, the diff of this file is not supported yet
Sorry, the diff of this file is not supported yet
Sorry, the diff of this file is not supported yet
Sorry, the diff of this file is not supported yet
Sorry, the diff of this file is not supported yet
Sorry, the diff of this file is not supported yet
License Policy Violation
LicenseThis package is not allowed per your license policy. Review the package's license to ensure compliance.
Found 1 instance in 1 package
License Policy Violation
LicenseThis package is not allowed per your license policy. Review the package's license to ensure compliance.
Found 1 instance in 1 package
320000
120
3102
7
+ Addedeventsource@^2.0.2
+ Addedeventsource@2.0.2(transitive)
Updated@devcycle/types@^1.14.0