forked from express-rate-limit/express-rate-limit
-
Notifications
You must be signed in to change notification settings - Fork 0
/
Copy pathmemory-store.ts
211 lines (186 loc) · 5.57 KB
/
memory-store.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
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
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
// /source/memory-store.ts
// A memory store for hit counts
import type { Store, Options, ClientRateLimitInfo } from './types.js'
/**
* The record that stores information about a client - namely, how many times
* they have hit the endpoint, and when their hit count resets.
*
* Similar to `ClientRateLimitInfo`, except `resetTime` is a compulsory field.
*/
type Client = {
totalHits: number
resetTime: Date
}
/**
* A `Store` that stores the hit count for each client in memory.
*
* @public
*/
export default class MemoryStore implements Store {
/**
* The duration of time before which all hit counts are reset (in milliseconds).
*/
windowMs!: number
/**
* These two maps store usage (requests) and reset time by key (for example, IP
* addresses or API keys).
*
* They are split into two to avoid having to iterate through the entire set to
* determine which ones need reset. Instead, `Client`s are moved from `previous`
* to `current` as they hit the endpoint. Once `windowMs` has elapsed, all clients
* left in `previous`, i.e., those that have not made any recent requests, are
* known to be expired and can be deleted in bulk.
*/
previous = new Map<string, Client>()
current = new Map<string, Client>()
/**
* A reference to the active timer.
*/
interval?: NodeJS.Timeout
/**
* Confirmation that the keys incremented in once instance of MemoryStore
* cannot affect other instances.
*/
localKeys = true
/**
* Method that initializes the store.
*
* @param options {Options} - The options used to setup the middleware.
*/
init(options: Options): void {
// Get the duration of a window from the options.
this.windowMs = options.windowMs
// Indicates that init was called more than once.
// Could happen if a store was shared between multiple instances.
if (this.interval) clearInterval(this.interval)
// Reset all clients left in previous every `windowMs`.
this.interval = setInterval(() => {
this.clearExpired()
}, this.windowMs)
// Cleaning up the interval will be taken care of by the `shutdown` method.
if (this.interval.unref) this.interval.unref()
}
/**
* Method to fetch a client's hit count and reset time.
*
* @param key {string} - The identifier for a client.
*
* @returns {ClientRateLimitInfo | undefined} - The number of hits and reset time for that client.
*
* @public
*/
async get(key: string): Promise<ClientRateLimitInfo | undefined> {
return this.current.get(key) ?? this.previous.get(key)
}
/**
* Method to increment a client's hit counter.
*
* @param key {string} - The identifier for a client.
*
* @returns {ClientRateLimitInfo} - The number of hits and reset time for that client.
*
* @public
*/
async increment(key: string): Promise<ClientRateLimitInfo> {
const client = this.getClient(key)
const now = Date.now()
if (client.resetTime.getTime() <= now) {
this.resetClient(client, now)
}
client.totalHits++
return client
}
/**
* Method to decrement a client's hit counter.
*
* @param key {string} - The identifier for a client.
*
* @public
*/
async decrement(key: string): Promise<void> {
const client = this.getClient(key)
if (client.totalHits > 0) client.totalHits--
}
/**
* Method to reset a client's hit counter.
*
* @param key {string} - The identifier for a client.
*
* @public
*/
async resetKey(key: string): Promise<void> {
this.current.delete(key)
this.previous.delete(key)
}
/**
* Method to reset everyone's hit counter.
*
* @public
*/
async resetAll(): Promise<void> {
this.current.clear()
this.previous.clear()
}
/**
* Method to stop the timer (if currently running) and prevent any memory
* leaks.
*
* @public
*/
shutdown(): void {
clearInterval(this.interval)
void this.resetAll()
}
/**
* Recycles a client by setting its hit count to zero, and reset time to
* `windowMs` milliseconds from now.
*
* NOT to be confused with `#resetKey()`, which removes a client from both the
* `current` and `previous` maps.
*
* @param client {Client} - The client to recycle.
* @param now {number} - The current time, to which the `windowMs` is added to get the `resetTime` for the client.
*
* @return {Client} - The modified client that was passed in, to allow for chaining.
*/
private resetClient(client: Client, now = Date.now()): Client {
client.totalHits = 0
client.resetTime.setTime(now + this.windowMs)
return client
}
/**
* Retrieves or creates a client, given a key. Also ensures that the client being
* returned is in the `current` map.
*
* @param key {string} - The key under which the client is (or is to be) stored.
*
* @returns {Client} - The requested client.
*/
private getClient(key: string): Client {
// If we already have a client for that key in the `current` map, return it.
if (this.current.has(key)) return this.current.get(key)!
let client
if (this.previous.has(key)) {
// If it's in the `previous` map, take it out
client = this.previous.get(key)!
this.previous.delete(key)
} else {
// Finally, if we don't have an existing entry for this client, create a new one
client = { totalHits: 0, resetTime: new Date() }
this.resetClient(client)
}
// Make sure the client is bumped into the `current` map, and return it.
this.current.set(key, client)
return client
}
/**
* Move current clients to previous, create a new map for current.
*
* This function is called every `windowMs`.
*/
private clearExpired(): void {
// At this point, all clients in previous are expired
this.previous = this.current
this.current = new Map()
}
}