/
rate_limiter.ts
106 lines (90 loc) · 2.78 KB
/
rate_limiter.ts
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
/* This Source Code Form is subject to the terms of the Mozilla Public
* License, v. 2.0. If a copy of the MPL was not distributed with this
* file, You can obtain one at http://mozilla.org/MPL/2.0/. */
import { isUndefined, getMonotonicNow } from "../utils.js";
// Default rate limiter interval, in milliseconds.
export const RATE_LIMITER_INTERVAL_MS = 60 * 1000;
// Default max pings per internal.
export const MAX_PINGS_PER_INTERVAL = 40;
/**
* An enum to represent the current state of the RateLimiter.
*/
export const enum RateLimiterState {
// The RateLimiter has not reached the maximum count and is still incrementing.
Incrementing,
// The RateLimiter has reached the maximum count for the current interval.
Throttled,
}
class RateLimiter {
constructor(
// The duration of each interval, in millisecods.
private interval: number = RATE_LIMITER_INTERVAL_MS,
// The maximum count per interval.
private maxCount: number = MAX_PINGS_PER_INTERVAL,
// The count for the current interval.
private count: number = 0,
// The instant the current interval has started, in milliseconds.
private started?: number,
) {}
get elapsed(): number {
if (isUndefined(this.started)) {
return NaN;
}
const now = getMonotonicNow();
const result = now - this.started;
// It's very unlikely elapsed will be a negative number since we are using a monotonic timer
// here, but just to be extra sure, we account for it.
if (result < 0) {
return NaN;
}
return result;
}
private reset(): void {
this.started = getMonotonicNow();
this.count = 0;
}
/**
* The rate limiter should reset if
*
* 1. It has never started i.e. `started` is still `undefined`;
* 2. It has been started more than the interval time ago;
* 3. Something goes wrong while trying to calculate the elapsed time since the last reset.
*
* @returns Whether or not this rate limiter should reset.
*/
private shouldReset(): boolean {
if (isUndefined(this.started)) {
return true;
}
if (isNaN(this.elapsed) || this.elapsed > this.interval) {
return true;
}
return false;
}
/**
* Tries to increment the internal counter.
*
* @returns The current state of the RateLimiter plus the remaining time
* (in milliseconds) until the end of the current window.
*/
getState(): {
state: RateLimiterState,
remainingTime?: number,
} {
if (this.shouldReset()) {
this.reset();
}
const remainingTime = this.interval - this.elapsed;
if (this.count >= this.maxCount) {
return {
state: RateLimiterState.Throttled,
remainingTime,
};
}
this.count++;
return {
state: RateLimiterState.Incrementing
};
}
}
export default RateLimiter;