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 +18 -11
- package/package.json +2 -2
- package/src/index.js +135 -28
- package/typings/index.d.ts +23 -11
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
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
http.get('https://example.com/api/v1/users.json?page=
|
|
33
|
-
http.
|
|
34
|
-
|
|
35
|
-
// options hot-reloading
|
|
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 })
|
|
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
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.
|
|
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
|
|
18
|
-
|
|
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
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
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
|
-
|
|
100
|
-
|
|
101
|
-
|
|
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()
|
|
212
|
+
this.shift()
|
|
123
213
|
return
|
|
124
214
|
}
|
|
125
215
|
|
|
126
|
-
|
|
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
|
-
|
|
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
|
package/typings/index.d.ts
CHANGED
|
@@ -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
|
-
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
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
|
|
60
|
+
options?: rateLimitOptions
|
|
49
61
|
): RateLimitedAxiosInstance;
|