tiny-essentials 1.8.5 โ 1.9.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/TinyEssentials.js +199 -0
- package/dist/TinyEssentials.min.js +1 -1
- package/dist/TinyRateLimiter.js +236 -0
- package/dist/TinyRateLimiter.min.js +1 -0
- package/dist/v1/build/TinyRateLimiter.cjs +7 -0
- package/dist/v1/build/TinyRateLimiter.d.mts +3 -0
- package/dist/v1/build/TinyRateLimiter.mjs +2 -0
- package/dist/v1/index.cjs +2 -0
- package/dist/v1/index.d.mts +2 -1
- package/dist/v1/index.mjs +2 -1
- package/dist/v1/libs/TinyRateLimit.cjs +196 -0
- package/dist/v1/libs/TinyRateLimit.d.mts +86 -0
- package/dist/v1/libs/TinyRateLimit.mjs +171 -0
- package/dist/v1/libs/TinyRateLimiter.cjs +197 -0
- package/dist/v1/libs/TinyRateLimiter.d.mts +86 -0
- package/dist/v1/libs/TinyRateLimiter.mjs +173 -0
- package/docs/README.md +1 -0
- package/docs/libs/TinyRateLimiter.md +156 -0
- package/package.json +2 -1
|
@@ -0,0 +1,197 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* Class representing a flexible rate limiter per user.
|
|
5
|
+
*
|
|
6
|
+
* This rate limiter can be configured by maximum number of hits,
|
|
7
|
+
* time interval, or a combination of both. It supports automatic
|
|
8
|
+
* cleanup of inactive users to optimize memory usage.
|
|
9
|
+
*
|
|
10
|
+
* @class
|
|
11
|
+
*/
|
|
12
|
+
class TinyRateLimiter {
|
|
13
|
+
/**
|
|
14
|
+
* @param {Object} options
|
|
15
|
+
* @param {number} [options.maxHits] - Max interactions allowed
|
|
16
|
+
* @param {number} [options.interval] - Time window in milliseconds
|
|
17
|
+
* @param {number} [options.cleanupInterval] - Interval for automatic cleanup (ms)
|
|
18
|
+
* @param {number} [options.maxIdle=300000] - Max idle time for a user before being cleaned (ms)
|
|
19
|
+
*/
|
|
20
|
+
constructor({ maxHits, interval, cleanupInterval, maxIdle = 300000 }) {
|
|
21
|
+
/** @param {number|undefined} val */
|
|
22
|
+
const isPositiveInteger = (val) =>
|
|
23
|
+
typeof val === 'number' && Number.isFinite(val) && val >= 1 && Number.isInteger(val);
|
|
24
|
+
|
|
25
|
+
const isMaxHitsValid = isPositiveInteger(maxHits);
|
|
26
|
+
const isIntervalValid = isPositiveInteger(interval);
|
|
27
|
+
const isCleanupValid = isPositiveInteger(cleanupInterval);
|
|
28
|
+
const isMaxIdleValid = isPositiveInteger(maxIdle);
|
|
29
|
+
|
|
30
|
+
if (!isMaxHitsValid && !isIntervalValid)
|
|
31
|
+
throw new Error("RateLimiter requires at least one valid option: 'maxHits' or 'interval'.");
|
|
32
|
+
if (maxHits !== undefined && !isMaxHitsValid)
|
|
33
|
+
throw new Error("'maxHits' must be a positive integer if defined.");
|
|
34
|
+
if (interval !== undefined && !isIntervalValid)
|
|
35
|
+
throw new Error("'interval' must be a positive integer in milliseconds if defined.");
|
|
36
|
+
if (cleanupInterval !== undefined && !isCleanupValid)
|
|
37
|
+
throw new Error("'cleanupInterval' must be a positive integer in milliseconds if defined.");
|
|
38
|
+
if (!isMaxIdleValid) throw new Error("'maxIdle' must be a positive integer in milliseconds.");
|
|
39
|
+
|
|
40
|
+
this.maxHits = isMaxHitsValid ? maxHits : null;
|
|
41
|
+
this.interval = isIntervalValid ? interval : null;
|
|
42
|
+
this.cleanupInterval = isCleanupValid ? cleanupInterval : null;
|
|
43
|
+
this.maxIdle = maxIdle;
|
|
44
|
+
|
|
45
|
+
/** @type {Map<string, number[]>} */
|
|
46
|
+
this.userData = new Map();
|
|
47
|
+
|
|
48
|
+
/** @type {Map<string, number>} */
|
|
49
|
+
this.lastSeen = new Map();
|
|
50
|
+
|
|
51
|
+
// Start automatic cleanup only if cleanupInterval is valid
|
|
52
|
+
if (this.cleanupInterval !== null)
|
|
53
|
+
this._cleanupTimer = setInterval(() => this._cleanup(), this.cleanupInterval);
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
/**
|
|
57
|
+
* Get the interval window in milliseconds.
|
|
58
|
+
*
|
|
59
|
+
* @returns {number} The interval value.
|
|
60
|
+
* @throws {Error} If interval is not a valid finite number.
|
|
61
|
+
*/
|
|
62
|
+
getInterval() {
|
|
63
|
+
if (typeof this.interval !== 'number' || !Number.isFinite(this.interval))
|
|
64
|
+
throw new Error("'interval' is not a valid finite number.");
|
|
65
|
+
return this.interval;
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
/**
|
|
69
|
+
* Get the maximum number of allowed hits.
|
|
70
|
+
*
|
|
71
|
+
* @returns {number} The maxHits value.
|
|
72
|
+
* @throws {Error} If maxHits is not a valid finite number.
|
|
73
|
+
*/
|
|
74
|
+
getMaxHits() {
|
|
75
|
+
if (typeof this.maxHits !== 'number' || !Number.isFinite(this.maxHits)) {
|
|
76
|
+
throw new Error("'maxHits' is not a valid finite number.");
|
|
77
|
+
}
|
|
78
|
+
return this.maxHits;
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
/**
|
|
82
|
+
* Register a hit for a specific user
|
|
83
|
+
* @param {string} userId
|
|
84
|
+
*/
|
|
85
|
+
hit(userId) {
|
|
86
|
+
const now = Date.now();
|
|
87
|
+
|
|
88
|
+
if (!this.userData.has(userId)) {
|
|
89
|
+
this.userData.set(userId, []);
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
const history = this.userData.get(userId);
|
|
93
|
+
if (!history) throw new Error(`No data found for userId: ${userId}`);
|
|
94
|
+
|
|
95
|
+
history.push(now);
|
|
96
|
+
this.lastSeen.set(userId, now);
|
|
97
|
+
|
|
98
|
+
// Clean up old entries
|
|
99
|
+
if (this.interval !== null) {
|
|
100
|
+
const interval = this.getInterval();
|
|
101
|
+
const cutoff = now - interval;
|
|
102
|
+
while (history.length && history[0] < cutoff) {
|
|
103
|
+
history.shift();
|
|
104
|
+
}
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
// Optional: keep only the last N entries for memory optimization
|
|
108
|
+
if (this.maxHits !== null) {
|
|
109
|
+
const maxHits = this.getMaxHits();
|
|
110
|
+
if (history.length > maxHits) {
|
|
111
|
+
history.splice(0, history.length - maxHits);
|
|
112
|
+
}
|
|
113
|
+
}
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
/**
|
|
117
|
+
* Check if the user is currently rate limited
|
|
118
|
+
* @param {string} userId
|
|
119
|
+
* @returns {boolean}
|
|
120
|
+
*/
|
|
121
|
+
isRateLimited(userId) {
|
|
122
|
+
const now = Date.now();
|
|
123
|
+
|
|
124
|
+
if (!this.userData.has(userId)) return false;
|
|
125
|
+
|
|
126
|
+
const history = this.userData.get(userId);
|
|
127
|
+
if (!history) throw new Error(`No data found for userId: ${userId}`);
|
|
128
|
+
|
|
129
|
+
if (this.interval !== null) {
|
|
130
|
+
const interval = this.getInterval();
|
|
131
|
+
const recent = history.filter((t) => t > now - interval);
|
|
132
|
+
if (this.maxHits !== null) {
|
|
133
|
+
return recent.length >= this.getMaxHits();
|
|
134
|
+
}
|
|
135
|
+
return recent.length > 0;
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
if (this.maxHits !== null) {
|
|
139
|
+
return history.length >= this.getMaxHits();
|
|
140
|
+
}
|
|
141
|
+
|
|
142
|
+
return false;
|
|
143
|
+
}
|
|
144
|
+
|
|
145
|
+
/**
|
|
146
|
+
* Manually reset user data
|
|
147
|
+
* @param {string} userId
|
|
148
|
+
*/
|
|
149
|
+
reset(userId) {
|
|
150
|
+
this.userData.delete(userId);
|
|
151
|
+
this.lastSeen.delete(userId);
|
|
152
|
+
}
|
|
153
|
+
|
|
154
|
+
/**
|
|
155
|
+
* Set hit timestamps for a user
|
|
156
|
+
* @param {string} userId
|
|
157
|
+
* @param {number[]} timestamps
|
|
158
|
+
*/
|
|
159
|
+
setData(userId, timestamps) {
|
|
160
|
+
this.userData.set(userId, timestamps);
|
|
161
|
+
this.lastSeen.set(userId, Date.now());
|
|
162
|
+
}
|
|
163
|
+
|
|
164
|
+
/**
|
|
165
|
+
* Get timestamps from user
|
|
166
|
+
* @param {string} userId
|
|
167
|
+
* @returns {number[]}
|
|
168
|
+
*/
|
|
169
|
+
getData(userId) {
|
|
170
|
+
return this.userData.get(userId) || [];
|
|
171
|
+
}
|
|
172
|
+
|
|
173
|
+
/**
|
|
174
|
+
* Cleanup old/inactive users
|
|
175
|
+
* @private
|
|
176
|
+
*/
|
|
177
|
+
_cleanup() {
|
|
178
|
+
const now = Date.now();
|
|
179
|
+
for (const [userId, last] of this.lastSeen.entries()) {
|
|
180
|
+
if (now - last > this.maxIdle) {
|
|
181
|
+
this.userData.delete(userId);
|
|
182
|
+
this.lastSeen.delete(userId);
|
|
183
|
+
}
|
|
184
|
+
}
|
|
185
|
+
}
|
|
186
|
+
|
|
187
|
+
/**
|
|
188
|
+
* Destroy the rate limiter, stopping all intervals
|
|
189
|
+
*/
|
|
190
|
+
destroy() {
|
|
191
|
+
if (this._cleanupTimer) clearInterval(this._cleanupTimer);
|
|
192
|
+
this.userData.clear();
|
|
193
|
+
this.lastSeen.clear();
|
|
194
|
+
}
|
|
195
|
+
}
|
|
196
|
+
|
|
197
|
+
module.exports = TinyRateLimiter;
|
|
@@ -0,0 +1,86 @@
|
|
|
1
|
+
export default TinyRateLimiter;
|
|
2
|
+
/**
|
|
3
|
+
* Class representing a flexible rate limiter per user.
|
|
4
|
+
*
|
|
5
|
+
* This rate limiter can be configured by maximum number of hits,
|
|
6
|
+
* time interval, or a combination of both. It supports automatic
|
|
7
|
+
* cleanup of inactive users to optimize memory usage.
|
|
8
|
+
*
|
|
9
|
+
* @class
|
|
10
|
+
*/
|
|
11
|
+
declare class TinyRateLimiter {
|
|
12
|
+
/**
|
|
13
|
+
* @param {Object} options
|
|
14
|
+
* @param {number} [options.maxHits] - Max interactions allowed
|
|
15
|
+
* @param {number} [options.interval] - Time window in milliseconds
|
|
16
|
+
* @param {number} [options.cleanupInterval] - Interval for automatic cleanup (ms)
|
|
17
|
+
* @param {number} [options.maxIdle=300000] - Max idle time for a user before being cleaned (ms)
|
|
18
|
+
*/
|
|
19
|
+
constructor({ maxHits, interval, cleanupInterval, maxIdle }: {
|
|
20
|
+
maxHits?: number | undefined;
|
|
21
|
+
interval?: number | undefined;
|
|
22
|
+
cleanupInterval?: number | undefined;
|
|
23
|
+
maxIdle?: number | undefined;
|
|
24
|
+
});
|
|
25
|
+
maxHits: number | null | undefined;
|
|
26
|
+
interval: number | null | undefined;
|
|
27
|
+
cleanupInterval: number | null | undefined;
|
|
28
|
+
maxIdle: number;
|
|
29
|
+
/** @type {Map<string, number[]>} */
|
|
30
|
+
userData: Map<string, number[]>;
|
|
31
|
+
/** @type {Map<string, number>} */
|
|
32
|
+
lastSeen: Map<string, number>;
|
|
33
|
+
_cleanupTimer: NodeJS.Timeout | undefined;
|
|
34
|
+
/**
|
|
35
|
+
* Get the interval window in milliseconds.
|
|
36
|
+
*
|
|
37
|
+
* @returns {number} The interval value.
|
|
38
|
+
* @throws {Error} If interval is not a valid finite number.
|
|
39
|
+
*/
|
|
40
|
+
getInterval(): number;
|
|
41
|
+
/**
|
|
42
|
+
* Get the maximum number of allowed hits.
|
|
43
|
+
*
|
|
44
|
+
* @returns {number} The maxHits value.
|
|
45
|
+
* @throws {Error} If maxHits is not a valid finite number.
|
|
46
|
+
*/
|
|
47
|
+
getMaxHits(): number;
|
|
48
|
+
/**
|
|
49
|
+
* Register a hit for a specific user
|
|
50
|
+
* @param {string} userId
|
|
51
|
+
*/
|
|
52
|
+
hit(userId: string): void;
|
|
53
|
+
/**
|
|
54
|
+
* Check if the user is currently rate limited
|
|
55
|
+
* @param {string} userId
|
|
56
|
+
* @returns {boolean}
|
|
57
|
+
*/
|
|
58
|
+
isRateLimited(userId: string): boolean;
|
|
59
|
+
/**
|
|
60
|
+
* Manually reset user data
|
|
61
|
+
* @param {string} userId
|
|
62
|
+
*/
|
|
63
|
+
reset(userId: string): void;
|
|
64
|
+
/**
|
|
65
|
+
* Set hit timestamps for a user
|
|
66
|
+
* @param {string} userId
|
|
67
|
+
* @param {number[]} timestamps
|
|
68
|
+
*/
|
|
69
|
+
setData(userId: string, timestamps: number[]): void;
|
|
70
|
+
/**
|
|
71
|
+
* Get timestamps from user
|
|
72
|
+
* @param {string} userId
|
|
73
|
+
* @returns {number[]}
|
|
74
|
+
*/
|
|
75
|
+
getData(userId: string): number[];
|
|
76
|
+
/**
|
|
77
|
+
* Cleanup old/inactive users
|
|
78
|
+
* @private
|
|
79
|
+
*/
|
|
80
|
+
private _cleanup;
|
|
81
|
+
/**
|
|
82
|
+
* Destroy the rate limiter, stopping all intervals
|
|
83
|
+
*/
|
|
84
|
+
destroy(): void;
|
|
85
|
+
}
|
|
86
|
+
//# sourceMappingURL=TinyRateLimiter.d.mts.map
|
|
@@ -0,0 +1,173 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Class representing a flexible rate limiter per user.
|
|
3
|
+
*
|
|
4
|
+
* This rate limiter can be configured by maximum number of hits,
|
|
5
|
+
* time interval, or a combination of both. It supports automatic
|
|
6
|
+
* cleanup of inactive users to optimize memory usage.
|
|
7
|
+
*
|
|
8
|
+
* @class
|
|
9
|
+
*/
|
|
10
|
+
class TinyRateLimiter {
|
|
11
|
+
/**
|
|
12
|
+
* @param {Object} options
|
|
13
|
+
* @param {number} [options.maxHits] - Max interactions allowed
|
|
14
|
+
* @param {number} [options.interval] - Time window in milliseconds
|
|
15
|
+
* @param {number} [options.cleanupInterval] - Interval for automatic cleanup (ms)
|
|
16
|
+
* @param {number} [options.maxIdle=300000] - Max idle time for a user before being cleaned (ms)
|
|
17
|
+
*/
|
|
18
|
+
constructor({ maxHits, interval, cleanupInterval, maxIdle = 300000 }) {
|
|
19
|
+
/** @param {number|undefined} val */
|
|
20
|
+
const isPositiveInteger = (val) => typeof val === 'number' && Number.isFinite(val) && val >= 1 && Number.isInteger(val);
|
|
21
|
+
const isMaxHitsValid = isPositiveInteger(maxHits);
|
|
22
|
+
const isIntervalValid = isPositiveInteger(interval);
|
|
23
|
+
const isCleanupValid = isPositiveInteger(cleanupInterval);
|
|
24
|
+
const isMaxIdleValid = isPositiveInteger(maxIdle);
|
|
25
|
+
if (!isMaxHitsValid && !isIntervalValid)
|
|
26
|
+
throw new Error("RateLimiter requires at least one valid option: 'maxHits' or 'interval'.");
|
|
27
|
+
if (maxHits !== undefined && !isMaxHitsValid)
|
|
28
|
+
throw new Error("'maxHits' must be a positive integer if defined.");
|
|
29
|
+
if (interval !== undefined && !isIntervalValid)
|
|
30
|
+
throw new Error("'interval' must be a positive integer in milliseconds if defined.");
|
|
31
|
+
if (cleanupInterval !== undefined && !isCleanupValid)
|
|
32
|
+
throw new Error("'cleanupInterval' must be a positive integer in milliseconds if defined.");
|
|
33
|
+
if (!isMaxIdleValid)
|
|
34
|
+
throw new Error("'maxIdle' must be a positive integer in milliseconds.");
|
|
35
|
+
this.maxHits = isMaxHitsValid ? maxHits : null;
|
|
36
|
+
this.interval = isIntervalValid ? interval : null;
|
|
37
|
+
this.cleanupInterval = isCleanupValid ? cleanupInterval : null;
|
|
38
|
+
this.maxIdle = maxIdle;
|
|
39
|
+
/** @type {Map<string, number[]>} */
|
|
40
|
+
this.userData = new Map();
|
|
41
|
+
/** @type {Map<string, number>} */
|
|
42
|
+
this.lastSeen = new Map();
|
|
43
|
+
// Start automatic cleanup only if cleanupInterval is valid
|
|
44
|
+
if (this.cleanupInterval !== null)
|
|
45
|
+
this._cleanupTimer = setInterval(() => this._cleanup(), this.cleanupInterval);
|
|
46
|
+
}
|
|
47
|
+
/**
|
|
48
|
+
* Get the interval window in milliseconds.
|
|
49
|
+
*
|
|
50
|
+
* @returns {number} The interval value.
|
|
51
|
+
* @throws {Error} If interval is not a valid finite number.
|
|
52
|
+
*/
|
|
53
|
+
getInterval() {
|
|
54
|
+
if (typeof this.interval !== 'number' || !Number.isFinite(this.interval))
|
|
55
|
+
throw new Error("'interval' is not a valid finite number.");
|
|
56
|
+
return this.interval;
|
|
57
|
+
}
|
|
58
|
+
/**
|
|
59
|
+
* Get the maximum number of allowed hits.
|
|
60
|
+
*
|
|
61
|
+
* @returns {number} The maxHits value.
|
|
62
|
+
* @throws {Error} If maxHits is not a valid finite number.
|
|
63
|
+
*/
|
|
64
|
+
getMaxHits() {
|
|
65
|
+
if (typeof this.maxHits !== 'number' || !Number.isFinite(this.maxHits)) {
|
|
66
|
+
throw new Error("'maxHits' is not a valid finite number.");
|
|
67
|
+
}
|
|
68
|
+
return this.maxHits;
|
|
69
|
+
}
|
|
70
|
+
/**
|
|
71
|
+
* Register a hit for a specific user
|
|
72
|
+
* @param {string} userId
|
|
73
|
+
*/
|
|
74
|
+
hit(userId) {
|
|
75
|
+
const now = Date.now();
|
|
76
|
+
if (!this.userData.has(userId)) {
|
|
77
|
+
this.userData.set(userId, []);
|
|
78
|
+
}
|
|
79
|
+
const history = this.userData.get(userId);
|
|
80
|
+
if (!history)
|
|
81
|
+
throw new Error(`No data found for userId: ${userId}`);
|
|
82
|
+
history.push(now);
|
|
83
|
+
this.lastSeen.set(userId, now);
|
|
84
|
+
// Clean up old entries
|
|
85
|
+
if (this.interval !== null) {
|
|
86
|
+
const interval = this.getInterval();
|
|
87
|
+
const cutoff = now - interval;
|
|
88
|
+
while (history.length && history[0] < cutoff) {
|
|
89
|
+
history.shift();
|
|
90
|
+
}
|
|
91
|
+
}
|
|
92
|
+
// Optional: keep only the last N entries for memory optimization
|
|
93
|
+
if (this.maxHits !== null) {
|
|
94
|
+
const maxHits = this.getMaxHits();
|
|
95
|
+
if (history.length > maxHits) {
|
|
96
|
+
history.splice(0, history.length - maxHits);
|
|
97
|
+
}
|
|
98
|
+
}
|
|
99
|
+
}
|
|
100
|
+
/**
|
|
101
|
+
* Check if the user is currently rate limited
|
|
102
|
+
* @param {string} userId
|
|
103
|
+
* @returns {boolean}
|
|
104
|
+
*/
|
|
105
|
+
isRateLimited(userId) {
|
|
106
|
+
const now = Date.now();
|
|
107
|
+
if (!this.userData.has(userId))
|
|
108
|
+
return false;
|
|
109
|
+
const history = this.userData.get(userId);
|
|
110
|
+
if (!history)
|
|
111
|
+
throw new Error(`No data found for userId: ${userId}`);
|
|
112
|
+
if (this.interval !== null) {
|
|
113
|
+
const interval = this.getInterval();
|
|
114
|
+
const recent = history.filter((t) => t > now - interval);
|
|
115
|
+
if (this.maxHits !== null) {
|
|
116
|
+
return recent.length >= this.getMaxHits();
|
|
117
|
+
}
|
|
118
|
+
return recent.length > 0;
|
|
119
|
+
}
|
|
120
|
+
if (this.maxHits !== null) {
|
|
121
|
+
return history.length >= this.getMaxHits();
|
|
122
|
+
}
|
|
123
|
+
return false;
|
|
124
|
+
}
|
|
125
|
+
/**
|
|
126
|
+
* Manually reset user data
|
|
127
|
+
* @param {string} userId
|
|
128
|
+
*/
|
|
129
|
+
reset(userId) {
|
|
130
|
+
this.userData.delete(userId);
|
|
131
|
+
this.lastSeen.delete(userId);
|
|
132
|
+
}
|
|
133
|
+
/**
|
|
134
|
+
* Set hit timestamps for a user
|
|
135
|
+
* @param {string} userId
|
|
136
|
+
* @param {number[]} timestamps
|
|
137
|
+
*/
|
|
138
|
+
setData(userId, timestamps) {
|
|
139
|
+
this.userData.set(userId, timestamps);
|
|
140
|
+
this.lastSeen.set(userId, Date.now());
|
|
141
|
+
}
|
|
142
|
+
/**
|
|
143
|
+
* Get timestamps from user
|
|
144
|
+
* @param {string} userId
|
|
145
|
+
* @returns {number[]}
|
|
146
|
+
*/
|
|
147
|
+
getData(userId) {
|
|
148
|
+
return this.userData.get(userId) || [];
|
|
149
|
+
}
|
|
150
|
+
/**
|
|
151
|
+
* Cleanup old/inactive users
|
|
152
|
+
* @private
|
|
153
|
+
*/
|
|
154
|
+
_cleanup() {
|
|
155
|
+
const now = Date.now();
|
|
156
|
+
for (const [userId, last] of this.lastSeen.entries()) {
|
|
157
|
+
if (now - last > this.maxIdle) {
|
|
158
|
+
this.userData.delete(userId);
|
|
159
|
+
this.lastSeen.delete(userId);
|
|
160
|
+
}
|
|
161
|
+
}
|
|
162
|
+
}
|
|
163
|
+
/**
|
|
164
|
+
* Destroy the rate limiter, stopping all intervals
|
|
165
|
+
*/
|
|
166
|
+
destroy() {
|
|
167
|
+
if (this._cleanupTimer)
|
|
168
|
+
clearInterval(this._cleanupTimer);
|
|
169
|
+
this.userData.clear();
|
|
170
|
+
this.lastSeen.clear();
|
|
171
|
+
}
|
|
172
|
+
}
|
|
173
|
+
export default TinyRateLimiter;
|
package/docs/README.md
CHANGED
|
@@ -23,6 +23,7 @@ This folder contains the core scripts we have worked on so far. Each file is a m
|
|
|
23
23
|
- ๐๏ธ **[TinyPromiseQueue](./libs/TinyPromiseQueue.md)** โ A class that allows sequential execution of asynchronous tasks, supporting task delays, cancellation, and queue management.
|
|
24
24
|
- ๐
**[TinyLevelUp](./libs/TinyLevelUp.md)** โ A class to manage user level-up logic based on experience points, providing methods for experience validation, addition, removal, and calculation.
|
|
25
25
|
- ๐จ **[ColorSafeStringify](./libs/ColorSafeStringify.md)** โ A utility for applying customizable ANSI colors to JSON strings in terminal outputs, supporting presets and fine-grained type-based highlighting.
|
|
26
|
+
- ๐ฆ **[TinyRateLimiter](./libs/TinyRateLimiter.md)** โ A flexible per-user rate limiter supporting time windows, hit caps, and automatic cleanup of inactive users.
|
|
26
27
|
|
|
27
28
|
---
|
|
28
29
|
|
|
@@ -0,0 +1,156 @@
|
|
|
1
|
+
# ๐ง `TinyRateLimiter` โ A Simple Per-User Rate Limiter
|
|
2
|
+
|
|
3
|
+
The `TinyRateLimiter` is a flexible and lightweight JavaScript class designed to help you limit actions (like requests or commands) per user. You can define rules like **maximum hits**, **time windows**, and benefit from automatic memory cleanup. โจ
|
|
4
|
+
|
|
5
|
+
---
|
|
6
|
+
|
|
7
|
+
## ๐ฆ Constructor
|
|
8
|
+
|
|
9
|
+
```js
|
|
10
|
+
new TinyRateLimiter(options)
|
|
11
|
+
```
|
|
12
|
+
|
|
13
|
+
### ๐ง Options
|
|
14
|
+
|
|
15
|
+
| Option | Type | Default | Description |
|
|
16
|
+
| ----------------- | -------- | ----------- | ---------------------------------------------- |
|
|
17
|
+
| `maxHits` | `number` | `undefined` | Max number of hits per user before blocking ๐ง |
|
|
18
|
+
| `interval` | `number` | `undefined` | Time window in milliseconds โฑ๏ธ |
|
|
19
|
+
| `cleanupInterval` | `number` | `undefined` | Interval to auto-clean inactive users ๐งน |
|
|
20
|
+
| `maxIdle` | `number` | `300000` | Max idle time per user before cleanup ๐ค |
|
|
21
|
+
|
|
22
|
+
> โ ๏ธ At least one of `maxHits` or `interval` must be defined.
|
|
23
|
+
|
|
24
|
+
---
|
|
25
|
+
|
|
26
|
+
## ๐ Methods
|
|
27
|
+
|
|
28
|
+
### ๐ `hit(userId: string): void`
|
|
29
|
+
|
|
30
|
+
Registers a hit for the given `userId`.
|
|
31
|
+
|
|
32
|
+
```js
|
|
33
|
+
rateLimiter.hit("user123");
|
|
34
|
+
```
|
|
35
|
+
|
|
36
|
+
It tracks timestamps internally and automatically cleans old entries based on `interval` and `maxHits`.
|
|
37
|
+
|
|
38
|
+
---
|
|
39
|
+
|
|
40
|
+
### โ `isRateLimited(userId: string): boolean`
|
|
41
|
+
|
|
42
|
+
Checks if the given `userId` is currently rate limited.
|
|
43
|
+
|
|
44
|
+
```js
|
|
45
|
+
if (rateLimiter.isRateLimited("user123")) {
|
|
46
|
+
console.log("Too many actions! Please slow down.");
|
|
47
|
+
}
|
|
48
|
+
```
|
|
49
|
+
|
|
50
|
+
---
|
|
51
|
+
|
|
52
|
+
### ๐ `reset(userId: string): void`
|
|
53
|
+
|
|
54
|
+
Resets all stored data for the given user.
|
|
55
|
+
|
|
56
|
+
```js
|
|
57
|
+
rateLimiter.reset("user123");
|
|
58
|
+
```
|
|
59
|
+
|
|
60
|
+
---
|
|
61
|
+
|
|
62
|
+
### ๐ ๏ธ `setData(userId: string, timestamps: number[]): void`
|
|
63
|
+
|
|
64
|
+
Sets the hit timestamps for a specific user. Useful for restoring or mocking state.
|
|
65
|
+
|
|
66
|
+
```js
|
|
67
|
+
rateLimiter.setData("user123", [Date.now() - 1000, Date.now()]);
|
|
68
|
+
```
|
|
69
|
+
|
|
70
|
+
---
|
|
71
|
+
|
|
72
|
+
### ๐ฅ `getData(userId: string): number[]`
|
|
73
|
+
|
|
74
|
+
Returns all hit timestamps for a given user.
|
|
75
|
+
|
|
76
|
+
```js
|
|
77
|
+
const hits = rateLimiter.getData("user123");
|
|
78
|
+
console.log(hits); // [timestamp1, timestamp2, ...]
|
|
79
|
+
```
|
|
80
|
+
|
|
81
|
+
---
|
|
82
|
+
|
|
83
|
+
### ๐ง `getMaxHits(): number`
|
|
84
|
+
|
|
85
|
+
Returns the configured `maxHits` value, or throws if invalid.
|
|
86
|
+
|
|
87
|
+
---
|
|
88
|
+
|
|
89
|
+
### โณ `getInterval(): number`
|
|
90
|
+
|
|
91
|
+
Returns the configured `interval` value, or throws if invalid.
|
|
92
|
+
|
|
93
|
+
---
|
|
94
|
+
|
|
95
|
+
### ๐งผ `destroy(): void`
|
|
96
|
+
|
|
97
|
+
Stops internal timers and clears all stored data. Ideal for cleanup in tests or long-running apps.
|
|
98
|
+
|
|
99
|
+
```js
|
|
100
|
+
rateLimiter.destroy();
|
|
101
|
+
```
|
|
102
|
+
|
|
103
|
+
---
|
|
104
|
+
|
|
105
|
+
## ๐งฝ Automatic Cleanup
|
|
106
|
+
|
|
107
|
+
Inactive users are automatically removed every `cleanupInterval` milliseconds if they haven't had any hits for longer than `maxIdle`.
|
|
108
|
+
|
|
109
|
+
This helps reduce memory usage and keeps your limiter lean and clean! ๐ชถ
|
|
110
|
+
|
|
111
|
+
---
|
|
112
|
+
|
|
113
|
+
## ๐ฅ Errors
|
|
114
|
+
|
|
115
|
+
The constructor will throw if:
|
|
116
|
+
|
|
117
|
+
* Neither `maxHits` nor `interval` are provided.
|
|
118
|
+
* Any of the values are not positive integers.
|
|
119
|
+
|
|
120
|
+
---
|
|
121
|
+
|
|
122
|
+
## ๐งช Example Usage
|
|
123
|
+
|
|
124
|
+
```js
|
|
125
|
+
const limiter = new TinyRateLimiter({
|
|
126
|
+
maxHits: 5,
|
|
127
|
+
interval: 10000,
|
|
128
|
+
cleanupInterval: 60000,
|
|
129
|
+
maxIdle: 120000
|
|
130
|
+
});
|
|
131
|
+
|
|
132
|
+
limiter.hit("user42");
|
|
133
|
+
|
|
134
|
+
if (limiter.isRateLimited("user42")) {
|
|
135
|
+
console.log("Rate limited! ๐");
|
|
136
|
+
} else {
|
|
137
|
+
console.log("Action allowed โ
");
|
|
138
|
+
}
|
|
139
|
+
```
|
|
140
|
+
|
|
141
|
+
---
|
|
142
|
+
|
|
143
|
+
## ๐ Summary
|
|
144
|
+
|
|
145
|
+
| Feature | Support |
|
|
146
|
+
| -------------------- | ------- |
|
|
147
|
+
| Per-user tracking | โ
|
|
|
148
|
+
| Max hits | โ
|
|
|
149
|
+
| Time window interval | โ
|
|
|
150
|
+
| Automatic cleanup | โ
|
|
|
151
|
+
| Manual data control | โ
|
|
|
152
|
+
| Lightweight + fast | โ
|
|
|
153
|
+
|
|
154
|
+
---
|
|
155
|
+
|
|
156
|
+
Made with ๐ to protect your app from spammers and abusers โ in a cute and efficient way! ๐
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "tiny-essentials",
|
|
3
|
-
"version": "1.
|
|
3
|
+
"version": "1.9.0",
|
|
4
4
|
"description": "Collection of small, essential scripts designed to be used across various projects. These simple utilities are crafted for speed, ease of use, and versatility.",
|
|
5
5
|
"scripts": {
|
|
6
6
|
"test": "npm run test:mjs && npm run test:cjs && npm run test:js",
|
|
@@ -10,6 +10,7 @@
|
|
|
10
10
|
"test:mjs:promisequeue": "node test/index.mjs promiseQueue",
|
|
11
11
|
"test:mjs:objtype": "node test/index.mjs objType",
|
|
12
12
|
"test:mjs:jsoncolor": "node test/index.mjs colorStringify",
|
|
13
|
+
"test:mjs:ratelimit": "node test/index.mjs rateLimit",
|
|
13
14
|
"fix:prettier": "npm run fix:prettier:src && npm run fix:prettier:test && npm run fix:prettier:rollup.config && npm run fix:prettier:webpack.config",
|
|
14
15
|
"fix:prettier:src": "prettier --write ./src/*",
|
|
15
16
|
"fix:prettier:test": "prettier --write ./test/*",
|