vwo-fme-node-sdk
Version:
VWO Node/JavaScript SDK for Feature Management and Experimentation
303 lines (266 loc) • 10.7 kB
text/typescript
/**
* Copyright 2024-2025 Wingify Software Pvt. Ltd.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
import { dynamic } from '../types/Common';
import { Storage } from '../packages/storage';
import { LogManager } from '../packages/logger';
import { NetworkManager, RequestModel, ResponseModel } from '../packages/network-layer';
import { Deferred } from '../utils/PromiseUtil';
import { Constants } from '../constants';
import { HTTPS_PROTOCOL, HTTP_PROTOCOL } from '../constants/Url';
import { HttpMethodEnum } from '../enums/HttpMethodEnum';
import { DebugLogMessagesEnum, ErrorLogMessagesEnum, InfoLogMessagesEnum } from '../enums/log-messages';
import { SettingsSchema } from '../models/schemas/SettingsSchemaValidation';
import { buildMessage } from '../utils/LogMessageUtil';
import { getSettingsPath } from '../utils/NetworkUtil';
interface ISettingsService {
sdkKey: string;
getSettings(forceFetch: boolean): Promise<Record<any, any>>;
fetchSettings(): Promise<Record<any, any>>;
}
export class SettingsService implements ISettingsService {
sdkKey: string;
accountId: number;
expiry: number;
networkTimeout: number;
hostname: string;
port: number;
protocol: string;
isGatewayServiceProvided: boolean = false;
private static instance: SettingsService;
constructor(options: Record<string, any>) {
this.sdkKey = options.sdkKey;
this.accountId = options.accountId;
this.expiry = options?.settings?.expiry || Constants.SETTINGS_EXPIRY;
this.networkTimeout = options?.settings?.timeout || Constants.SETTINGS_TIMEOUT;
// if sdk is running in browser environment then set isGatewayServiceProvided to true
// when gatewayService is not provided then we dont update the url and let it point to dacdn by default
// Check if sdk running in browser and not in edge/serverless environment
if (typeof process.env === 'undefined' && typeof XMLHttpRequest !== 'undefined') {
this.isGatewayServiceProvided = true;
}
if (options?.gatewayService?.url) {
let parsedUrl;
this.isGatewayServiceProvided = true;
if (
options.gatewayService.url.startsWith(HTTP_PROTOCOL) ||
options.gatewayService.url.startsWith(HTTPS_PROTOCOL)
) {
parsedUrl = new URL(`${options.gatewayService.url}`);
} else if (options.gatewayService?.protocol) {
parsedUrl = new URL(`${options.gatewayService.protocol}://${options.gatewayService.url}`);
} else {
parsedUrl = new URL(`${HTTPS_PROTOCOL}${options.gatewayService.url}`);
}
this.hostname = parsedUrl.hostname;
this.protocol = parsedUrl.protocol.replace(':', '');
if (parsedUrl.port) {
this.port = parseInt(parsedUrl.port);
} else if (options.gatewayService?.port) {
this.port = options.gatewayService.port;
}
} else {
this.hostname = Constants.HOST_NAME;
}
// if (this.expiry > 0) {
// this.setSettingsExpiry();
// }
LogManager.Instance.debug(
buildMessage(DebugLogMessagesEnum.SERVICE_INITIALIZED, {
service: 'Settings Manager',
}),
);
SettingsService.instance = this;
}
static get Instance(): SettingsService {
return SettingsService.instance;
}
private setSettingsExpiry() {
const settingsTimeout = setTimeout(() => {
this.fetchSettingsAndCacheInStorage().then(() => {
clearTimeout(settingsTimeout);
// again set the timer
// NOTE: setInterval could be used but it will not consider the time required to fetch settings
// This breaks the timer rythm and also sends more call than required
this.setSettingsExpiry();
});
}, this.expiry);
}
private async normalizeSettings(settings: Record<any, any>): Promise<Record<any, any>> {
const normalizedSettings = { ...settings };
if (!normalizedSettings.features || Object.keys(normalizedSettings.features).length === 0) {
normalizedSettings.features = [];
}
if (!normalizedSettings.campaigns || Object.keys(normalizedSettings.campaigns).length === 0) {
normalizedSettings.campaigns = [];
}
return normalizedSettings;
}
private async handleBrowserEnvironment(
storageConnector: any,
deferredObject: { resolve: (value: any) => void; reject: (reason?: any) => void },
): Promise<void> {
try {
const cachedSettings = await storageConnector.getSettingsFromStorage(this.sdkKey, this.accountId);
if (cachedSettings) {
LogManager.Instance.info(buildMessage(InfoLogMessagesEnum.SETTINGS_FETCH_FROM_CACHE));
deferredObject.resolve(cachedSettings);
} else {
LogManager.Instance.info(buildMessage(InfoLogMessagesEnum.SETTINGS_CACHE_MISS));
}
const freshSettings = await this.fetchSettings();
const normalizedSettings = await this.normalizeSettings(freshSettings);
// set the settings in storage only if settings are valid
const isSettingsValid = new SettingsSchema().isSettingsValid(normalizedSettings);
if (isSettingsValid) {
await storageConnector.setSettingsInStorage(normalizedSettings);
}
if (cachedSettings) {
LogManager.Instance.info(buildMessage(InfoLogMessagesEnum.SETTINGS_BACKGROUND_UPDATE));
} else {
LogManager.Instance.info(buildMessage(InfoLogMessagesEnum.SETTINGS_FETCH_SUCCESS));
deferredObject.resolve(normalizedSettings);
}
} catch (error) {
LogManager.Instance.error(
buildMessage(ErrorLogMessagesEnum.SETTINGS_FETCH_ERROR, {
err: JSON.stringify(error),
}),
);
deferredObject.resolve(null);
}
}
private async handleServerEnvironment(deferredObject: {
resolve: (value: any) => void;
reject: (reason?: any) => void;
}): Promise<void> {
try {
const settings = await this.fetchSettings();
const normalizedSettings = await this.normalizeSettings(settings);
deferredObject.resolve(normalizedSettings);
} catch (error) {
LogManager.Instance.error(
buildMessage(ErrorLogMessagesEnum.SETTINGS_FETCH_ERROR, {
err: JSON.stringify(error),
}),
);
deferredObject.resolve(null);
}
}
private fetchSettingsAndCacheInStorage(): Promise<Record<any, any>> {
const deferredObject = new Deferred();
const storageConnector = Storage.Instance.getConnector();
if (typeof process.env === 'undefined' && typeof XMLHttpRequest !== 'undefined') {
this.handleBrowserEnvironment(storageConnector, deferredObject);
} else {
this.handleServerEnvironment(deferredObject);
}
return deferredObject.promise;
}
fetchSettings(isViaWebhook = false): Promise<Record<any, any>> {
const deferredObject = new Deferred();
if (!this.sdkKey || !this.accountId) {
deferredObject.reject(new Error('sdkKey is required for fetching account settings. Aborting!'));
}
const networkInstance = NetworkManager.Instance;
const options: Record<string, dynamic> = getSettingsPath(this.sdkKey, this.accountId);
const retryConfig = networkInstance.getRetryConfig();
options.platform = Constants.PLATFORM;
options['api-version'] = Constants.API_VERSION;
if (!networkInstance.getConfig().getDevelopmentMode()) {
options.s = 'prod';
}
let path = Constants.SETTINTS_ENDPOINT;
if (isViaWebhook) {
path = Constants.WEBHOOK_SETTINTS_ENDPOINT;
}
try {
const request: RequestModel = new RequestModel(
this.hostname,
HttpMethodEnum.GET,
path,
options,
null,
null,
this.protocol,
this.port,
retryConfig,
);
request.setTimeout(this.networkTimeout);
networkInstance
.get(request)
.then((response: ResponseModel) => {
deferredObject.resolve(response.getData());
})
.catch((err: ResponseModel) => {
deferredObject.reject(err);
});
return deferredObject.promise;
} catch (err) {
LogManager.Instance.error(
buildMessage(ErrorLogMessagesEnum.SETTINGS_FETCH_ERROR, {
err: JSON.stringify(err),
}),
);
deferredObject.reject(err);
return deferredObject.promise;
}
}
getSettings(forceFetch = false): Promise<Record<any, any>> {
const deferredObject = new Deferred();
if (forceFetch) {
this.fetchSettingsAndCacheInStorage().then((settings: Record<any, any>) => {
deferredObject.resolve(settings);
});
} else {
// const storageConnector = Storage.Instance.getConnector();
// if (storageConnector) {
// storageConnector
// .get(Constants.SETTINGS)
// .then((storedSettings: dynamic) => {
// if (!isObject(storedSettings)) {
// this.fetchSettingsAndCacheInStorage().then((fetchedSettings) => {
// const isSettingsValid = new SettingsSchema().isSettingsValid(fetchedSettings);
// if (isSettingsValid) {
// deferredObject.resolve(fetchedSettings);
// } else {
// deferredObject.reject(new Error('Settings are not valid. Failed schema validation.'));
// }
// });
// } else {
// deferredObject.resolve(storedSettings);
// }
// })
// .catch(() => {
// this.fetchSettingsAndCacheInStorage().then((fetchedSettings) => {
// deferredObject.resolve(fetchedSettings);
// });
// });
// } else {
this.fetchSettingsAndCacheInStorage().then((fetchedSettings: Record<any, any>) => {
const isSettingsValid = new SettingsSchema().isSettingsValid(fetchedSettings);
if (isSettingsValid) {
LogManager.Instance.info(InfoLogMessagesEnum.SETTINGS_FETCH_SUCCESS);
deferredObject.resolve(fetchedSettings);
} else {
LogManager.Instance.error(ErrorLogMessagesEnum.SETTINGS_SCHEMA_INVALID);
deferredObject.resolve({});
}
});
// }
}
return deferredObject.promise;
}
}