Skip to content
This repository was archived by the owner on Apr 15, 2025. It is now read-only.

Commit 291f64f

Browse files
author
williamd5
committed
rate limit class
1 parent 2feefd7 commit 291f64f

File tree

2 files changed

+457
-0
lines changed

2 files changed

+457
-0
lines changed

lib/RateLimit.js

Lines changed: 225 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,225 @@
1+
/**
2+
* Rate limit
3+
* @class
4+
*/
5+
export class RateLimit {
6+
/**
7+
* Rate limit instances
8+
* @private
9+
* @static
10+
*/
11+
static #instances = new Map();
12+
/**
13+
* Whether this rate limit is deleted
14+
* @private
15+
*/
16+
#deleted = false;
17+
/**
18+
* Attempts memory
19+
* @private
20+
*/
21+
#attempts = new Map();
22+
/**
23+
* Name of the rate limit
24+
* @private
25+
*/
26+
#name;
27+
/**
28+
* The number of requests allowed per time window
29+
*/
30+
limit;
31+
/**
32+
* The time window in seconds (e.g. 60)
33+
*/
34+
timeWindow;
35+
/**
36+
* Create a new rate limit
37+
* @param {string} name - The name of the rate limit
38+
* @param {number} limit - The number of requests allowed per time window (e.g. 60)
39+
* @param {number} timeWindow - The time window in seconds (e.g. 60)
40+
* @throws {Error} - If the rate limit already exists
41+
*/
42+
constructor(name, limit, timeWindow) {
43+
if (RateLimit.#instances.has(name))
44+
throw new Error(`Rate limit with name "${name}" already exists`);
45+
this.#name = name;
46+
this.limit = limit;
47+
this.timeWindow = timeWindow;
48+
RateLimit.#instances.set(name, this);
49+
}
50+
/**
51+
* Check the attempt state for a source ID without decrementing the remaining attempts
52+
* @param {string} source - Unique source identifier (e.g. username, IP, etc.)
53+
*/
54+
check(source) {
55+
if (this.#deleted)
56+
throw new Error(`Rate limit "${this.#name}" has been deleted. Construct a new instance`);
57+
const attempts = this.#attempts.get(source) ?? [0, Date.now()];
58+
const remaining = this.limit - attempts[0];
59+
const reset = Math.ceil((attempts[1] + (this.timeWindow * 1000) - Date.now()) / 1000);
60+
return {
61+
limit: this.limit,
62+
remaining,
63+
reset,
64+
rateLimit: this,
65+
allow: remaining > 0
66+
};
67+
}
68+
/**
69+
* Make an attempt with a source ID
70+
* @param {string} source - Unique source identifier (e.g. username, IP, etc.)
71+
* @param {number} [attempts=1] - The number of attempts to make
72+
*/
73+
attempt(source, attempts = 1) {
74+
if (this.#deleted)
75+
throw new Error(`Rate limit "${this.#name}" has been deleted. Construct a new instance`);
76+
const data = this.#attempts.get(source) ?? [0, Date.now()];
77+
// if the time window has expired, reset the attempts
78+
if (data[1] + (this.timeWindow * 1000) < Date.now()) {
79+
data[0] = 0;
80+
data[1] = Date.now();
81+
}
82+
// increment the attempts
83+
data[0] += attempts;
84+
this.#attempts.set(source, data);
85+
return this.check(source);
86+
}
87+
/**
88+
* Reset limit for a source ID. The storage entry will be deleted and a new one will be created on the next attempt.
89+
* @param {string} source - Unique source identifier (e.g. username, IP, etc.)
90+
*/
91+
reset(source) {
92+
if (this.#deleted)
93+
throw new Error(`Rate limit "${this.#name}" has been deleted. Construct a new instance`);
94+
this.#attempts.delete(source);
95+
}
96+
/**
97+
* Set the remaining attempts for a source ID.
98+
* > **Warning**: This is not recommended as the remaining attempts depend on the limit of the instance.
99+
* @param {string} source - Unique source identifier (e.g. username, IP, etc.)
100+
* @param {number} remaining - The number of remaining attempts
101+
*/
102+
setRemaining(source, remaining) {
103+
if (this.#deleted)
104+
throw new Error(`Rate limit "${this.#name}" has been deleted. Construct a new instance`);
105+
const data = this.#attempts.get(source) ?? [0, Date.now()];
106+
data[0] = this.limit - remaining;
107+
this.#attempts.set(source, data);
108+
}
109+
/**
110+
* Clear rate limit attempts storage. This is equivalent to resetting all rate limits.
111+
*/
112+
clear() {
113+
if (this.#deleted)
114+
throw new Error(`Rate limit "${this.#name}" has been deleted. Construct a new instance`);
115+
this.#attempts.clear();
116+
}
117+
/**
118+
* Delete the rate limit instance. After it is deleted, it should not be used any further without constructing a new instance.
119+
*/
120+
delete() {
121+
this.clear();
122+
this.#deleted = true;
123+
RateLimit.#instances.delete(this.#name);
124+
}
125+
/**
126+
* Get a rate limit instance
127+
* @param {string} name - The name of the rate limit
128+
* @static
129+
*/
130+
static get(name) {
131+
return RateLimit.#instances.get(name) ?? null;
132+
}
133+
/**
134+
* Check the attempt state for a source ID without decrementing the remaining attempts
135+
* @param {string} name - The name of the rate limit
136+
* @param {string} source - Unique source identifier (e.g. username, IP, etc.)
137+
* @throws {Error} - If the rate limit does not exist
138+
* @static
139+
*/
140+
static check(name, source) {
141+
const rateLimit = RateLimit.get(name);
142+
if (!rateLimit)
143+
throw new Error(`Rate limit with name "${name}" does not exist`);
144+
return rateLimit.check(source);
145+
}
146+
/**
147+
* Make an attempt with a source ID
148+
* @param {string} name - The name of the rate limit
149+
* @param {string} source - Unique source identifier (e.g. username, IP, etc.)
150+
* @param {number} [attempts=1] - The number of attempts to make
151+
* @throws {Error} - If the rate limit does not exist
152+
* @static
153+
*/
154+
static attempt(name, source, attempts = 1) {
155+
const rateLimit = RateLimit.get(name);
156+
if (!rateLimit)
157+
throw new Error(`Rate limit with name "${name}" does not exist`);
158+
return rateLimit.attempt(source, attempts);
159+
}
160+
/**
161+
* Reset limit for a source ID. The storage entry will be deleted and a new one will be created on the next attempt.
162+
* @param {string} name - The name of the rate limit
163+
* @param {string} source - Unique source identifier (e.g. username, IP, etc.)
164+
* @throws {Error} - If the rate limit does not exist
165+
* @static
166+
*/
167+
static reset(name, source) {
168+
const rateLimit = RateLimit.get(name);
169+
if (!rateLimit)
170+
throw new Error(`Rate limit with name "${name}" does not exist`);
171+
return rateLimit.reset(source);
172+
}
173+
/**
174+
* Set the remaining attempts for a source ID.
175+
* > **Warning**: This is not recommended as the remaining attempts depend on the limit of the instance.
176+
* @param {string} name - The name of the rate limit
177+
* @param {string} source - Unique source identifier (e.g. username, IP, etc.)
178+
* @param {number} remaining - The number of remaining attempts
179+
* @throws {Error} - If the rate limit does not exist
180+
* @static
181+
*/
182+
static setRemaining(name, source, remaining) {
183+
const rateLimit = RateLimit.get(name);
184+
if (!rateLimit)
185+
throw new Error(`Rate limit with name "${name}" does not exist`);
186+
return rateLimit.setRemaining(source, remaining);
187+
}
188+
/**
189+
* Clear rate limit attempts storage. This is equivalent to resetting all rate limits.
190+
* @param {string} name - The name of the rate limit
191+
* @throws {Error} - If the rate limit does not exist
192+
* @static
193+
*/
194+
static clear(name) {
195+
const rateLimit = RateLimit.get(name);
196+
if (!rateLimit)
197+
throw new Error(`Rate limit with name "${name}" does not exist`);
198+
return rateLimit.clear();
199+
}
200+
/**
201+
* Delete the rate limit instance. After it is deleted, it should not be used any further without constructing a new instance.
202+
* @param {string} name - The name of the rate limit
203+
* @throws {Error} - If the rate limit does not exist
204+
* @static
205+
*/
206+
static delete(name) {
207+
const rateLimit = RateLimit.get(name);
208+
if (!rateLimit)
209+
throw new Error(`Rate limit with name "${name}" does not exist`);
210+
return rateLimit.delete();
211+
}
212+
/**
213+
* Create a new rate limit
214+
* @param {string} name - The name of the rate limit
215+
* @param {number} limit - The number of attempts allowed per time window (e.g. 60)
216+
* @param {number} timeWindow - The time window in seconds (e.g. 60)
217+
* @static
218+
*/
219+
static create(name, limit, timeWindow) {
220+
const existing = RateLimit.get(name);
221+
if (existing)
222+
return existing;
223+
return new RateLimit(name, limit, timeWindow);
224+
}
225+
}

0 commit comments

Comments
 (0)