tiny-essentials 1.24.5 → 1.25.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.
@@ -1,60 +1,188 @@
1
1
  'use strict';
2
2
 
3
- /** @typedef {(groupId: string) => void} OnMemoryExceeded */
3
+ /**
4
+ * Callback triggered when a group's stored hit history exceeds
5
+ * the configured memory limit.
6
+ *
7
+ * This callback is purely informational and does not block execution.
8
+ *
9
+ * @typedef {(groupId: string) => void} OnMemoryExceeded
10
+ */
4
11
 
5
- /** @typedef {(groupId: string) => void} OnGroupExpired */
12
+ /**
13
+ * Callback triggered when a group is considered expired and removed
14
+ * during the cleanup process.
15
+ *
16
+ * This usually happens when the group remains inactive longer than
17
+ * its configured TTL or the global maxIdle value.
18
+ *
19
+ * @typedef {(groupId: string) => void} OnGroupExpired
20
+ */
6
21
 
7
22
  /**
8
- * Class representing a flexible rate limiter per user or group.
23
+ * A lightweight and flexible rate limiter supporting both user-based
24
+ * and group-based throttling.
25
+ *
26
+ * ## Core Concepts
27
+ * - Every user belongs to a group.
28
+ * - If no explicit group is assigned, the user acts as their own group.
29
+ * - All users within the same group share the same hit history and limits.
30
+ *
31
+ * ## Supported Limiting Strategies
32
+ * - Max number of hits
33
+ * - Time-based sliding window
34
+ * - Combination of both
35
+ *
36
+ * ## Extra Features
37
+ * - Per-group TTL (time-to-live)
38
+ * - Automatic cleanup of inactive groups
39
+ * - Optional memory cap per group
40
+ * - Runtime metrics and statistics
9
41
  *
10
- * This rate limiter supports limiting per user or per group by mapping
11
- * userIds to a common groupId. All users within the same group share
12
- * rate limits.
42
+ * ## Interval Window (Sliding Window) Behavior
43
+ *
44
+ * This rate limiter uses a **sliding time window** strategy when `interval`
45
+ * is configured.
46
+ *
47
+ * ### How it works
48
+ * - Each hit is stored as a timestamp (Date.now()).
49
+ * - On every new hit and rate check, timestamps older than:
50
+ *
51
+ * `now - interval`
52
+ *
53
+ * are discarded.
54
+ *
55
+ * - Only hits that occurred within the last `interval` milliseconds
56
+ * are considered valid.
57
+ *
58
+ * ### Practical example
59
+ * If:
60
+ * - interval = 10_000 (10 seconds)
61
+ * - maxHits = 5
62
+ *
63
+ * Then:
64
+ * - Any group may perform **up to 5 hits in any rolling 10-second window**.
65
+ * - The window moves continuously with time.
66
+ *
67
+ * This avoids burst issues common in fixed-window rate limiters.
68
+ *
69
+ * This class is designed to be deterministic, predictable,
70
+ * and safe to use in long-running Node.js processes.
13
71
  */
14
72
  class TinyRateLimiter {
15
- /** @type {number|null} */
73
+ /**
74
+ * Maximum number of timestamps stored per group.
75
+ * When exceeded, older entries are discarded.
76
+ *
77
+ * `null` means unlimited.
78
+ *
79
+ * @type {number|null}
80
+ */
16
81
  #maxMemory = null;
17
82
 
18
- /** @type {NodeJS.Timeout|null} */
83
+ /**
84
+ * Internal timer reference used for periodic cleanup.
85
+ *
86
+ * @type {NodeJS.Timeout|null}
87
+ */
19
88
  #cleanupTimer = null;
20
89
 
21
- /** @type {number|null|undefined} */
90
+ /**
91
+ * Maximum number of allowed hits within the interval window.
92
+ *
93
+ * If `null`, hit count is not enforced.
94
+ *
95
+ * @type {number|null|undefined}
96
+ */
22
97
  #maxHits = null;
23
98
 
24
- /** @type {number|null|undefined} */
99
+ /**
100
+ * Sliding window duration in milliseconds.
101
+ *
102
+ * When defined, the rate limiter operates in **sliding window mode**:
103
+ * only hits that occurred within the last `interval` milliseconds
104
+ * are considered when evaluating limits.
105
+ *
106
+ * If `null`, time-based limiting is disabled and only `maxHits`
107
+ * (if defined) is used.
108
+ *
109
+ * @type {number|null|undefined}
110
+ */
25
111
  #interval = null;
26
112
 
27
- /** @type {number|null|undefined} */
113
+ /**
114
+ * Interval (in milliseconds) at which cleanup runs automatically.
115
+ *
116
+ * If `null`, cleanup must be triggered manually.
117
+ *
118
+ * @type {number|null|undefined}
119
+ */
28
120
  #cleanupInterval = null;
29
121
 
30
- /** @type {number|null|undefined} */
122
+ /**
123
+ * Maximum allowed inactivity time (in milliseconds)
124
+ * before a group is considered expired.
125
+ *
126
+ * @type {number|null|undefined}
127
+ */
31
128
  #maxIdle = null;
32
129
 
33
- /** @type {Map<string, number[]>} */
130
+ /**
131
+ * Stores hit timestamps per group.
132
+ *
133
+ * Key: groupId
134
+ * Value: array of timestamps (ms)
135
+ *
136
+ * @type {Map<string, number[]>}
137
+ */
34
138
  groupData = new Map(); // groupId -> timestamps[]
35
139
 
36
- /** @type {Map<string, number>} */
140
+ /**
141
+ * Stores the timestamp of the most recent activity per group.
142
+ *
143
+ * Used for expiration and cleanup logic.
144
+ *
145
+ * @type {Map<string, number>}
146
+ */
37
147
  lastSeen = new Map(); // groupId -> timestamp
38
148
 
39
- /** @type {Map<string, string>} */
149
+ /**
150
+ * Maps user IDs to their assigned group IDs.
151
+ *
152
+ * @type {Map<string, string>}
153
+ */
40
154
  userToGroup = new Map(); // userId -> groupId
41
155
 
42
- /** @type {Map<string, boolean>} */
156
+ /**
157
+ * Flags whether an ID represents a true group or an implicit user group.
158
+ *
159
+ * `true` → explicit group
160
+ * `false` → implicit (user acting as group)
161
+ *
162
+ * @type {Map<string, boolean>}
163
+ */
43
164
  groupFlags = new Map(); // groupId -> boolean
44
165
 
45
166
  /**
167
+ * Per-group TTL (time-to-live) overrides.
168
+ *
169
+ * If not defined for a group, `maxIdle` is used instead.
170
+ *
46
171
  * @type {Map<string, number>}
47
- * Stores TTL (in ms) for each groupId individually
48
172
  */
49
173
  groupTTL = new Map();
50
174
 
51
175
  /**
176
+ * Callback invoked when a group's memory limit is exceeded.
177
+ *
52
178
  * @type {null|OnMemoryExceeded}
53
179
  */
54
180
  #onMemoryExceeded = null;
55
181
 
56
182
  /**
57
- * Set the callback to be triggered when a group exceeds its limit
183
+ * Assign a callback to be notified when a group's stored
184
+ * hit history exceeds the configured memory limit.
185
+ *
58
186
  * @param {OnMemoryExceeded} callback
59
187
  */
60
188
  setOnMemoryExceeded(callback) {
@@ -63,13 +191,15 @@ class TinyRateLimiter {
63
191
  }
64
192
 
65
193
  /**
66
- * Clear the onMemoryExceeded callback
194
+ * Removes the memory-exceeded callback.
67
195
  */
68
196
  clearOnMemoryExceeded() {
69
197
  this.#onMemoryExceeded = null;
70
198
  }
71
199
 
72
200
  /**
201
+ * Callback invoked when a group expires and is removed.
202
+ *
73
203
  * @type {null|OnGroupExpired}
74
204
  */
75
205
  #onGroupExpired = null;
@@ -88,19 +218,28 @@ class TinyRateLimiter {
88
218
  }
89
219
 
90
220
  /**
91
- * Clear the onGroupExpired callback
221
+ * Removes the group-expiration callback.
92
222
  */
93
223
  clearOnGroupExpired() {
94
224
  this.#onGroupExpired = null;
95
225
  }
96
226
 
97
227
  /**
228
+ * Creates a new TinyRateLimiter instance.
229
+ *
230
+ * At least one of `maxHits` or `interval` must be provided.
231
+ *
98
232
  * @param {Object} options
99
- * @param {number|null} [options.maxMemory] - Max memory allowed
100
- * @param {number} [options.maxHits] - Max interactions allowed
101
- * @param {number} [options.interval] - Time window in milliseconds
102
- * @param {number} [options.cleanupInterval] - Interval for automatic cleanup (ms)
103
- * @param {number} [options.maxIdle=300000] - Max idle time for a user before being cleaned (ms)
233
+ * @param {number|null} [options.maxMemory=100000]
234
+ * Maximum timestamps stored per group (memory cap).
235
+ * @param {number} [options.maxHits]
236
+ * Maximum number of allowed hits.
237
+ * @param {number} [options.interval]
238
+ * Sliding time window in milliseconds.
239
+ * @param {number} [options.cleanupInterval]
240
+ * Interval (ms) for automatic cleanup execution.
241
+ * @param {number} [options.maxIdle=300000]
242
+ * Maximum inactivity time (ms) before a group expires.
104
243
  */
105
244
  constructor({ maxHits, interval, cleanupInterval, maxIdle = 300000, maxMemory = 100000 }) {
106
245
  /** @param {number|undefined} val */
@@ -195,10 +334,14 @@ class TinyRateLimiter {
195
334
  }
196
335
 
197
336
  /**
198
- * Assign a userId to a groupId, with merge if user has existing data.
337
+ * Assigns a user to a group.
338
+ *
339
+ * If the user already has recorded hits, those hits
340
+ * are merged into the target group.
341
+ *
199
342
  * @param {string} userId
200
343
  * @param {string} groupId
201
- * @throws {Error} If userId is already assigned to a different group
344
+ * @throws {Error} If the user belongs to another group
202
345
  */
203
346
  assignToGroup(userId, groupId) {
204
347
  const existingGroup = this.userToGroup.get(userId);
@@ -239,7 +382,11 @@ class TinyRateLimiter {
239
382
  }
240
383
 
241
384
  /**
242
- * Get the groupId for a given userId
385
+ * Resolves the effective group ID for a user.
386
+ *
387
+ * If the user is not explicitly assigned to a group,
388
+ * the user ID itself is treated as the group ID.
389
+ *
243
390
  * @param {string} userId
244
391
  * @returns {string}
245
392
  */
@@ -248,7 +395,25 @@ class TinyRateLimiter {
248
395
  }
249
396
 
250
397
  /**
251
- * Register a hit for a specific user
398
+ * Registers a hit for a user and applies the sliding window logic.
399
+ *
400
+ * ⚠️ **Important usage notice**
401
+ * This method **must be called before** `isRateLimited(userId)`
402
+ * in order for rate limit checks to work correctly.
403
+ *
404
+ * ### Sliding window cleanup
405
+ * When `interval` is configured:
406
+ * - The current timestamp is added to the group's history.
407
+ * - All timestamps older than `now - interval` are immediately removed.
408
+ *
409
+ * This ensures that the stored history always represents
410
+ * the **current active window**.
411
+ *
412
+ * ### Important notes
413
+ * - Cleanup happens on every hit, not on a fixed schedule.
414
+ * - The window continuously moves forward in time.
415
+ * - Memory usage is naturally bounded by time, and optionally by `maxMemory`.
416
+ *
252
417
  * @param {string} userId
253
418
  */
254
419
  hit(userId) {
@@ -285,7 +450,22 @@ class TinyRateLimiter {
285
450
  }
286
451
 
287
452
  /**
288
- * Check if the user (via their group) is currently rate limited
453
+ * Checks whether a user is currently rate limited.
454
+ *
455
+ * ### Evaluation process
456
+ * When `interval` is defined:
457
+ * 1. The cutoff time is calculated as:
458
+ *
459
+ * `Date.now() - interval`
460
+ *
461
+ * 2. Only hits newer than this cutoff are counted.
462
+ * 3. If `maxHits` is defined:
463
+ * - The group is limited when the count exceeds `maxHits`.
464
+ * 4. If `maxHits` is not defined:
465
+ * - Any hit within the window causes a limited state.
466
+ *
467
+ * This guarantees consistent behavior regardless of when hits occur.
468
+ *
289
469
  * @param {string} userId
290
470
  * @returns {boolean}
291
471
  */
@@ -431,7 +611,11 @@ class TinyRateLimiter {
431
611
  }
432
612
 
433
613
  /**
434
- * Get the interval window in milliseconds.
614
+ * Returns the configured sliding window size in milliseconds.
615
+ *
616
+ * This value represents how far back in time hits are considered
617
+ * valid for rate limiting decisions.
618
+ *
435
619
  * @returns {number}
436
620
  */
437
621
  getInterval() {
@@ -1,23 +1,103 @@
1
1
  export default TinyRateLimiter;
2
+ /**
3
+ * Callback triggered when a group's stored hit history exceeds
4
+ * the configured memory limit.
5
+ *
6
+ * This callback is purely informational and does not block execution.
7
+ */
2
8
  export type OnMemoryExceeded = (groupId: string) => void;
9
+ /**
10
+ * Callback triggered when a group is considered expired and removed
11
+ * during the cleanup process.
12
+ *
13
+ * This usually happens when the group remains inactive longer than
14
+ * its configured TTL or the global maxIdle value.
15
+ */
3
16
  export type OnGroupExpired = (groupId: string) => void;
4
- /** @typedef {(groupId: string) => void} OnMemoryExceeded */
5
- /** @typedef {(groupId: string) => void} OnGroupExpired */
6
17
  /**
7
- * Class representing a flexible rate limiter per user or group.
18
+ * Callback triggered when a group's stored hit history exceeds
19
+ * the configured memory limit.
20
+ *
21
+ * This callback is purely informational and does not block execution.
22
+ *
23
+ * @typedef {(groupId: string) => void} OnMemoryExceeded
24
+ */
25
+ /**
26
+ * Callback triggered when a group is considered expired and removed
27
+ * during the cleanup process.
28
+ *
29
+ * This usually happens when the group remains inactive longer than
30
+ * its configured TTL or the global maxIdle value.
31
+ *
32
+ * @typedef {(groupId: string) => void} OnGroupExpired
33
+ */
34
+ /**
35
+ * A lightweight and flexible rate limiter supporting both user-based
36
+ * and group-based throttling.
37
+ *
38
+ * ## Core Concepts
39
+ * - Every user belongs to a group.
40
+ * - If no explicit group is assigned, the user acts as their own group.
41
+ * - All users within the same group share the same hit history and limits.
42
+ *
43
+ * ## Supported Limiting Strategies
44
+ * - Max number of hits
45
+ * - Time-based sliding window
46
+ * - Combination of both
8
47
  *
9
- * This rate limiter supports limiting per user or per group by mapping
10
- * userIds to a common groupId. All users within the same group share
11
- * rate limits.
48
+ * ## Extra Features
49
+ * - Per-group TTL (time-to-live)
50
+ * - Automatic cleanup of inactive groups
51
+ * - Optional memory cap per group
52
+ * - Runtime metrics and statistics
53
+ *
54
+ * ## Interval Window (Sliding Window) Behavior
55
+ *
56
+ * This rate limiter uses a **sliding time window** strategy when `interval`
57
+ * is configured.
58
+ *
59
+ * ### How it works
60
+ * - Each hit is stored as a timestamp (Date.now()).
61
+ * - On every new hit and rate check, timestamps older than:
62
+ *
63
+ * `now - interval`
64
+ *
65
+ * are discarded.
66
+ *
67
+ * - Only hits that occurred within the last `interval` milliseconds
68
+ * are considered valid.
69
+ *
70
+ * ### Practical example
71
+ * If:
72
+ * - interval = 10_000 (10 seconds)
73
+ * - maxHits = 5
74
+ *
75
+ * Then:
76
+ * - Any group may perform **up to 5 hits in any rolling 10-second window**.
77
+ * - The window moves continuously with time.
78
+ *
79
+ * This avoids burst issues common in fixed-window rate limiters.
80
+ *
81
+ * This class is designed to be deterministic, predictable,
82
+ * and safe to use in long-running Node.js processes.
12
83
  */
13
84
  declare class TinyRateLimiter {
14
85
  /**
86
+ * Creates a new TinyRateLimiter instance.
87
+ *
88
+ * At least one of `maxHits` or `interval` must be provided.
89
+ *
15
90
  * @param {Object} options
16
- * @param {number|null} [options.maxMemory] - Max memory allowed
17
- * @param {number} [options.maxHits] - Max interactions allowed
18
- * @param {number} [options.interval] - Time window in milliseconds
19
- * @param {number} [options.cleanupInterval] - Interval for automatic cleanup (ms)
20
- * @param {number} [options.maxIdle=300000] - Max idle time for a user before being cleaned (ms)
91
+ * @param {number|null} [options.maxMemory=100000]
92
+ * Maximum timestamps stored per group (memory cap).
93
+ * @param {number} [options.maxHits]
94
+ * Maximum number of allowed hits.
95
+ * @param {number} [options.interval]
96
+ * Sliding time window in milliseconds.
97
+ * @param {number} [options.cleanupInterval]
98
+ * Interval (ms) for automatic cleanup execution.
99
+ * @param {number} [options.maxIdle=300000]
100
+ * Maximum inactivity time (ms) before a group expires.
21
101
  */
22
102
  constructor({ maxHits, interval, cleanupInterval, maxIdle, maxMemory }: {
23
103
  maxMemory?: number | null | undefined;
@@ -26,26 +106,55 @@ declare class TinyRateLimiter {
26
106
  cleanupInterval?: number | undefined;
27
107
  maxIdle?: number | undefined;
28
108
  });
29
- /** @type {Map<string, number[]>} */
109
+ /**
110
+ * Stores hit timestamps per group.
111
+ *
112
+ * Key: groupId
113
+ * Value: array of timestamps (ms)
114
+ *
115
+ * @type {Map<string, number[]>}
116
+ */
30
117
  groupData: Map<string, number[]>;
31
- /** @type {Map<string, number>} */
118
+ /**
119
+ * Stores the timestamp of the most recent activity per group.
120
+ *
121
+ * Used for expiration and cleanup logic.
122
+ *
123
+ * @type {Map<string, number>}
124
+ */
32
125
  lastSeen: Map<string, number>;
33
- /** @type {Map<string, string>} */
126
+ /**
127
+ * Maps user IDs to their assigned group IDs.
128
+ *
129
+ * @type {Map<string, string>}
130
+ */
34
131
  userToGroup: Map<string, string>;
35
- /** @type {Map<string, boolean>} */
132
+ /**
133
+ * Flags whether an ID represents a true group or an implicit user group.
134
+ *
135
+ * `true` → explicit group
136
+ * `false` → implicit (user acting as group)
137
+ *
138
+ * @type {Map<string, boolean>}
139
+ */
36
140
  groupFlags: Map<string, boolean>;
37
141
  /**
142
+ * Per-group TTL (time-to-live) overrides.
143
+ *
144
+ * If not defined for a group, `maxIdle` is used instead.
145
+ *
38
146
  * @type {Map<string, number>}
39
- * Stores TTL (in ms) for each groupId individually
40
147
  */
41
148
  groupTTL: Map<string, number>;
42
149
  /**
43
- * Set the callback to be triggered when a group exceeds its limit
150
+ * Assign a callback to be notified when a group's stored
151
+ * hit history exceeds the configured memory limit.
152
+ *
44
153
  * @param {OnMemoryExceeded} callback
45
154
  */
46
155
  setOnMemoryExceeded(callback: OnMemoryExceeded): void;
47
156
  /**
48
- * Clear the onMemoryExceeded callback
157
+ * Removes the memory-exceeded callback.
49
158
  */
50
159
  clearOnMemoryExceeded(): void;
51
160
  /**
@@ -58,7 +167,7 @@ declare class TinyRateLimiter {
58
167
  */
59
168
  setOnGroupExpired(callback: OnGroupExpired): void;
60
169
  /**
61
- * Clear the onGroupExpired callback
170
+ * Removes the group-expiration callback.
62
171
  */
63
172
  clearOnGroupExpired(): void;
64
173
  /**
@@ -91,25 +200,66 @@ declare class TinyRateLimiter {
91
200
  */
92
201
  deleteGroupTTL(groupId: string): void;
93
202
  /**
94
- * Assign a userId to a groupId, with merge if user has existing data.
203
+ * Assigns a user to a group.
204
+ *
205
+ * If the user already has recorded hits, those hits
206
+ * are merged into the target group.
207
+ *
95
208
  * @param {string} userId
96
209
  * @param {string} groupId
97
- * @throws {Error} If userId is already assigned to a different group
210
+ * @throws {Error} If the user belongs to another group
98
211
  */
99
212
  assignToGroup(userId: string, groupId: string): void;
100
213
  /**
101
- * Get the groupId for a given userId
214
+ * Resolves the effective group ID for a user.
215
+ *
216
+ * If the user is not explicitly assigned to a group,
217
+ * the user ID itself is treated as the group ID.
218
+ *
102
219
  * @param {string} userId
103
220
  * @returns {string}
104
221
  */
105
222
  getGroupId(userId: string): string;
106
223
  /**
107
- * Register a hit for a specific user
224
+ * Registers a hit for a user and applies the sliding window logic.
225
+ *
226
+ * ⚠️ **Important usage notice**
227
+ * This method **must be called before** `isRateLimited(userId)`
228
+ * in order for rate limit checks to work correctly.
229
+ *
230
+ * ### Sliding window cleanup
231
+ * When `interval` is configured:
232
+ * - The current timestamp is added to the group's history.
233
+ * - All timestamps older than `now - interval` are immediately removed.
234
+ *
235
+ * This ensures that the stored history always represents
236
+ * the **current active window**.
237
+ *
238
+ * ### Important notes
239
+ * - Cleanup happens on every hit, not on a fixed schedule.
240
+ * - The window continuously moves forward in time.
241
+ * - Memory usage is naturally bounded by time, and optionally by `maxMemory`.
242
+ *
108
243
  * @param {string} userId
109
244
  */
110
245
  hit(userId: string): void;
111
246
  /**
112
- * Check if the user (via their group) is currently rate limited
247
+ * Checks whether a user is currently rate limited.
248
+ *
249
+ * ### Evaluation process
250
+ * When `interval` is defined:
251
+ * 1. The cutoff time is calculated as:
252
+ *
253
+ * `Date.now() - interval`
254
+ *
255
+ * 2. Only hits newer than this cutoff are counted.
256
+ * 3. If `maxHits` is defined:
257
+ * - The group is limited when the count exceeds `maxHits`.
258
+ * 4. If `maxHits` is not defined:
259
+ * - Any hit within the window causes a limited state.
260
+ *
261
+ * This guarantees consistent behavior regardless of when hits occur.
262
+ *
113
263
  * @param {string} userId
114
264
  * @returns {boolean}
115
265
  */
@@ -168,7 +318,11 @@ declare class TinyRateLimiter {
168
318
  */
169
319
  getAllUserMappings(): Record<string, string>;
170
320
  /**
171
- * Get the interval window in milliseconds.
321
+ * Returns the configured sliding window size in milliseconds.
322
+ *
323
+ * This value represents how far back in time hits are considered
324
+ * valid for rate limiting decisions.
325
+ *
172
326
  * @returns {number}
173
327
  */
174
328
  getInterval(): number;