UNPKG

vwo-fme-node-sdk

Version:

VWO Node/JavaScript SDK for Feature Management and Experimentation

295 lines (267 loc) 10.3 kB
/** * 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 * as http from 'http'; import * as https from 'https'; import { dynamic } from '../../../types/Common'; import { Deferred } from '../../../utils/PromiseUtil'; import { HTTPS } from '../../../constants/Url'; import { RequestModel } from '../models/RequestModel'; import { ResponseModel } from '../models/ResponseModel'; import { NetworkClientInterface } from './NetworkClientInterface'; import { LogManager } from '../../../packages/logger'; import { buildMessage } from '../../../utils/LogMessageUtil'; import { ErrorLogMessagesEnum } from '../../../enums/log-messages'; import { EventEnum } from '../../../enums/EventEnum'; export interface IRetryConfig { shouldRetry?: boolean; initialDelay?: number; maxRetries?: number; backoffMultiplier?: number; } /** * Implements the NetworkClientInterface to handle network requests. */ export class NetworkClient implements NetworkClientInterface { /** * Performs a GET request using the provided RequestModel. * @param {RequestModel} requestModel - The model containing request options. * @returns {Promise<ResponseModel>} A promise that resolves to a ResponseModel. */ GET(requestModel: RequestModel): Promise<ResponseModel> { const attemptRequest = (attempt: number): Promise<ResponseModel> => { const deferred = new Deferred(); // Extract network options from the request model. const networkOptions: Record<string, dynamic> = requestModel.getOptions(); const responseModel = new ResponseModel(); try { // Choose HTTP or HTTPS client based on the scheme. const httpClient = networkOptions.scheme === HTTPS ? https : http; // Perform the HTTP GET request. const req = httpClient.get(networkOptions, (res) => { responseModel.setStatusCode(res.statusCode); const contentType = res.headers['content-type']; let error; let rawData = ''; // Check for expected content-type. if (!/^application\/json/.test(contentType)) { error = `Invalid content-type.\nExpected application/json but received ${contentType}. Status Code: ${res?.statusCode}`; } if (error) { // Log error and consume response data to free up memory. res.resume(); return this.retryOrReject( error, attempt, deferred, networkOptions, attemptRequest, requestModel.getRetryConfig(), ); } res.setEncoding('utf8'); // Collect data chunks. res.on('data', (chunk) => { rawData += chunk; }); // Handle the end of the response. res.on('end', () => { try { const parsedData = JSON.parse(rawData); // Check for successful response status. if (responseModel.getStatusCode() < 200 || responseModel.getStatusCode() >= 300) { const error = `${rawData}, Status Code: ${responseModel.getStatusCode()}`; // if status code is 400, reject the promise as it is a bad request if (responseModel.getStatusCode() === 400) { responseModel.setError(error); deferred.reject(responseModel); return; } return this.retryOrReject( error, attempt, deferred, networkOptions, attemptRequest, requestModel.getRetryConfig(), ); } responseModel.setData(parsedData); deferred.resolve(responseModel); } catch (err) { return this.retryOrReject( err, attempt, deferred, networkOptions, attemptRequest, requestModel.getRetryConfig(), ); } }); }); // Handle request timeout. req.on('timeout', () => { return this.retryOrReject( new Error('timeout'), attempt, deferred, networkOptions, attemptRequest, requestModel.getRetryConfig(), ); }); req.on('error', (err) => { return this.retryOrReject( err, attempt, deferred, networkOptions, attemptRequest, requestModel.getRetryConfig(), ); }); } catch (err) { this.retryOrReject(err, attempt, deferred, networkOptions, attemptRequest, requestModel.getRetryConfig()); } return deferred.promise; }; return attemptRequest(0); } /** * Performs a POST request using the provided RequestModel. * @param {RequestModel} request - The model containing request options. * @returns {Promise<ResponseModel>} A promise that resolves or rejects with a ResponseModel. */ POST(request: RequestModel): Promise<ResponseModel> { const attemptRequest = (attempt: number): Promise<ResponseModel> => { const deferred = new Deferred(); const networkOptions: Record<string, dynamic> = request.getOptions(); const responseModel: ResponseModel = new ResponseModel(); try { // Choose HTTP or HTTPS client based on the scheme. const httpClient = networkOptions.scheme === HTTPS ? https : http; // Perform the HTTP POST request. const req = httpClient.request(networkOptions, (res) => { let rawData = ''; res.setEncoding('utf8'); // Collect data chunks. res.on('data', function (chunk) { rawData += chunk; }); // Handle the end of the response. res.on('end', () => { try { if (res.statusCode === 200) { responseModel.setStatusCode(res.statusCode); responseModel.setData(request.getBody()); deferred.resolve(responseModel); } else { const error = `Raw Data: ${rawData}, Status Code: ${res.statusCode}`; responseModel.setStatusCode(res.statusCode); // if status code is 400, reject the promise as it is a bad request if (res.statusCode === 400) { responseModel.setError(error); deferred.reject(responseModel); return; } return this.retryOrReject( error, attempt, deferred, networkOptions, attemptRequest, request.getRetryConfig(), ); } } catch (err) { return this.retryOrReject( err, attempt, deferred, networkOptions, attemptRequest, request.getRetryConfig(), ); } }); }); // Handle request timeout. req.on('timeout', () => { const error = `Timeout: ${networkOptions.timeout}`; return this.retryOrReject(error, attempt, deferred, networkOptions, attemptRequest, request.getRetryConfig()); }); req.on('error', (err) => { return this.retryOrReject(err, attempt, deferred, networkOptions, attemptRequest, request.getRetryConfig()); }); // Write data to the request body and end the request. req.write(JSON.stringify(networkOptions.body)); req.end(); } catch (err) { this.retryOrReject(err, attempt, deferred, networkOptions, attemptRequest, request.getRetryConfig()); } return deferred.promise; }; return attemptRequest(0); } /** * Helper function to retry or reject * @param {any} error - The error to retry or reject * @param {number} attempt - The attempt number * @param {any} deferred - The deferred object * @param {string} operation - The operation to retry or reject * @param {Function} attemptRequest - The function to attempt the request */ private retryOrReject( error: any, attempt: number, deferred: any, networkOptions: Record<string, dynamic>, attemptRequest: (attempt: number) => Promise<ResponseModel>, retryConfig: IRetryConfig, ) { const endpoint = String(networkOptions.path).split('?')[0]; const delay = retryConfig.initialDelay * Math.pow(retryConfig.backoffMultiplier, attempt) * 1000; if (retryConfig.shouldRetry && attempt < retryConfig.maxRetries) { LogManager.Instance.error( buildMessage(ErrorLogMessagesEnum.NETWORK_CALL_RETRY_ATTEMPT, { endPoint: endpoint, err: error, delay: delay, attempt: attempt + 1, maxRetries: retryConfig.maxRetries, }), ); setTimeout(() => { attemptRequest(attempt + 1) .then(deferred.resolve) .catch(deferred.reject); }, delay); } else { if (!String(networkOptions.path).includes(EventEnum.VWO_LOG_EVENT)) { // only log error if the endpoint is not vwo_log event LogManager.Instance.error( buildMessage(ErrorLogMessagesEnum.NETWORK_CALL_RETRY_FAILED, { endPoint: endpoint, err: error, }), ); } const responseModel = new ResponseModel(); responseModel.setError(error); deferred.reject(responseModel); } } }