swell-node
Version:
Swell API client for NodeJS
358 lines • 13.2 kB
JavaScript
"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