Skip to content

Commit

Permalink
Replace the axios rate limiter lib with own
Browse files Browse the repository at this point in the history
  • Loading branch information
victorkirov committed Oct 28, 2024
1 parent 35aba9e commit 5f96dbf
Show file tree
Hide file tree
Showing 4 changed files with 116 additions and 15 deletions.
18 changes: 16 additions & 2 deletions api/esplora/esploraAPiProvider.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,4 @@
import axios, { AxiosInstance, AxiosRequestConfig } from 'axios';
import rateLimit from 'axios-rate-limit';
import axiosRetry from 'axios-retry';
import { XVERSE_BTC_BASE_URI_MAINNET, XVERSE_BTC_BASE_URI_SIGNET, XVERSE_BTC_BASE_URI_TESTNET } from '../../constant';
import {
Expand All @@ -12,6 +11,7 @@ import {
TransactionOutspend,
UTXO,
} from '../../types';
import { AxiosRateLimit } from '../../utils/axiosRateLimit';

export interface EsploraApiProviderOptions {
network: NetworkType;
Expand All @@ -22,6 +22,8 @@ export interface EsploraApiProviderOptions {
export class BitcoinEsploraApiProvider {
bitcoinApi: AxiosInstance;

rateLimiter: AxiosRateLimit;

fallbackBitcoinApi?: AxiosInstance;

_network: NetworkType;
Expand All @@ -48,13 +50,25 @@ export class BitcoinEsploraApiProvider {
const axiosConfig: AxiosRequestConfig = { baseURL };

this._network = network;
this.bitcoinApi = rateLimit(axios.create(axiosConfig), {
this.bitcoinApi = axios.create(axiosConfig);

this.rateLimiter = new AxiosRateLimit(this.bitcoinApi, {
maxRPS: 10,
});

axiosRetry(this.bitcoinApi, {
retries: 1,
retryDelay: axiosRetry.exponentialDelay,
retryCondition: (error) => {
if (
error?.code === 'ECONNABORTED' ||
error?.response?.status === 429 ||
(error?.response?.status ?? 0) >= 500
) {
return true;
}
return false;
},
});

if (fallbackUrl) {
Expand Down
12 changes: 0 additions & 12 deletions package-lock.json

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

1 change: 0 additions & 1 deletion package.json
Original file line number Diff line number Diff line change
Expand Up @@ -37,7 +37,6 @@
"@zondax/ledger-stacks": "^1.0.4",
"async-mutex": "^0.4.0",
"axios": "1.7.7",
"axios-rate-limit": "1.4.0",
"axios-retry": "4.5.0",
"base64url": "^3.0.1",
"bip32": "^4.0.0",
Expand Down
100 changes: 100 additions & 0 deletions utils/axiosRateLimit.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,100 @@
/**
* inspired by axios-rate-limit with a few adjustments and bug fixes
* https://github.com/aishek/axios-rate-limit
*/

import { AxiosInstance, InternalAxiosRequestConfig } from 'axios';

type RateLimitRequestHandler = {
resolve: () => boolean;
};

type RateLimitOptions =
| {
maxRPS: number;
}
| {
maxRequests: number;
perMilliseconds: number;
};

function throwIfCancellationRequested(config: InternalAxiosRequestConfig<any>) {
if (config.cancelToken) {
config.cancelToken.throwIfRequested();
}
}

export class AxiosRateLimit {
private queue: RateLimitRequestHandler[];

private timeslotRequests: number;

private perMilliseconds: number;

private maxRequests: number;

constructor(axios: AxiosInstance, options: RateLimitOptions) {
this.queue = [];
this.timeslotRequests = 0;

if ('maxRPS' in options) {
this.perMilliseconds = 1000;
this.maxRequests = options.maxRPS;
} else {
this.perMilliseconds = options.perMilliseconds;
this.maxRequests = options.maxRequests;
}

function handleError(error: unknown) {
return Promise.reject(error);
}

axios.interceptors.request.use(this.handleRequest, handleError);
}

private handleRequest = (request: InternalAxiosRequestConfig<any>) => {
return new Promise<InternalAxiosRequestConfig<any>>((resolve, reject) => {
this.push({
resolve: function () {
try {
throwIfCancellationRequested(request);
} catch (error) {
reject(error);
return false;
}
resolve(request);
return true;
},
});
});
};

private push = (requestHandler: RateLimitRequestHandler) => {
this.queue.push(requestHandler);
this.shift();
};

private onRequestTimerMet = () => {
this.timeslotRequests--;
this.shift();
};

private shift = () => {
if (this.timeslotRequests >= this.maxRequests) return;

const queued = this.queue.shift();
if (!queued) return;

const resolved = queued.resolve();

if (!resolved) {
this.shift(); // rejected request --> shift another request
return;
}

this.timeslotRequests++;
setTimeout(this.onRequestTimerMet, this.perMilliseconds);

this.timeslotRequests += 1;
};
}

0 comments on commit 5f96dbf

Please sign in to comment.