Skip to content
Open
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
122 changes: 96 additions & 26 deletions src/classes/client.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
import { CHARGILY_LIVE_URL, CHARGILY_TEST_URL } from '../consts';
import Bottleneck from 'bottleneck';
import {
Balance,
Checkout,
Expand Down Expand Up @@ -33,11 +34,20 @@ export interface ChargilyClientOptions {
*/
api_key: string;


/**
* Operating mode of the client, indicating whether to use the test or live API endpoints.
* @type {'test' | 'live'}
*/
mode: 'test' | 'live';

// Adding rate limiting options
rateLimit?: {
enabled?: boolean;
maxRequests?: number; // default is 60
intervalMs?: number; // default is 60000 (1 minute)
minTime?: number; // minimum time between requests in ms
}
}

/**
Expand All @@ -46,6 +56,7 @@ export interface ChargilyClientOptions {
export class ChargilyClient {
private api_key: string;
private base_url: string;
private limiter: Bottleneck | null = null;

/**
* Constructs a ChargilyClient instance.
Expand All @@ -55,6 +66,25 @@ export class ChargilyClient {
this.api_key = options.api_key;
this.base_url =
options.mode === 'test' ? CHARGILY_TEST_URL : CHARGILY_LIVE_URL;
// initialize rate limiter if enabled
if (options.rateLimit?.enabled !== false) {
const maxRequests = options.rateLimit?.maxRequests || 60;
const intervalMs = options.rateLimit?.intervalMs || 60000;
const minTime = options.rateLimit?.minTime || 100;

this.limiter = new Bottleneck({
minTime: minTime,
maxConcurrent: 5,
reservoir: maxRequests,
reservoirRefreshAmount: maxRequests,
reservoirRefreshInterval: intervalMs
});

// Add error handling for rate limit exceeded
this.limiter.on('error', (error) => {
console.error('Rate limiter error:', error);
});
}
}

/**
Expand All @@ -65,39 +95,79 @@ export class ChargilyClient {
* @returns {Promise<any>} - The JSON response from the API.
* @private
*/


/**
* Internal method to make requests to the Chargily API.
* @param {string} endpoint - The endpoint path to make the request to.
* @param {string} [method='GET'] - The HTTP method for the request.
* @param {Object} [body] - The request payload, necessary for POST or PATCH requests.
* @param {number} [retryCount=0] - Current retry attempt count.
* @returns {Promise<any>} - The JSON response from the API.
* @private
*/
private async request(
endpoint: string,
method: string = 'GET',
body?: any
body?: any,
retryCount: number = 0
): Promise<any> {
const url = `${this.base_url}/${endpoint}`;
const headers = {
Authorization: `Bearer ${this.api_key}`,
'Content-Type': 'application/json',
};

const fetchOptions: RequestInit = {
method,
headers,
};

if (body !== undefined) {
fetchOptions.body = JSON.stringify(body);
}

try {
const response = await fetch(url, fetchOptions);
// maximum number of retries for rate limiting
const MAX_RETRIES = 3;

// wrapped request logic to handle rate limiting and retries
const executeRequest = async () => {
const url = `${this.base_url}/${endpoint}`;
const headers = {
Authorization: `Bearer ${this.api_key}`,
'Content-Type': 'application/json',
};

const fetchOptions: RequestInit = {
method,
headers,
};

if (body !== undefined) {
fetchOptions.body = JSON.stringify(body);
}

if (!response.ok) {
throw new Error(
`API request failed with status ${response.status}: ${response.statusText}`
);
try {
const response = await fetch(url, fetchOptions);

if (response.status === 429) {
if (retryCount >= MAX_RETRIES) {
throw new Error('Rate limit exceeded. Max retries reached.');
}

const retryAfter = response.headers.get('Retry-After');
const waitTime = retryAfter ? parseInt(retryAfter) * 1000 : 5000;

console.warn(
`Rate limit exceeded. Retrying after ${waitTime / 1000} seconds...`
);
await new Promise((resolve) => setTimeout(resolve, waitTime));
return this.request(endpoint, method, body, retryCount + 1);
}

if (!response.ok) {
throw new Error(
`API request failed with status ${response.status}: ${response.statusText}`
);
}

return response.json();
} catch (error) {
const errorMessage =
error instanceof Error ? error.message : String(error);
throw new Error(`Failed to make API request: ${errorMessage}`);
}
};

return response.json();
} catch (error) {
throw new Error(`Failed to make API request: ${error}`);
}
// Use rate limiter if initialized
return this.limiter
? this.limiter.schedule(executeRequest)
: executeRequest();
}

/**
Expand Down