Huge News!Announcing our $40M Series B led by Abstract Ventures.Learn More
Socket
Sign inDemoInstall
Socket

@devcycle/nodejs-server-sdk

Package Overview
Dependencies
Maintainers
6
Versions
191
Alerts
File Explorer

Advanced tools

Socket logo

Install Socket

Detect and block malicious and high-risk dependencies

Install

@devcycle/nodejs-server-sdk - npm Package Compare versions

Comparing version 1.30.2 to 1.31.0

config-manager/lib/shared/sse-connection/src/index.d.ts

1

config-manager/lib/shared/server-request/src/request.d.ts

@@ -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>;

3

config-manager/lib/shared/server-request/src/request.js

@@ -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

SocketSocket SOC 2 Logo

Product

  • Package Alerts
  • Integrations
  • Docs
  • Pricing
  • FAQ
  • Roadmap
  • Changelog

Packages

npm

Stay in touch

Get open source security insights delivered straight into your inbox.


  • Terms
  • Privacy
  • Security

Made with ⚡️ by Socket Inc