UNPKG

swell-node

Version:
358 lines 13.2 kB
"use strict"; var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) { if (k2 === undefined) k2 = k; var desc = Object.getOwnPropertyDescriptor(m, k); if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) { desc = { enumerable: true, get: function() { return m[k]; } }; } Object.defineProperty(o, k2, desc); }) : (function(o, m, k, k2) { if (k2 === undefined) k2 = k; o[k2] = m[k]; })); var __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? (function(o, v) { Object.defineProperty(o, "default", { enumerable: true, value: v }); }) : function(o, v) { o["default"] = v; }); var __importStar = (this && this.__importStar) || function (mod) { if (mod && mod.__esModule) return mod; var result = {}; if (mod != null) for (var k in mod) if (k !== "default" && Object.prototype.hasOwnProperty.call(mod, k)) __createBinding(result, mod, k); __setModuleDefault(result, mod); return result; }; Object.defineProperty(exports, "__esModule", { value: true }); exports.Client = void 0; const axios = __importStar(require("axios")); const retry = __importStar(require("retry")); const tough_cookie_1 = require("tough-cookie"); const http_1 = require("http-cookie-agent/http"); const MODULE_VERSION = (({ name, version }) => { return `${name}@${version}`; })(require('../package.json')); // eslint-disable-line @typescript-eslint/no-var-requires const USER_APP_VERSION = process.env.npm_package_name && process.env.npm_package_version ? `${process.env.npm_package_name}@${process.env.npm_package_version}` : undefined; const DEFAULT_OPTIONS = Object.freeze({ url: 'https://api.swell.store', verifyCert: true, version: 1, headers: {}, retries: 0, // 0 => no retries maxSockets: 100, keepAliveMs: 1000, recycleAfterRequests: 1000, recycleAfterMs: 15000, // 15 seconds }); class ApiError extends Error { constructor(message, code, status, headers = {}) { super(); this.message = message; this.code = code; this.status = status; this.headers = headers; } } // We should retry request only in case of timeout or disconnect const RETRY_CODES = new Set([ 'ECONNABORTED', 'ECONNREFUSED', 'ECONNRESET', 'EPIPE', 'NO_RESPONSE', ]); /** * Swell API Client. */ class Client { constructor(clientId, clientKey, options = {}) { this.clientId = typeof clientId === 'string' ? clientId : ''; this.clientKey = typeof clientKey === 'string' ? clientKey : ''; this.options = {}; this.httpClient = null; this._activeClient = null; this._oldClients = new Map(); this._clientCounter = 0; if (clientId) { this.init(clientId, clientKey, options); } } /** * Convenience method to create a new client instance from a singleton instance. */ createClient(clientId, clientKey, options = {}) { return new Client(clientId, clientKey, options); } init(clientId, clientKey, options) { if (!clientId) { throw new Error("Swell store 'id' is required to connect"); } if (!clientKey) { throw new Error("Swell store 'key' is required to connect"); } this.clientId = clientId; this.clientKey = clientKey; this.options = { ...DEFAULT_OPTIONS, ...options }; this._initHttpClient(); } _initHttpClient() { const { url, timeout, verifyCert, headers, maxSockets, keepAliveMs } = this.options; const authToken = Buffer.from(`${this.clientId}:${this.clientKey}`, 'utf8').toString('base64'); const jar = new tough_cookie_1.CookieJar(); const newClient = axios.create({ baseURL: url, headers: { common: { ...headers, 'Content-Type': 'application/json', 'User-Agent': MODULE_VERSION, 'X-User-Application': USER_APP_VERSION, Authorization: `Basic ${authToken}`, }, }, httpAgent: new http_1.HttpCookieAgent({ cookies: { jar }, keepAlive: true, maxSockets: maxSockets || 100, keepAliveMsecs: keepAliveMs || 1000, }), httpsAgent: new http_1.HttpsCookieAgent({ cookies: { jar }, rejectUnauthorized: Boolean(verifyCert), keepAlive: true, maxSockets: maxSockets || 100, keepAliveMsecs: keepAliveMs || 1000, }), ...(timeout ? { timeout } : undefined), }); this.httpClient = newClient; this._activeClient = { client: newClient, createdAt: Date.now(), activeRequests: 0, totalRequests: 0, }; } _shouldRecycleClient() { if (!this._activeClient) return false; const { recycleAfterRequests, recycleAfterMs } = this.options; const now = Date.now(); const ageMs = now - this._activeClient.createdAt; return (this._activeClient.totalRequests >= (recycleAfterRequests || 1000) && ageMs >= (recycleAfterMs || 300000)); } _recycleHttpClient() { if (!this._activeClient) return; const oldClientStats = { createdAt: this._activeClient.createdAt, activeRequests: this._activeClient.activeRequests, totalRequests: this._activeClient.totalRequests, ageMs: Date.now() - this._activeClient.createdAt, }; // Move current client to old clients map const clientId = `client_${++this._clientCounter}`; this._oldClients.set(clientId, this._activeClient); // Create new client this._initHttpClient(); // Call the callback if provided if (this.options.onClientRecycle) { try { this.options.onClientRecycle({ ...oldClientStats, newClientCreatedAt: this._activeClient.createdAt, }); } catch (error) { // Silently ignore callback errors to prevent disrupting the recycling process console.warn('Error in onClientRecycle callback:', error); } } // Schedule cleanup of old client when no active requests this._scheduleOldClientCleanup(clientId); } _scheduleOldClientCleanup(clientId) { const checkInterval = setInterval(() => { const oldClient = this._oldClients.get(clientId); if (!oldClient) { clearInterval(checkInterval); return; } if (oldClient.activeRequests === 0) { // Destroy the HTTP agents to free resources if (oldClient.client.defaults.httpAgent) { oldClient.client.defaults.httpAgent.destroy?.(); } if (oldClient.client.defaults.httpsAgent) { oldClient.client.defaults.httpsAgent.destroy?.(); } this._oldClients.delete(clientId); clearInterval(checkInterval); } }, 1000); // Check every second } _getClientForRequest() { // Check if we need to recycle the current client if (this._shouldRecycleClient()) { this._recycleHttpClient(); } return this._activeClient; } get(url, data, headers) { return this.request("get" /* HttpMethod.get */, url, data, headers); } post(url, data, headers) { return this.request("post" /* HttpMethod.post */, url, data, headers); } put(url, data, headers) { return this.request("put" /* HttpMethod.put */, url, data, headers); } delete(url, data, headers) { return this.request("delete" /* HttpMethod.delete */, url, data, headers); } async request(method, url, data, headers) { // Prepare url and data for request const requestParams = transformRequest(method, url, data, headers); return new Promise((resolve, reject) => { const { retries } = this.options; const operation = retry.operation({ retries, minTimeout: 20, maxTimeout: 100, factor: 1, randomize: false, }); operation.attempt(async () => { if (this.httpClient === null) { return reject(new Error('Swell API client not initialized')); } const clientWrapper = this._getClientForRequest(); // Increment counters clientWrapper.activeRequests++; clientWrapper.totalRequests++; try { const response = await clientWrapper.client.request(requestParams); resolve(transformResponse(response).data); } catch (error) { // Attempt retry if we encounter a timeout or connection error const code = axios.isAxiosError(error) ? error?.code : null; if (code && RETRY_CODES.has(code) && operation.retry(error)) { return; } reject(transformError(error)); } finally { // Decrement active request counter clientWrapper.activeRequests--; } }); }); } /** * Get statistics about HTTP client usage */ getClientStats() { return { activeClient: this._activeClient ? { createdAt: this._activeClient.createdAt, activeRequests: this._activeClient.activeRequests, totalRequests: this._activeClient.totalRequests, ageMs: Date.now() - this._activeClient.createdAt, } : null, oldClientsCount: this._oldClients.size, oldClients: Array.from(this._oldClients.entries()).map(([id, client]) => ({ id, createdAt: client.createdAt, activeRequests: client.activeRequests, totalRequests: client.totalRequests, ageMs: Date.now() - client.createdAt, })), }; } } exports.Client = Client; /** * Transforms the request. * * @param method The HTTP method * @param url The request URL * @param data The request data * @return a normalized request object */ function transformRequest(method, url, data, headers) { return { method, url: typeof url?.toString === 'function' ? url.toString() : '', data: data !== undefined ? data : null, headers, }; } /** * Transforms the response. * * @param response The response object * @return a normalized response object */ function transformResponse(response) { const { data, headers, status } = response; return { data, headers: normalizeHeaders(headers), status, }; } function isError(error) { return error instanceof Error; } /** * Transforms the error response. * * @param error The Error object * @return {ApiError} */ function transformError(error) { let code, message = '', status, headers; if (axios.isAxiosError(error)) { if (error.response) { // The request was made and the server responded with a status code // that falls out of the range of 2xx const { data, statusText } = error.response; code = statusText; message = formatMessage(data); status = error.response.status; headers = normalizeHeaders(error.response.headers); } else if (error.request) { // The request was made but no response was received code = 'NO_RESPONSE'; message = `No response from server${error.message ? ` (${error.message})` : ''}`; } else { // Something happened in setting up the request that triggered an Error // The request was made but no response was received code = error.code; message = error.message; } } else if (isError(error)) { code = error.code; message = error.message; } return new ApiError(message, typeof code === 'string' ? code.toUpperCase().replace(/ /g, '_') : 'ERROR', status, headers); } function normalizeHeaders(headers) { // so that headers are not returned as AxiosHeaders return Object.fromEntries(Object.entries(headers || {})); } function formatMessage(message) { // get rid of trailing newlines return typeof message === 'string' ? message.trim() : String(message); } //# sourceMappingURL=client.js.map