-
-
Notifications
You must be signed in to change notification settings - Fork 2
Expand file tree
/
Copy pathfixedWindow.ts
More file actions
131 lines (116 loc) · 5.55 KB
/
fixedWindow.ts
File metadata and controls
131 lines (116 loc) · 5.55 KB
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
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
import Redis from 'ioredis';
import { RateLimiter, RateLimiterResponse, FixedWindow as Window } from '../@types/rateLimit.js';
/**
* The FixedWindow instance of a RateLimiter limits requests based on a unique user ID and a fixed time window.
* Whenever a user makes a request the following steps are performed:
* 1. Define the time window with fixed amount of queries.
* 2. Update the timestamp of the last request.
* 3. Allow the request and decrease the allowed amount of requests if the user has enough at this time window.
* 4. Otherwise, disallow the request until the next time window opens.
*/
class FixedWindow implements RateLimiter {
private capacity: number;
private keyExpiry: number;
private windowSize: number;
private client: Redis;
/**
* Create a new instance of a FixedWindow rate limiter that can be connected to any database store
* @param capacity max requests capacity in one time window
* @param windowSize rate at which the token bucket is refilled
* @param client redis client where rate limiter will cache information
*/
constructor(capacity: number, windowSize: number, client: Redis, expiry: number) {
this.capacity = capacity;
this.windowSize = windowSize;
this.client = client;
this.keyExpiry = expiry;
if (!windowSize || !capacity || windowSize <= 0 || capacity <= 0 || expiry <= 0)
throw Error('FixedWindow windowSize, capacity and keyExpiry must be positive');
}
/**
* @function processRequest - Fixed Window algorithm to allow or block
* based on the depth/complexity (in amount of tokens) of incoming requests.
* Fixed Window
* _________________________________
* | *full capacity |
* | | move to next time window
* | token adds up until full | ---------->
*____._________________________________.____
* |<-- window size -->|
*current timestamp next timestamp
*
* First, checks if a window exists in the redis cache.
* If not, then `fixedWindowStart` is set as the current timestamp, and `currentTokens` is checked against `capacity`.
* If enough room exists for the request, returns success as true and tokens as how many tokens remain in the current fixed window.
*
* If a window does exist in the cache, we first check if the timestamp is greater than the fixedWindowStart + windowSize.
* If it isn't, we update currentToken with the incoming token until reach the capcity
*
* @param {string} uuid - unique identifer used to throttle requests
* @param {number} timestamp - time the request was recieved
* @param {number} [tokens=1] - complexity of the query for throttling requests
* @return {*} {Promise<RateLimiterResponse>}
* @memberof FixedWindow
*/
async processRequest(
uuid: string,
timestamp: number,
tokens = 1
): Promise<RateLimiterResponse> {
// attempt to get the value for the uuid from the redis cache
const windowJSON = await this.client.get(uuid);
if (!windowJSON) {
const newUserWindow: Window = {
currentTokens: tokens > this.capacity ? 0 : tokens,
fixedWindowStart: timestamp,
};
if (tokens > this.capacity) {
await this.client.setex(uuid, this.keyExpiry, JSON.stringify(newUserWindow));
return { success: false, tokens: this.capacity, retryAfter: Infinity };
}
await this.client.setex(uuid, this.keyExpiry, JSON.stringify(newUserWindow));
return { success: true, tokens: this.capacity - newUserWindow.currentTokens };
}
const window: Window = await JSON.parse(windowJSON);
const previousWindowStart = window.fixedWindowStart;
const updatedUserWindow = this.updateTimeWindow(window, timestamp);
updatedUserWindow.currentTokens += tokens;
// update the currentToken until reaches its capacity
if (updatedUserWindow.currentTokens > this.capacity) {
updatedUserWindow.currentTokens -= tokens;
return {
success: false,
tokens: this.capacity - updatedUserWindow.currentTokens,
retryAfter: Math.ceil((this.windowSize - (timestamp - previousWindowStart)) / 1000),
};
}
await this.client.setex(uuid, this.keyExpiry, JSON.stringify(updatedUserWindow));
return {
success: true,
tokens: this.capacity - updatedUserWindow.currentTokens,
};
}
/**
* Resets the rate limiter to the intial state by clearing the redis store.
*/
public reset(): void {
this.client.flushall();
}
private updateTimeWindow = (window: Window, timestamp: number): Window => {
const updatedUserWindow: Window = {
currentTokens: window.currentTokens,
fixedWindowStart: window.fixedWindowStart,
};
if (timestamp >= window.fixedWindowStart + this.windowSize) {
if (timestamp >= window.fixedWindowStart + this.windowSize * 2) {
updatedUserWindow.fixedWindowStart = timestamp;
updatedUserWindow.currentTokens = 0;
} else {
updatedUserWindow.fixedWindowStart = window.fixedWindowStart + this.windowSize;
updatedUserWindow.currentTokens = 0;
}
}
return updatedUserWindow;
};
}
export default FixedWindow;