axios-rate-limit 1.3.3 → 1.5.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md CHANGED
@@ -23,21 +23,28 @@ npm install axios-rate-limit
23
23
  import axios from 'axios';
24
24
  import rateLimit from 'axios-rate-limit';
25
25
 
26
- // sets max 2 requests per 1 second, other will be delayed
27
- // note maxRPS is a shorthand for perMilliseconds: 1000, and it takes precedence
28
- // if specified both with maxRequests and perMilliseconds
29
- const http = rateLimit(axios.create(), { maxRequests: 2, perMilliseconds: 1000, maxRPS: 2 })
30
- http.getMaxRPS() // 2
31
- http.get('https://example.com/api/v1/users.json?page=1') // will perform immediately
32
- http.get('https://example.com/api/v1/users.json?page=2') // will perform immediately
33
- http.get('https://example.com/api/v1/users.json?page=3') // will perform after 1 second from the first one
34
-
35
- // options hot-reloading also available
26
+ const http = rateLimit(axios.create(), {
27
+ limits: [
28
+ { maxRequests: 5, duration: '2s' },
29
+ { maxRequests: 2, duration: '500ms' }
30
+ ]
31
+ })
32
+ http.get('https://example.com/api/v1/users.json?page=1')
33
+ http.getQueue()
34
+
35
+ // options hot-reloading (same options as constructor)
36
36
  http.setMaxRPS(3)
37
37
  http.getMaxRPS() // 3
38
- http.setRateLimitOptions({ maxRequests: 6, perMilliseconds: 150 }) // same options as constructor
38
+ http.setRateLimitOptions({ maxRequests: 6, perMilliseconds: 150 })
39
+ http.setRateLimitOptions({ maxRequests: 10, duration: '1s' })
40
+ http.setRateLimitOptions({ limits: [{ maxRequests: 3, duration: '1s' }, { maxRequests: 1, duration: '200ms' }] })
39
41
  ```
40
42
 
43
+ ## Tech Details
44
+
45
+ The axios-rate-limit implements fixed-window, queued rate limiter. The main disadvantage of this
46
+ approach is possibility of bursts at window boundaries in case of limit hit.
47
+
41
48
  ## Alternatives
42
49
 
43
50
  Consider using Axios built-in [rate-limiting](https://www.npmjs.com/package/axios#user-content--rate-limiting) functionality.
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "axios-rate-limit",
3
- "description": "Rate limit for axios.",
4
- "version": "1.3.3",
3
+ "description": "Rate limit for axios",
4
+ "version": "1.5.0",
5
5
  "license": "MIT",
6
6
  "bugs": {
7
7
  "url": "https://github.com/aishek/axios-rate-limit/issues"
package/src/index.js CHANGED
@@ -1,6 +1,100 @@
1
+ var DURATION_MSG = " Expected format: number+unit ms, s, m, h (e.g. '1s')."
2
+
3
+ var DURATION_UNITS = { ms: 1, s: 1000, m: 60000, h: 3600000 }
4
+
5
+ function throwDurationError (value) {
6
+ var msg = "Unrecognized duration: '" + String(value) + "'." + DURATION_MSG
7
+ throw new Error(msg)
8
+ }
9
+
10
+ function parseDuration (value) {
11
+ if (typeof value === 'number' && !isNaN(value)) {
12
+ if (value < 0) throwDurationError(value)
13
+ return value
14
+ }
15
+ if (typeof value !== 'string') {
16
+ throwDurationError(value)
17
+ }
18
+ var s = value.trim()
19
+ var num
20
+ var mult
21
+ if (s.length >= 2 && s.slice(-2) === 'ms') {
22
+ num = parseFloat(s.slice(0, -2))
23
+ mult = DURATION_UNITS.ms
24
+ } else if (s.length >= 1) {
25
+ var u = s.slice(-1)
26
+ mult = DURATION_UNITS[u]
27
+ if (mult == null) throwDurationError(value)
28
+ num = parseFloat(s.slice(0, -1))
29
+ } else {
30
+ throwDurationError(value)
31
+ }
32
+ if (isNaN(num) || num < 0) {
33
+ throwDurationError(value)
34
+ }
35
+ return num * mult
36
+ }
37
+
38
+ function buildWindows (options) {
39
+ var limits = options && options.limits
40
+ if (limits && limits.length > 0) {
41
+ return limits.map(function (limit, i) {
42
+ var max = limit.maxRequests
43
+ if (typeof max !== 'number' || !isFinite(max) || max <= 0) {
44
+ throw new Error(
45
+ 'Invalid rate limit option at limits[' + i + ']: ' +
46
+ 'maxRequests is required and must be a positive number.'
47
+ )
48
+ }
49
+ var perMs = parseDuration(limit.duration)
50
+ if (typeof perMs !== 'number' || !isFinite(perMs) || perMs <= 0) {
51
+ throw new Error(
52
+ 'Invalid rate limit option at limits[' + i + ']: ' +
53
+ 'duration must be a positive finite number.'
54
+ )
55
+ }
56
+ return { count: 0, max: max, perMs: perMs, timeoutId: null }
57
+ })
58
+ }
59
+ var maxRequests = options.maxRequests
60
+ var perMs
61
+ if (options.maxRPS != null) {
62
+ maxRequests = options.maxRPS
63
+ perMs = 1000
64
+ } else {
65
+ var optD = options.duration
66
+ perMs = optD != null ? parseDuration(optD) : options.perMilliseconds
67
+ }
68
+ if (typeof perMs !== 'number' || !isFinite(perMs) || perMs <= 0) {
69
+ throw new Error(
70
+ 'Invalid rate limit options: one of maxRPS, duration, or ' +
71
+ 'perMilliseconds is required and must be positive.'
72
+ )
73
+ }
74
+ var maxInvalid = typeof maxRequests !== 'number' ||
75
+ !isFinite(maxRequests) || maxRequests <= 0
76
+ if (maxInvalid) {
77
+ throw new Error(
78
+ 'Invalid rate limit options: maxRequests is required and ' +
79
+ 'must be a positive number.'
80
+ )
81
+ }
82
+ return [{ count: 0, max: maxRequests, perMs: perMs, timeoutId: null }]
83
+ }
84
+
85
+ function clearWindowsTimeouts (windows) {
86
+ if (!windows) return
87
+ for (var i = 0; i < windows.length; i++) {
88
+ if (windows[i].timeoutId != null) {
89
+ clearTimeout(windows[i].timeoutId)
90
+ windows[i].timeoutId = null
91
+ }
92
+ }
93
+ }
94
+
1
95
  function AxiosRateLimit (axios) {
2
96
  this.queue = []
3
- this.timeslotRequests = 0
97
+ this.windows = []
4
98
 
5
99
  this.interceptors = {
6
100
  request: null,
@@ -14,8 +108,13 @@ function AxiosRateLimit (axios) {
14
108
  }
15
109
 
16
110
  AxiosRateLimit.prototype.getMaxRPS = function () {
17
- var perSeconds = (this.perMilliseconds / 1000)
18
- return this.maxRequests / perSeconds
111
+ var w = this.windows[0]
112
+ if (!w) return 0
113
+ return w.max / (w.perMs / 1000)
114
+ }
115
+
116
+ AxiosRateLimit.prototype.getQueue = function () {
117
+ return this.queue
19
118
  }
20
119
 
21
120
  AxiosRateLimit.prototype.setMaxRPS = function (rps) {
@@ -26,12 +125,11 @@ AxiosRateLimit.prototype.setMaxRPS = function (rps) {
26
125
  }
27
126
 
28
127
  AxiosRateLimit.prototype.setRateLimitOptions = function (options) {
29
- if (options.maxRPS) {
30
- this.setMaxRPS(options.maxRPS)
31
- } else {
32
- this.perMilliseconds = options.perMilliseconds
33
- this.maxRequests = options.maxRequests
34
- }
128
+ if (!options) return
129
+ var newWindows = buildWindows(options)
130
+ clearWindowsTimeouts(this.windows)
131
+ this.windows = newWindows
132
+ this.shift()
35
133
  }
36
134
 
37
135
  AxiosRateLimit.prototype.enable = function (axios) {
@@ -96,34 +194,40 @@ AxiosRateLimit.prototype.shiftInitial = function () {
96
194
 
97
195
  AxiosRateLimit.prototype.shift = function () {
98
196
  if (!this.queue.length) return
99
- if (this.timeslotRequests === this.maxRequests) {
100
- if (this.timeoutId && typeof this.timeoutId.ref === 'function') {
101
- this.timeoutId.ref()
197
+ var windows = this.windows
198
+ for (var i = 0; i < windows.length; i++) {
199
+ if (windows[i].count === windows[i].max) {
200
+ var tid = windows[i].timeoutId
201
+ if (tid && typeof tid.ref === 'function') {
202
+ tid.ref()
203
+ }
204
+ return
102
205
  }
103
-
104
- return
105
206
  }
106
207
 
107
208
  var queued = this.queue.shift()
108
209
  var resolved = queued.resolve()
109
210
 
110
- if (this.timeslotRequests === 0) {
111
- this.timeoutId = setTimeout(function () {
112
- this.timeslotRequests = 0
113
- this.shift()
114
- }.bind(this), this.perMilliseconds)
115
-
116
- if (typeof this.timeoutId.unref === 'function') {
117
- if (this.queue.length === 0) this.timeoutId.unref()
118
- }
119
- }
120
-
121
211
  if (!resolved) {
122
- this.shift() // rejected request --> shift another request
212
+ this.shift()
123
213
  return
124
214
  }
125
215
 
126
- this.timeslotRequests += 1
216
+ var self = this
217
+ for (var j = 0; j < windows.length; j++) {
218
+ var w = windows[j]
219
+ w.count += 1
220
+ if (w.count === 1) {
221
+ w.timeoutId = setTimeout(function (win) {
222
+ win.count = 0
223
+ win.timeoutId = null
224
+ self.shift()
225
+ }.bind(null, w), w.perMs)
226
+ if (typeof w.timeoutId.unref === 'function') {
227
+ if (this.queue.length === 0) w.timeoutId.unref()
228
+ }
229
+ }
230
+ }
127
231
  }
128
232
 
129
233
  /**
@@ -153,8 +257,11 @@ AxiosRateLimit.prototype.shift = function () {
153
257
  */
154
258
  function axiosRateLimit (axios, options) {
155
259
  var rateLimitInstance = new AxiosRateLimit(axios)
156
- rateLimitInstance.setRateLimitOptions(options)
260
+ if (options != null) {
261
+ rateLimitInstance.setRateLimitOptions(options)
262
+ }
157
263
 
264
+ axios.getQueue = AxiosRateLimit.prototype.getQueue.bind(rateLimitInstance)
158
265
  axios.getMaxRPS = AxiosRateLimit.prototype.getMaxRPS.bind(rateLimitInstance)
159
266
  axios.setMaxRPS = AxiosRateLimit.prototype.setMaxRPS.bind(rateLimitInstance)
160
267
  axios.setRateLimitOptions = AxiosRateLimit.prototype.setRateLimitOptions
@@ -1,21 +1,33 @@
1
1
  import { AxiosInstance } from 'axios';
2
2
 
3
+ export type RateLimitRequestHandler = {
4
+ resolve: () => boolean
5
+ }
6
+
3
7
  export interface RateLimitedAxiosInstance extends AxiosInstance {
4
- getMaxRPS: () => number,
5
- setMaxRPS: (rps: number) => void,
6
- setRateLimitOptions: (options: rateLimitOptions) => void,
7
- // enable(axios: any): void,
8
- // handleRequest(request:any):any,
9
- // handleResponse(response: any): any,
10
- // push(requestHandler:any):any,
11
- // shiftInitial():any,
12
- // shift():any
8
+ getQueue: () => RateLimitRequestHandler[],
9
+ getMaxRPS: () => number,
10
+ setMaxRPS: (rps: number) => void,
11
+ setRateLimitOptions: (options?: rateLimitOptions) => void,
12
+ // enable(axios: any): void,
13
+ // handleRequest(request:any):any,
14
+ // handleResponse(response: any): any,
15
+ // push(requestHandler:any):any,
16
+ // shiftInitial():any,
17
+ // shift():any
13
18
  }
14
19
 
20
+ export type RateLimitEntry = {
21
+ maxRequests: number,
22
+ duration: string | number
23
+ };
24
+
15
25
  export type rateLimitOptions = {
16
26
  maxRequests?: number,
17
27
  perMilliseconds?: number,
18
- maxRPS?: number
28
+ maxRPS?: number,
29
+ duration?: string | number,
30
+ limits?: RateLimitEntry[]
19
31
  };
20
32
 
21
33
  /**
@@ -45,5 +57,5 @@ export type rateLimitOptions = {
45
57
  */
46
58
  export default function axiosRateLimit(
47
59
  axiosInstance: AxiosInstance,
48
- options: rateLimitOptions
60
+ options?: rateLimitOptions
49
61
  ): RateLimitedAxiosInstance;