axios-rate-limit 1.4.0 → 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.4.0",
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,9 @@ 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)
19
114
  }
20
115
 
21
116
  AxiosRateLimit.prototype.getQueue = function () {
@@ -30,12 +125,11 @@ AxiosRateLimit.prototype.setMaxRPS = function (rps) {
30
125
  }
31
126
 
32
127
  AxiosRateLimit.prototype.setRateLimitOptions = function (options) {
33
- if (options.maxRPS) {
34
- this.setMaxRPS(options.maxRPS)
35
- } else {
36
- this.perMilliseconds = options.perMilliseconds
37
- this.maxRequests = options.maxRequests
38
- }
128
+ if (!options) return
129
+ var newWindows = buildWindows(options)
130
+ clearWindowsTimeouts(this.windows)
131
+ this.windows = newWindows
132
+ this.shift()
39
133
  }
40
134
 
41
135
  AxiosRateLimit.prototype.enable = function (axios) {
@@ -100,34 +194,40 @@ AxiosRateLimit.prototype.shiftInitial = function () {
100
194
 
101
195
  AxiosRateLimit.prototype.shift = function () {
102
196
  if (!this.queue.length) return
103
- if (this.timeslotRequests === this.maxRequests) {
104
- if (this.timeoutId && typeof this.timeoutId.ref === 'function') {
105
- 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
106
205
  }
107
-
108
- return
109
206
  }
110
207
 
111
208
  var queued = this.queue.shift()
112
209
  var resolved = queued.resolve()
113
210
 
114
- if (this.timeslotRequests === 0) {
115
- this.timeoutId = setTimeout(function () {
116
- this.timeslotRequests = 0
117
- this.shift()
118
- }.bind(this), this.perMilliseconds)
119
-
120
- if (typeof this.timeoutId.unref === 'function') {
121
- if (this.queue.length === 0) this.timeoutId.unref()
122
- }
123
- }
124
-
125
211
  if (!resolved) {
126
- this.shift() // rejected request --> shift another request
212
+ this.shift()
127
213
  return
128
214
  }
129
215
 
130
- 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
+ }
131
231
  }
132
232
 
133
233
  /**
@@ -157,7 +257,9 @@ AxiosRateLimit.prototype.shift = function () {
157
257
  */
158
258
  function axiosRateLimit (axios, options) {
159
259
  var rateLimitInstance = new AxiosRateLimit(axios)
160
- rateLimitInstance.setRateLimitOptions(options)
260
+ if (options != null) {
261
+ rateLimitInstance.setRateLimitOptions(options)
262
+ }
161
263
 
162
264
  axios.getQueue = AxiosRateLimit.prototype.getQueue.bind(rateLimitInstance)
163
265
  axios.getMaxRPS = AxiosRateLimit.prototype.getMaxRPS.bind(rateLimitInstance)
@@ -8,7 +8,7 @@ export interface RateLimitedAxiosInstance extends AxiosInstance {
8
8
  getQueue: () => RateLimitRequestHandler[],
9
9
  getMaxRPS: () => number,
10
10
  setMaxRPS: (rps: number) => void,
11
- setRateLimitOptions: (options: rateLimitOptions) => void,
11
+ setRateLimitOptions: (options?: rateLimitOptions) => void,
12
12
  // enable(axios: any): void,
13
13
  // handleRequest(request:any):any,
14
14
  // handleResponse(response: any): any,
@@ -17,10 +17,17 @@ export interface RateLimitedAxiosInstance extends AxiosInstance {
17
17
  // shift():any
18
18
  }
19
19
 
20
+ export type RateLimitEntry = {
21
+ maxRequests: number,
22
+ duration: string | number
23
+ };
24
+
20
25
  export type rateLimitOptions = {
21
26
  maxRequests?: number,
22
27
  perMilliseconds?: number,
23
- maxRPS?: number
28
+ maxRPS?: number,
29
+ duration?: string | number,
30
+ limits?: RateLimitEntry[]
24
31
  };
25
32
 
26
33
  /**
@@ -50,5 +57,5 @@ export type rateLimitOptions = {
50
57
  */
51
58
  export default function axiosRateLimit(
52
59
  axiosInstance: AxiosInstance,
53
- options: rateLimitOptions
60
+ options?: rateLimitOptions
54
61
  ): RateLimitedAxiosInstance;