From 6444fbafb426b3f22a8853359ae4f24b4a50ff77 Mon Sep 17 00:00:00 2001 From: Leonardo Rochael Almida Date: Wed, 24 Jul 2024 19:17:42 +0200 Subject: [PATCH] Enable sharing a rate limiter Between multiple axios clients, sharing the rate limiting between them. --- README.md | 21 +++++++++++++++++ __tests__/index.js | 28 +++++++++++++++++++++++ src/index.js | 57 ++++++++++++++++++++++++++++++++++------------ typings/index.d.ts | 30 ++++++++++++++++++++++-- 4 files changed, 119 insertions(+), 17 deletions(-) diff --git a/README.md b/README.md index 060f175..891512c 100644 --- a/README.md +++ b/README.md @@ -39,6 +39,27 @@ http.getMaxRPS() // 3 http.setRateLimitOptions({ maxRequests: 6, perMilliseconds: 150 }) // same options as constructor ``` +You can also share a rate limiter between axios instances + +```javascript +import axios from 'axios'; +import rateLimit, { getLimiter } from 'axios-rate-limit'; + +const rateLimiter = getLimiter({ maxRPS: 2 }) + +const http1 = rateLimiter.enabled(axios.create({baseUrl: 'http://example.com/api/v1/users/1'})) +// another way of doing the same thing: +const http2 = rateLimit( + axios.create({baseUrl: 'http://example.com/api/v1/users/2'}), + { rateLimiter: rateLimiter } +) + +http1.get('/info.json') // will perform immediately +http2.get('/info.json') // will perform immediately +http1.get('/info.json') // will after one second from the first one + +``` + ## Alternatives Consider using Axios built-in [rate-limiting](https://www.npmjs.com/package/axios#user-content--rate-limiting) functionality. diff --git a/__tests__/index.js b/__tests__/index.js index 6032427..ec7888a 100644 --- a/__tests__/index.js +++ b/__tests__/index.js @@ -191,3 +191,31 @@ it('not delay requests if requests are cancelled', async function () { expect(end - start).toBeLessThan(perMilliseconds * 2) expect(end - start).toBeGreaterThan(perMilliseconds) }) + +it('can share a limiter between multiple axios instances', async function () { + function adapter (config) { return Promise.resolve(config) } + + var limiter = axiosRateLimit.getLimiter({ + maxRequests: 2, perMilliseconds: 100 + }) + + var http1 = limiter.enable(axios.create({ adapter: adapter })) + // another way of doing the same thing: + var http2 = axiosRateLimit( + axios.create({ adapter: adapter }), { rateLimiter: limiter } + ) + + var onSuccess = sinon.spy() + + var requests = [] + requests.push(http1.get('/users/1').then(onSuccess)) + requests.push(http1.get('/users/2').then(onSuccess)) + + requests.push(http2.get('/users/3').then(onSuccess)) + requests.push(http2.get('/users/4').then(onSuccess)) + + await delay(90) + expect(onSuccess.callCount).toEqual(2) + await Promise.all(requests) + expect(onSuccess.callCount).toEqual(4) +}) diff --git a/src/index.js b/src/index.js index 0b20c52..a5ab975 100644 --- a/src/index.js +++ b/src/index.js @@ -1,16 +1,13 @@ -function AxiosRateLimit (axios) { +function AxiosRateLimit (options) { this.queue = [] this.timeslotRequests = 0 - this.interceptors = { - request: null, - response: null - } + this.interceptors = [] this.handleRequest = this.handleRequest.bind(this) this.handleResponse = this.handleResponse.bind(this) - this.enable(axios) + this.setRateLimitOptions(options) } AxiosRateLimit.prototype.getMaxRPS = function () { @@ -43,14 +40,27 @@ AxiosRateLimit.prototype.enable = function (axios) { return Promise.reject(error) } - this.interceptors.request = axios.interceptors.request.use( + var requestInterceptor = axios.interceptors.request.use( this.handleRequest, handleError ) - this.interceptors.response = axios.interceptors.response.use( + var responseInterceptor = axios.interceptors.response.use( this.handleResponse, handleError ) + + this.interceptors.push({ + axios: axios, + request: requestInterceptor, + response: responseInterceptor + }) + + axios.getQueue = this.getQueue.bind(this) + axios.getMaxRPS = this.getMaxRPS.bind(this) + axios.setMaxRPS = this.setMaxRPS.bind(this) + axios.setRateLimitOptions = this.setRateLimitOptions.bind(this) + + return axios } /* @@ -156,16 +166,33 @@ AxiosRateLimit.prototype.shift = function () { * @returns {Object} axios instance with interceptors added */ function axiosRateLimit (axios, options) { - var rateLimitInstance = new AxiosRateLimit(axios) - rateLimitInstance.setRateLimitOptions(options) + var rateLimitInstance = options.rateLimiter || new AxiosRateLimit(options) - axios.getQueue = AxiosRateLimit.prototype.getQueue.bind(rateLimitInstance) - axios.getMaxRPS = AxiosRateLimit.prototype.getMaxRPS.bind(rateLimitInstance) - axios.setMaxRPS = AxiosRateLimit.prototype.setMaxRPS.bind(rateLimitInstance) - axios.setRateLimitOptions = AxiosRateLimit.prototype.setRateLimitOptions - .bind(rateLimitInstance) + rateLimitInstance.enable(axios) return axios } +/** + * Create a new rate limiter instance. It can be shared between multiple axios instances. + * The rate-limiting is shared between axios instances that are enabled with this rate limiter. + * + * @example + * import rateLimit, { getLimiter } from 'axios-rate-limit'; + * + * const limiter = getLimiter({ maxRequests: 2, perMilliseconds: 1000 }) + * // limit an axios instance with this rate limiter: + * const http1 = limiter.enable(axios.create()) + * // another way of doing the same thing: + * const http2 = rateLimit(axios.create(), { rateLimiter: limiter }) + * + * @param {Object} options options for rate limit, same as for rateLimit() + * @returns {Object} rate limiter instance + */ +function getLimiter (options) { + return new AxiosRateLimit(options) +} + module.exports = axiosRateLimit +module.exports.AxiosRateLimiter = AxiosRateLimit +module.exports.getLimiter = getLimiter diff --git a/typings/index.d.ts b/typings/index.d.ts index 4ef593e..9b2ae00 100644 --- a/typings/index.d.ts +++ b/typings/index.d.ts @@ -4,7 +4,7 @@ export type RateLimitRequestHandler = { resolve: () => boolean } -export interface RateLimitedAxiosInstance extends AxiosInstance { +export interface RateLimiter { getQueue: () => RateLimitRequestHandler[], getMaxRPS: () => number, setMaxRPS: (rps: number) => void, @@ -17,12 +17,38 @@ export interface RateLimitedAxiosInstance extends AxiosInstance { // shift():any } +export interface RateLimitedAxiosInstance extends AxiosInstance, RateLimiter {}; + export type rateLimitOptions = { maxRequests?: number, perMilliseconds?: number, maxRPS?: number }; +export interface AxiosRateLimiter extends RateLimiter {}; + +export class AxiosRateLimiter implements RateLimiter { + constructor(options: rateLimitOptions); +} + +/** + * Create a new rate limiter instance. It can be shared between multiple axios instances. + * The rate-limiting is shared between axios instances that are enabled with this rate limiter. + * + * @example + * import rateLimit, { getLimiter } from 'axios-rate-limit'; + * + * const limiter = getLimiter({ maxRequests: 2, perMilliseconds: 1000 }) + * // limit an axios instance with this rate limiter: + * const http1 = limiter.enable(axios.create()) + * // another way of doing the same thing: + * const http2 = rateLimit(axios.create(), { rateLimiter: limiter }) + * + * @param {Object} options options for rate limit, same as for rateLimit() + * @returns {Object} rate limiter instance + */ +export function getLimiter (options: rateLimitOptions): AxiosRateLimiter; + /** * Apply rate limit to axios instance. * @@ -50,5 +76,5 @@ export type rateLimitOptions = { */ export default function axiosRateLimit( axiosInstance: AxiosInstance, - options: rateLimitOptions + options: rateLimitOptions & { rateLimiter?: AxiosRateLimiter } ): RateLimitedAxiosInstance;