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
package/dist/v1/index.cjs
CHANGED
|
@@ -10,6 +10,7 @@ var simpleMath = require('./basics/simpleMath.cjs');
|
|
|
10
10
|
var text = require('./basics/text.cjs');
|
|
11
11
|
var ColorSafeStringify = require('./libs/ColorSafeStringify.cjs');
|
|
12
12
|
var TinyPromiseQueue = require('./libs/TinyPromiseQueue.cjs');
|
|
13
|
+
var TinyRateLimiter = require('./libs/TinyRateLimiter.cjs');
|
|
13
14
|
|
|
14
15
|
|
|
15
16
|
|
|
@@ -36,3 +37,4 @@ exports.toTitleCase = text.toTitleCase;
|
|
|
36
37
|
exports.toTitleCaseLowerFirst = text.toTitleCaseLowerFirst;
|
|
37
38
|
exports.ColorSafeStringify = ColorSafeStringify;
|
|
38
39
|
exports.TinyPromiseQueue = TinyPromiseQueue;
|
|
40
|
+
exports.TinyRateLimiter = TinyRateLimiter;
|
package/dist/v1/index.d.mts
CHANGED
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import TinyRateLimiter from './libs/TinyRateLimiter.mjs';
|
|
1
2
|
import ColorSafeStringify from './libs/ColorSafeStringify.mjs';
|
|
2
3
|
import TinyPromiseQueue from './libs/TinyPromiseQueue.mjs';
|
|
3
4
|
import TinyLevelUp from '../legacy/libs/userLevel.mjs';
|
|
@@ -21,5 +22,5 @@ import { getTimeDuration } from './basics/clock.mjs';
|
|
|
21
22
|
import { shuffleArray } from './basics/array.mjs';
|
|
22
23
|
import { toTitleCase } from './basics/text.mjs';
|
|
23
24
|
import { toTitleCaseLowerFirst } from './basics/text.mjs';
|
|
24
|
-
export { ColorSafeStringify, TinyPromiseQueue, TinyLevelUp, arraySortPositions, formatBytes, addAiMarkerShortcut, extendObjType, reorderObjTypeOrder, cloneObjTypeOrder, countObj, checkObj, objType, ruleOfThree, getSimplePerc, asyncReplace, getAge, formatCustomTimer, formatDayTimer, formatTimer, getTimeDuration, shuffleArray, toTitleCase, toTitleCaseLowerFirst };
|
|
25
|
+
export { TinyRateLimiter, ColorSafeStringify, TinyPromiseQueue, TinyLevelUp, arraySortPositions, formatBytes, addAiMarkerShortcut, extendObjType, reorderObjTypeOrder, cloneObjTypeOrder, countObj, checkObj, objType, ruleOfThree, getSimplePerc, asyncReplace, getAge, formatCustomTimer, formatDayTimer, formatTimer, getTimeDuration, shuffleArray, toTitleCase, toTitleCaseLowerFirst };
|
|
25
26
|
//# sourceMappingURL=index.d.mts.map
|
package/dist/v1/index.mjs
CHANGED
|
@@ -8,4 +8,5 @@ import { formatBytes, getAge, getSimplePerc, ruleOfThree } from './basics/simple
|
|
|
8
8
|
import { addAiMarkerShortcut, toTitleCase, toTitleCaseLowerFirst } from './basics/text.mjs';
|
|
9
9
|
import ColorSafeStringify from './libs/ColorSafeStringify.mjs';
|
|
10
10
|
import TinyPromiseQueue from './libs/TinyPromiseQueue.mjs';
|
|
11
|
-
|
|
11
|
+
import TinyRateLimiter from './libs/TinyRateLimiter.mjs';
|
|
12
|
+
export { TinyRateLimiter, ColorSafeStringify, TinyPromiseQueue, TinyLevelUp, arraySortPositions, formatBytes, addAiMarkerShortcut, extendObjType, reorderObjTypeOrder, cloneObjTypeOrder, countObj, checkObj, objType, ruleOfThree, getSimplePerc, asyncReplace, getAge, formatCustomTimer, formatDayTimer, formatTimer, getTimeDuration, shuffleArray, toTitleCase, toTitleCaseLowerFirst, };
|
|
@@ -0,0 +1,196 @@
|
|
|
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=60000] - 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 = 60000, 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 (!isCleanupValid)
|
|
37
|
+
throw new Error("'cleanupInterval' must be a positive integer in milliseconds.");
|
|
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 = cleanupInterval;
|
|
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
|
|
52
|
+
this._cleanupTimer = setInterval(() => this._cleanup(), this.cleanupInterval);
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
/**
|
|
56
|
+
* Get the interval window in milliseconds.
|
|
57
|
+
*
|
|
58
|
+
* @returns {number} The interval value.
|
|
59
|
+
* @throws {Error} If interval is not a valid finite number.
|
|
60
|
+
*/
|
|
61
|
+
getInterval() {
|
|
62
|
+
if (typeof this.interval !== 'number' || !Number.isFinite(this.interval))
|
|
63
|
+
throw new Error("'interval' is not a valid finite number.");
|
|
64
|
+
return this.interval;
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
/**
|
|
68
|
+
* Get the maximum number of allowed hits.
|
|
69
|
+
*
|
|
70
|
+
* @returns {number} The maxHits value.
|
|
71
|
+
* @throws {Error} If maxHits is not a valid finite number.
|
|
72
|
+
*/
|
|
73
|
+
getMaxHits() {
|
|
74
|
+
if (typeof this.maxHits !== 'number' || !Number.isFinite(this.maxHits)) {
|
|
75
|
+
throw new Error("'maxHits' is not a valid finite number.");
|
|
76
|
+
}
|
|
77
|
+
return this.maxHits;
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
/**
|
|
81
|
+
* Register a hit for a specific user
|
|
82
|
+
* @param {string} userId
|
|
83
|
+
*/
|
|
84
|
+
hit(userId) {
|
|
85
|
+
const now = Date.now();
|
|
86
|
+
|
|
87
|
+
if (!this.userData.has(userId)) {
|
|
88
|
+
this.userData.set(userId, []);
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
const history = this.userData.get(userId);
|
|
92
|
+
if (!history) throw new Error(`No data found for userId: ${userId}`);
|
|
93
|
+
|
|
94
|
+
history.push(now);
|
|
95
|
+
this.lastSeen.set(userId, now);
|
|
96
|
+
|
|
97
|
+
// Clean up old entries
|
|
98
|
+
if (this.interval !== null) {
|
|
99
|
+
const interval = this.getInterval();
|
|
100
|
+
const cutoff = now - interval;
|
|
101
|
+
while (history.length && history[0] < cutoff) {
|
|
102
|
+
history.shift();
|
|
103
|
+
}
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
// Optional: keep only the last N entries for memory optimization
|
|
107
|
+
if (this.maxHits !== null) {
|
|
108
|
+
const maxHits = this.getMaxHits();
|
|
109
|
+
if (history.length > maxHits) {
|
|
110
|
+
history.splice(0, history.length - maxHits);
|
|
111
|
+
}
|
|
112
|
+
}
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
/**
|
|
116
|
+
* Check if the user is currently rate limited
|
|
117
|
+
* @param {string} userId
|
|
118
|
+
* @returns {boolean}
|
|
119
|
+
*/
|
|
120
|
+
isRateLimited(userId) {
|
|
121
|
+
const now = Date.now();
|
|
122
|
+
|
|
123
|
+
if (!this.userData.has(userId)) return false;
|
|
124
|
+
|
|
125
|
+
const history = this.userData.get(userId);
|
|
126
|
+
if (!history) throw new Error(`No data found for userId: ${userId}`);
|
|
127
|
+
|
|
128
|
+
if (this.interval !== null) {
|
|
129
|
+
const interval = this.getInterval();
|
|
130
|
+
const recent = history.filter((t) => t > now - interval);
|
|
131
|
+
if (this.maxHits !== null) {
|
|
132
|
+
return recent.length >= this.getMaxHits();
|
|
133
|
+
}
|
|
134
|
+
return recent.length > 0;
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
if (this.maxHits !== null) {
|
|
138
|
+
return history.length >= this.getMaxHits();
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
return false;
|
|
142
|
+
}
|
|
143
|
+
|
|
144
|
+
/**
|
|
145
|
+
* Manually reset user data
|
|
146
|
+
* @param {string} userId
|
|
147
|
+
*/
|
|
148
|
+
reset(userId) {
|
|
149
|
+
this.userData.delete(userId);
|
|
150
|
+
this.lastSeen.delete(userId);
|
|
151
|
+
}
|
|
152
|
+
|
|
153
|
+
/**
|
|
154
|
+
* Set hit timestamps for a user
|
|
155
|
+
* @param {string} userId
|
|
156
|
+
* @param {number[]} timestamps
|
|
157
|
+
*/
|
|
158
|
+
setData(userId, timestamps) {
|
|
159
|
+
this.userData.set(userId, timestamps);
|
|
160
|
+
this.lastSeen.set(userId, Date.now());
|
|
161
|
+
}
|
|
162
|
+
|
|
163
|
+
/**
|
|
164
|
+
* Get timestamps from user
|
|
165
|
+
* @param {string} userId
|
|
166
|
+
* @returns {number[]}
|
|
167
|
+
*/
|
|
168
|
+
getData(userId) {
|
|
169
|
+
return this.userData.get(userId) || [];
|
|
170
|
+
}
|
|
171
|
+
|
|
172
|
+
/**
|
|
173
|
+
* Cleanup old/inactive users
|
|
174
|
+
* @private
|
|
175
|
+
*/
|
|
176
|
+
_cleanup() {
|
|
177
|
+
const now = Date.now();
|
|
178
|
+
for (const [userId, last] of this.lastSeen.entries()) {
|
|
179
|
+
if (now - last > this.maxIdle) {
|
|
180
|
+
this.userData.delete(userId);
|
|
181
|
+
this.lastSeen.delete(userId);
|
|
182
|
+
}
|
|
183
|
+
}
|
|
184
|
+
}
|
|
185
|
+
|
|
186
|
+
/**
|
|
187
|
+
* Destroy the rate limiter, stopping all intervals
|
|
188
|
+
*/
|
|
189
|
+
destroy() {
|
|
190
|
+
clearInterval(this._cleanupTimer);
|
|
191
|
+
this.userData.clear();
|
|
192
|
+
this.lastSeen.clear();
|
|
193
|
+
}
|
|
194
|
+
}
|
|
195
|
+
|
|
196
|
+
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=60000] - 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;
|
|
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;
|
|
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=TinyRateLimit.d.mts.map
|
|
@@ -0,0 +1,171 @@
|
|
|
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=60000] - 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 = 60000, 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 (!isCleanupValid)
|
|
32
|
+
throw new Error("'cleanupInterval' must be a positive integer in milliseconds.");
|
|
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 = cleanupInterval;
|
|
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
|
|
44
|
+
this._cleanupTimer = setInterval(() => this._cleanup(), this.cleanupInterval);
|
|
45
|
+
}
|
|
46
|
+
/**
|
|
47
|
+
* Get the interval window in milliseconds.
|
|
48
|
+
*
|
|
49
|
+
* @returns {number} The interval value.
|
|
50
|
+
* @throws {Error} If interval is not a valid finite number.
|
|
51
|
+
*/
|
|
52
|
+
getInterval() {
|
|
53
|
+
if (typeof this.interval !== 'number' || !Number.isFinite(this.interval))
|
|
54
|
+
throw new Error("'interval' is not a valid finite number.");
|
|
55
|
+
return this.interval;
|
|
56
|
+
}
|
|
57
|
+
/**
|
|
58
|
+
* Get the maximum number of allowed hits.
|
|
59
|
+
*
|
|
60
|
+
* @returns {number} The maxHits value.
|
|
61
|
+
* @throws {Error} If maxHits is not a valid finite number.
|
|
62
|
+
*/
|
|
63
|
+
getMaxHits() {
|
|
64
|
+
if (typeof this.maxHits !== 'number' || !Number.isFinite(this.maxHits)) {
|
|
65
|
+
throw new Error("'maxHits' is not a valid finite number.");
|
|
66
|
+
}
|
|
67
|
+
return this.maxHits;
|
|
68
|
+
}
|
|
69
|
+
/**
|
|
70
|
+
* Register a hit for a specific user
|
|
71
|
+
* @param {string} userId
|
|
72
|
+
*/
|
|
73
|
+
hit(userId) {
|
|
74
|
+
const now = Date.now();
|
|
75
|
+
if (!this.userData.has(userId)) {
|
|
76
|
+
this.userData.set(userId, []);
|
|
77
|
+
}
|
|
78
|
+
const history = this.userData.get(userId);
|
|
79
|
+
if (!history)
|
|
80
|
+
throw new Error(`No data found for userId: ${userId}`);
|
|
81
|
+
history.push(now);
|
|
82
|
+
this.lastSeen.set(userId, now);
|
|
83
|
+
// Clean up old entries
|
|
84
|
+
if (this.interval !== null) {
|
|
85
|
+
const interval = this.getInterval();
|
|
86
|
+
const cutoff = now - interval;
|
|
87
|
+
while (history.length && history[0] < cutoff) {
|
|
88
|
+
history.shift();
|
|
89
|
+
}
|
|
90
|
+
}
|
|
91
|
+
// Optional: keep only the last N entries for memory optimization
|
|
92
|
+
if (this.maxHits !== null) {
|
|
93
|
+
const maxHits = this.getMaxHits();
|
|
94
|
+
if (history.length > maxHits) {
|
|
95
|
+
history.splice(0, history.length - maxHits);
|
|
96
|
+
}
|
|
97
|
+
}
|
|
98
|
+
}
|
|
99
|
+
/**
|
|
100
|
+
* Check if the user is currently rate limited
|
|
101
|
+
* @param {string} userId
|
|
102
|
+
* @returns {boolean}
|
|
103
|
+
*/
|
|
104
|
+
isRateLimited(userId) {
|
|
105
|
+
const now = Date.now();
|
|
106
|
+
if (!this.userData.has(userId))
|
|
107
|
+
return false;
|
|
108
|
+
const history = this.userData.get(userId);
|
|
109
|
+
if (!history)
|
|
110
|
+
throw new Error(`No data found for userId: ${userId}`);
|
|
111
|
+
if (this.interval !== null) {
|
|
112
|
+
const interval = this.getInterval();
|
|
113
|
+
const recent = history.filter((t) => t > now - interval);
|
|
114
|
+
if (this.maxHits !== null) {
|
|
115
|
+
return recent.length >= this.getMaxHits();
|
|
116
|
+
}
|
|
117
|
+
return recent.length > 0;
|
|
118
|
+
}
|
|
119
|
+
if (this.maxHits !== null) {
|
|
120
|
+
return history.length >= this.getMaxHits();
|
|
121
|
+
}
|
|
122
|
+
return false;
|
|
123
|
+
}
|
|
124
|
+
/**
|
|
125
|
+
* Manually reset user data
|
|
126
|
+
* @param {string} userId
|
|
127
|
+
*/
|
|
128
|
+
reset(userId) {
|
|
129
|
+
this.userData.delete(userId);
|
|
130
|
+
this.lastSeen.delete(userId);
|
|
131
|
+
}
|
|
132
|
+
/**
|
|
133
|
+
* Set hit timestamps for a user
|
|
134
|
+
* @param {string} userId
|
|
135
|
+
* @param {number[]} timestamps
|
|
136
|
+
*/
|
|
137
|
+
setData(userId, timestamps) {
|
|
138
|
+
this.userData.set(userId, timestamps);
|
|
139
|
+
this.lastSeen.set(userId, Date.now());
|
|
140
|
+
}
|
|
141
|
+
/**
|
|
142
|
+
* Get timestamps from user
|
|
143
|
+
* @param {string} userId
|
|
144
|
+
* @returns {number[]}
|
|
145
|
+
*/
|
|
146
|
+
getData(userId) {
|
|
147
|
+
return this.userData.get(userId) || [];
|
|
148
|
+
}
|
|
149
|
+
/**
|
|
150
|
+
* Cleanup old/inactive users
|
|
151
|
+
* @private
|
|
152
|
+
*/
|
|
153
|
+
_cleanup() {
|
|
154
|
+
const now = Date.now();
|
|
155
|
+
for (const [userId, last] of this.lastSeen.entries()) {
|
|
156
|
+
if (now - last > this.maxIdle) {
|
|
157
|
+
this.userData.delete(userId);
|
|
158
|
+
this.lastSeen.delete(userId);
|
|
159
|
+
}
|
|
160
|
+
}
|
|
161
|
+
}
|
|
162
|
+
/**
|
|
163
|
+
* Destroy the rate limiter, stopping all intervals
|
|
164
|
+
*/
|
|
165
|
+
destroy() {
|
|
166
|
+
clearInterval(this._cleanupTimer);
|
|
167
|
+
this.userData.clear();
|
|
168
|
+
this.lastSeen.clear();
|
|
169
|
+
}
|
|
170
|
+
}
|
|
171
|
+
export default TinyRateLimiter;
|