UNPKG

4.68 kBPlain TextView Raw
1import { Interval, TokenBucket } from "./TokenBucket";
2import { getMilliseconds, wait } from "./clock";
3
4export type RateLimiterOpts = {
5 tokensPerInterval: number;
6 interval: Interval;
7 fireImmediately?: boolean;
8};
9
10/**
11 * A generic rate limiter. Underneath the hood, this uses a token bucket plus
12 * an additional check to limit how many tokens we can remove each interval.
13 *
14 * @param options
15 * @param options.tokensPerInterval Maximum number of tokens that can be
16 * removed at any given moment and over the course of one interval.
17 * @param options.interval The interval length in milliseconds, or as
18 * one of the following strings: 'second', 'minute', 'hour', day'.
19 * @param options.fireImmediately Whether or not the promise will resolve
20 * immediately when rate limiting is in effect (default is false).
21 */
22export class RateLimiter {
23 tokenBucket: TokenBucket;
24 curIntervalStart: number;
25 tokensThisInterval: number;
26 fireImmediately: boolean;
27
28 constructor({ tokensPerInterval, interval, fireImmediately }: RateLimiterOpts) {
29 this.tokenBucket = new TokenBucket({
30 bucketSize: tokensPerInterval,
31 tokensPerInterval,
32 interval,
33 });
34
35 // Fill the token bucket to start
36 this.tokenBucket.content = tokensPerInterval;
37
38 this.curIntervalStart = getMilliseconds();
39 this.tokensThisInterval = 0;
40 this.fireImmediately = fireImmediately ?? false;
41 }
42
43 /**
44 * Remove the requested number of tokens. If the rate limiter contains enough
45 * tokens and we haven't spent too many tokens in this interval already, this
46 * will happen immediately. Otherwise, the removal will happen when enough
47 * tokens become available.
48 * @param count The number of tokens to remove.
49 * @returns A promise for the remainingTokens count.
50 */
51 async removeTokens(count: number): Promise<number> {
52 // Make sure the request isn't for more than we can handle
53 if (count > this.tokenBucket.bucketSize) {
54 throw new Error(
55 `Requested tokens ${count} exceeds maximum tokens per interval ${this.tokenBucket.bucketSize}`
56 );
57 }
58
59 const now = getMilliseconds();
60
61 // Advance the current interval and reset the current interval token count
62 // if needed
63 if (now < this.curIntervalStart || now - this.curIntervalStart >= this.tokenBucket.interval) {
64 this.curIntervalStart = now;
65 this.tokensThisInterval = 0;
66 }
67
68 // If we don't have enough tokens left in this interval, wait until the
69 // next interval
70 if (count > this.tokenBucket.tokensPerInterval - this.tokensThisInterval) {
71 if (this.fireImmediately) {
72 return -1;
73 } else {
74 const waitMs = Math.ceil(this.curIntervalStart + this.tokenBucket.interval - now);
75 await wait(waitMs);
76 const remainingTokens = await this.tokenBucket.removeTokens(count);
77 this.tokensThisInterval += count;
78 return remainingTokens;
79 }
80 }
81
82 // Remove the requested number of tokens from the token bucket
83 const remainingTokens = await this.tokenBucket.removeTokens(count);
84 this.tokensThisInterval += count;
85 return remainingTokens;
86 }
87
88 /**
89 * Attempt to remove the requested number of tokens and return immediately.
90 * If the bucket (and any parent buckets) contains enough tokens and we
91 * haven't spent too many tokens in this interval already, this will return
92 * true. Otherwise, false is returned.
93 * @param {Number} count The number of tokens to remove.
94 * @param {Boolean} True if the tokens were successfully removed, otherwise
95 * false.
96 */
97 tryRemoveTokens(count: number): boolean {
98 // Make sure the request isn't for more than we can handle
99 if (count > this.tokenBucket.bucketSize) return false;
100
101 const now = getMilliseconds();
102
103 // Advance the current interval and reset the current interval token count
104 // if needed
105 if (now < this.curIntervalStart || now - this.curIntervalStart >= this.tokenBucket.interval) {
106 this.curIntervalStart = now;
107 this.tokensThisInterval = 0;
108 }
109
110 // If we don't have enough tokens left in this interval, return false
111 if (count > this.tokenBucket.tokensPerInterval - this.tokensThisInterval) return false;
112
113 // Try to remove the requested number of tokens from the token bucket
114 const removed = this.tokenBucket.tryRemoveTokens(count);
115 if (removed) {
116 this.tokensThisInterval += count;
117 }
118 return removed;
119 }
120
121 /**
122 * Returns the number of tokens remaining in the TokenBucket.
123 * @returns {Number} The number of tokens remaining.
124 */
125 getTokensRemaining(): number {
126 this.tokenBucket.drip();
127 return this.tokenBucket.content;
128 }
129}