tiny-essentials 1.9.1 → 1.10.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.
@@ -30,24 +30,109 @@ __webpack_require__.d(__webpack_exports__, {
30
30
  });
31
31
 
32
32
  ;// ./src/v1/libs/TinyRateLimiter.mjs
33
+ /** @typedef {(groupId: string) => void} OnMemoryExceeded */
34
+
35
+ /** @typedef {(groupId: string) => void} OnGroupExpired */
36
+
33
37
  /**
34
- * Class representing a flexible rate limiter per user.
35
- *
36
- * This rate limiter can be configured by maximum number of hits,
37
- * time interval, or a combination of both. It supports automatic
38
- * cleanup of inactive users to optimize memory usage.
38
+ * Class representing a flexible rate limiter per user or group.
39
39
  *
40
- * @class
40
+ * This rate limiter supports limiting per user or per group by mapping
41
+ * userIds to a common groupId. All users within the same group share
42
+ * rate limits.
41
43
  */
42
44
  class TinyRateLimiter {
45
+ /** @type {number|null} */
46
+ #maxMemory = null;
47
+
48
+ /** @type {NodeJS.Timeout|null} */
49
+ #cleanupTimer = null;
50
+
51
+ /** @type {number|null|undefined} */
52
+ #maxHits = null;
53
+
54
+ /** @type {number|null|undefined} */
55
+ #interval = null;
56
+
57
+ /** @type {number|null|undefined} */
58
+ #cleanupInterval = null;
59
+
60
+ /** @type {number|null|undefined} */
61
+ #maxIdle = null;
62
+
63
+ /** @type {Map<string, number[]>} */
64
+ groupData = new Map(); // groupId -> timestamps[]
65
+
66
+ /** @type {Map<string, number>} */
67
+ lastSeen = new Map(); // groupId -> timestamp
68
+
69
+ /** @type {Map<string, string>} */
70
+ userToGroup = new Map(); // userId -> groupId
71
+
72
+ /** @type {Map<string, boolean>} */
73
+ groupFlags = new Map(); // groupId -> boolean
74
+
75
+ /**
76
+ * @type {Map<string, number>}
77
+ * Stores TTL (in ms) for each groupId individually
78
+ */
79
+ groupTTL = new Map();
80
+
81
+ /**
82
+ * @type {null|OnMemoryExceeded}
83
+ */
84
+ #onMemoryExceeded = null;
85
+
86
+ /**
87
+ * Set the callback to be triggered when a group exceeds its limit
88
+ * @param {OnMemoryExceeded} callback
89
+ */
90
+ setOnMemoryExceeded(callback) {
91
+ if (typeof callback !== 'function') throw new Error('onMemoryExceeded must be a function');
92
+ this.#onMemoryExceeded = callback;
93
+ }
94
+
95
+ /**
96
+ * Clear the onMemoryExceeded callback
97
+ */
98
+ clearOnMemoryExceeded() {
99
+ this.#onMemoryExceeded = null;
100
+ }
101
+
102
+ /**
103
+ * @type {null|OnGroupExpired}
104
+ */
105
+ #onGroupExpired = null;
106
+
107
+ /**
108
+ * Set the callback to be triggered when a group expires and is removed.
109
+ *
110
+ * This callback is called automatically during cleanup when a group
111
+ * becomes inactive for longer than its TTL.
112
+ *
113
+ * @param {OnGroupExpired} callback - A function that receives the expired groupId.
114
+ */
115
+ setOnGroupExpired(callback) {
116
+ if (typeof callback !== 'function') throw new Error('onGroupExpired must be a function');
117
+ this.#onGroupExpired = callback;
118
+ }
119
+
120
+ /**
121
+ * Clear the onGroupExpired callback
122
+ */
123
+ clearOnGroupExpired() {
124
+ this.#onGroupExpired = null;
125
+ }
126
+
43
127
  /**
44
128
  * @param {Object} options
129
+ * @param {number|null} [options.maxMemory] - Max memory allowed
45
130
  * @param {number} [options.maxHits] - Max interactions allowed
46
131
  * @param {number} [options.interval] - Time window in milliseconds
47
132
  * @param {number} [options.cleanupInterval] - Interval for automatic cleanup (ms)
48
133
  * @param {number} [options.maxIdle=300000] - Max idle time for a user before being cleaned (ms)
49
134
  */
50
- constructor({ maxHits, interval, cleanupInterval, maxIdle = 300000 }) {
135
+ constructor({ maxHits, interval, cleanupInterval, maxIdle = 300000, maxMemory = 100000 }) {
51
136
  /** @param {number|undefined} val */
52
137
  const isPositiveInteger = (val) =>
53
138
  typeof val === 'number' && Number.isFinite(val) && val >= 1 && Number.isInteger(val);
@@ -67,45 +152,129 @@ class TinyRateLimiter {
67
152
  throw new Error("'cleanupInterval' must be a positive integer in milliseconds if defined.");
68
153
  if (!isMaxIdleValid) throw new Error("'maxIdle' must be a positive integer in milliseconds.");
69
154
 
70
- this.maxHits = isMaxHitsValid ? maxHits : null;
71
- this.interval = isIntervalValid ? interval : null;
72
- this.cleanupInterval = isCleanupValid ? cleanupInterval : null;
73
- this.maxIdle = maxIdle;
74
-
75
- /** @type {Map<string, number[]>} */
76
- this.userData = new Map();
155
+ if (typeof maxMemory === 'number' && Number.isFinite(maxMemory) && maxMemory > 0) {
156
+ this.#maxMemory = Math.floor(maxMemory);
157
+ } else if (maxMemory === null || maxMemory === undefined) {
158
+ this.#maxMemory = null;
159
+ } else {
160
+ throw new Error('maxMemory must be a positive number or null');
161
+ }
77
162
 
78
- /** @type {Map<string, number>} */
79
- this.lastSeen = new Map();
163
+ this.#maxHits = isMaxHitsValid ? maxHits : null;
164
+ this.#interval = isIntervalValid ? interval : null;
165
+ this.#cleanupInterval = isCleanupValid ? cleanupInterval : null;
166
+ this.#maxIdle = maxIdle;
80
167
 
81
168
  // Start automatic cleanup only if cleanupInterval is valid
82
- if (this.cleanupInterval !== null)
83
- this._cleanupTimer = setInterval(() => this._cleanup(), this.cleanupInterval);
169
+ if (this.#cleanupInterval !== null)
170
+ this.#cleanupTimer = setInterval(() => this._cleanup(), this.#cleanupInterval);
84
171
  }
85
172
 
86
173
  /**
87
- * Get the interval window in milliseconds.
88
- *
89
- * @returns {number} The interval value.
90
- * @throws {Error} If interval is not a valid finite number.
174
+ * Check if a given ID is a groupId (not a userId)
175
+ * @param {string} id
176
+ * @returns {boolean}
91
177
  */
92
- getInterval() {
93
- if (typeof this.interval !== 'number' || !Number.isFinite(this.interval))
94
- throw new Error("'interval' is not a valid finite number.");
95
- return this.interval;
178
+ isGroupId(id) {
179
+ const result = this.groupFlags.get(id);
180
+ return typeof result === 'boolean' ? result : false;
96
181
  }
97
182
 
98
183
  /**
99
- * Get the maximum number of allowed hits.
100
- *
101
- * @returns {number} The maxHits value.
102
- * @throws {Error} If maxHits is not a valid finite number.
184
+ * Get all user IDs that belong to a given group.
185
+ * @param {string} groupId
186
+ * @returns {string[]}
103
187
  */
104
- getMaxHits() {
105
- if (typeof this.maxHits !== 'number' || !Number.isFinite(this.maxHits)) {
106
- throw new Error("'maxHits' is not a valid finite number.");
188
+ getUsersInGroup(groupId) {
189
+ const users = [];
190
+ for (const [userId, assignedGroup] of this.userToGroup.entries()) {
191
+ if (assignedGroup === groupId) {
192
+ users.push(userId);
193
+ }
194
+ }
195
+
196
+ return users;
197
+ }
198
+
199
+ /**
200
+ * Set TTL (in milliseconds) for a specific group
201
+ * @param {string} groupId
202
+ * @param {number} ttl
203
+ */
204
+ setGroupTTL(groupId, ttl) {
205
+ if (typeof ttl !== 'number' || !Number.isFinite(ttl) || ttl <= 0)
206
+ throw new Error('TTL must be a positive number in milliseconds');
207
+ this.groupTTL.set(groupId, ttl);
208
+ }
209
+
210
+ /**
211
+ * Get TTL (in ms) for a specific group.
212
+ * @param {string} groupId
213
+ * @returns {number|null}
214
+ */
215
+ getGroupTTL(groupId) {
216
+ return this.groupTTL.get(groupId) ?? null;
217
+ }
218
+
219
+ /**
220
+ * Delete the TTL setting for a specific group
221
+ * @param {string} groupId
222
+ */
223
+ deleteGroupTTL(groupId) {
224
+ this.groupTTL.delete(groupId);
225
+ }
226
+
227
+ /**
228
+ * Assign a userId to a groupId, with merge if user has existing data.
229
+ * @param {string} userId
230
+ * @param {string} groupId
231
+ * @throws {Error} If userId is already assigned to a different group
232
+ */
233
+ assignToGroup(userId, groupId) {
234
+ const existingGroup = this.userToGroup.get(userId);
235
+ if (existingGroup && existingGroup !== groupId)
236
+ throw new Error(`User ${userId} is already assigned to group ${existingGroup}`);
237
+
238
+ // If the user is already in the group, nothing needs to be done
239
+ if (existingGroup === groupId) return;
240
+ const userData = this.groupData.get(userId);
241
+
242
+ // Associates the user to the group
243
+ if (this.isGroupId(userId)) {
244
+ for (const [uid, gId] of this.userToGroup.entries())
245
+ if (gId === userId) this.userToGroup.set(uid, groupId);
246
+ this.userToGroup.delete(userId);
247
+ } else this.userToGroup.set(userId, groupId);
248
+
249
+ // If the user has no data, nothing needs to be done
250
+ if (!userData) return;
251
+
252
+ const groupData = this.groupData.get(groupId);
253
+ if (groupData) {
254
+ for (const item of userData) groupData.push(item);
255
+ } else {
256
+ const newData = [];
257
+ for (const item of userData) newData.push(item);
258
+ this.groupData.set(groupId, newData);
107
259
  }
108
- return this.maxHits;
260
+
261
+ this.lastSeen.set(groupId, Date.now());
262
+
263
+ // Removes individual data as they are now in the group
264
+ this.groupFlags.delete(userId);
265
+ this.groupData.delete(userId);
266
+ this.lastSeen.delete(userId);
267
+ this.groupTTL.delete(userId);
268
+ this.groupFlags.set(groupId, true);
269
+ }
270
+
271
+ /**
272
+ * Get the groupId for a given userId
273
+ * @param {string} userId
274
+ * @returns {string}
275
+ */
276
+ getGroupId(userId) {
277
+ return this.userToGroup.get(userId) || userId; // fallback: use userId as own group
109
278
  }
110
279
 
111
280
  /**
@@ -113,20 +282,22 @@ class TinyRateLimiter {
113
282
  * @param {string} userId
114
283
  */
115
284
  hit(userId) {
285
+ const groupId = this.getGroupId(userId);
116
286
  const now = Date.now();
117
287
 
118
- if (!this.userData.has(userId)) {
119
- this.userData.set(userId, []);
288
+ if (!this.groupData.has(groupId)) {
289
+ this.groupData.set(groupId, []);
290
+ this.groupFlags.set(groupId, false);
120
291
  }
121
292
 
122
- const history = this.userData.get(userId);
123
- if (!history) throw new Error(`No data found for userId: ${userId}`);
293
+ const history = this.groupData.get(groupId);
294
+ if (!history) throw new Error(`No data found for groupId: ${groupId}`);
124
295
 
125
296
  history.push(now);
126
- this.lastSeen.set(userId, now);
297
+ this.lastSeen.set(groupId, now);
127
298
 
128
299
  // Clean up old entries
129
- if (this.interval !== null) {
300
+ if (this.#interval !== null) {
130
301
  const interval = this.getInterval();
131
302
  const cutoff = now - interval;
132
303
  while (history.length && history[0] < cutoff) {
@@ -135,92 +306,295 @@ class TinyRateLimiter {
135
306
  }
136
307
 
137
308
  // Optional: keep only the last N entries for memory optimization
138
- if (this.maxHits !== null) {
139
- const maxHits = this.getMaxHits();
140
- if (history.length > maxHits) {
141
- history.splice(0, history.length - maxHits);
309
+ if (this.#maxMemory !== null && typeof this.#maxMemory === 'number') {
310
+ if (history.length > this.#maxMemory) {
311
+ history.splice(0, history.length - this.#maxMemory);
312
+ if (typeof this.#onMemoryExceeded === 'function') this.#onMemoryExceeded(groupId);
142
313
  }
143
314
  }
144
315
  }
145
316
 
146
317
  /**
147
- * Check if the user is currently rate limited
318
+ * Check if the user (via their group) is currently rate limited
148
319
  * @param {string} userId
149
320
  * @returns {boolean}
150
321
  */
151
322
  isRateLimited(userId) {
152
- const now = Date.now();
153
-
154
- if (!this.userData.has(userId)) return false;
323
+ const groupId = this.getGroupId(userId);
324
+ if (!this.groupData.has(groupId)) return false;
155
325
 
156
- const history = this.userData.get(userId);
157
- if (!history) throw new Error(`No data found for userId: ${userId}`);
326
+ const history = this.groupData.get(groupId);
327
+ if (!history) throw new Error(`No data found for groupId: ${groupId}`);
158
328
 
159
- if (this.interval !== null) {
329
+ if (this.#interval !== null) {
330
+ const now = Date.now();
160
331
  const interval = this.getInterval();
161
- const recent = history.filter((t) => t > now - interval);
162
- if (this.maxHits !== null) {
163
- return recent.length >= this.getMaxHits();
332
+ const cutoff = now - interval;
333
+ let count = 0;
334
+ for (let i = history.length - 1; i >= 0; i--) {
335
+ if (history[i] > cutoff) count++;
336
+ else break;
164
337
  }
165
- return recent.length > 0;
338
+ if (this.#maxHits !== null) return count > this.getMaxHits();
339
+ return count > 0;
166
340
  }
167
341
 
168
- if (this.maxHits !== null) {
169
- return history.length >= this.getMaxHits();
342
+ if (this.#maxHits !== null) {
343
+ return history.length > this.getMaxHits();
170
344
  }
171
345
 
172
346
  return false;
173
347
  }
174
348
 
175
349
  /**
176
- * Manually reset user data
350
+ * Manually reset group data
351
+ * @param {string} groupId
352
+ */
353
+ resetGroup(groupId) {
354
+ this.groupFlags.delete(groupId);
355
+ this.groupData.delete(groupId);
356
+ this.lastSeen.delete(groupId);
357
+ this.groupTTL.delete(groupId);
358
+ }
359
+
360
+ /**
361
+ * Manually reset user data.
362
+ *
363
+ * @deprecated Use `resetUserGroup(userId)` instead. This method will be removed in future versions.
177
364
  * @param {string} userId
365
+ * @returns {void}
178
366
  */
179
367
  reset(userId) {
180
- this.userData.delete(userId);
181
- this.lastSeen.delete(userId);
368
+ if (false)
369
+ {}
370
+ return this.resetUserGroup(userId);
182
371
  }
183
372
 
184
373
  /**
185
- * Set hit timestamps for a user
374
+ * Manually reset a user mapping
186
375
  * @param {string} userId
376
+ */
377
+ resetUserGroup(userId) {
378
+ this.userToGroup.delete(userId);
379
+ }
380
+
381
+ /**
382
+ * Set custom timestamps to a group
383
+ * @param {string} groupId
187
384
  * @param {number[]} timestamps
188
385
  */
189
- setData(userId, timestamps) {
190
- this.userData.set(userId, timestamps);
191
- this.lastSeen.set(userId, Date.now());
386
+ setData(groupId, timestamps) {
387
+ if (!Array.isArray(timestamps)) throw new Error('timestamps must be an array of numbers.');
388
+ for (const t of timestamps) {
389
+ if (typeof t !== 'number' || !Number.isFinite(t)) {
390
+ throw new Error('All timestamps must be finite numbers.');
391
+ }
392
+ }
393
+ if (!this.groupData.has(groupId)) this.groupFlags.set(groupId, false);
394
+ this.groupData.set(groupId, timestamps);
395
+ this.lastSeen.set(groupId, Date.now());
192
396
  }
193
397
 
194
398
  /**
195
- * Get timestamps from user
196
- * @param {string} userId
399
+ * Check if a group has data
400
+ * @param {string} groupId
401
+ * @returns {boolean}
402
+ */
403
+ hasData(groupId) {
404
+ return this.groupData.has(groupId);
405
+ }
406
+
407
+ /**
408
+ * Get timestamps from a group
409
+ * @param {string} groupId
197
410
  * @returns {number[]}
198
411
  */
199
- getData(userId) {
200
- return this.userData.get(userId) || [];
412
+ getData(groupId) {
413
+ return this.groupData.get(groupId) || [];
201
414
  }
202
415
 
203
416
  /**
204
- * Cleanup old/inactive users
417
+ * Get the maximum idle time (in milliseconds) before a group is considered expired.
418
+ * @returns {number}
419
+ */
420
+ getMaxIdle() {
421
+ if (typeof this.#maxIdle !== 'number' || !Number.isFinite(this.#maxIdle) || this.#maxIdle < 0) {
422
+ throw new Error("'maxIdle' must be a non-negative finite number.");
423
+ }
424
+ return this.#maxIdle;
425
+ }
426
+
427
+ /**
428
+ * Set the maximum idle time (in milliseconds) before a group is considered expired.
429
+ * @param {number} ms
430
+ */
431
+ setMaxIdle(ms) {
432
+ if (typeof ms !== 'number' || !Number.isFinite(ms) || ms < 0) {
433
+ throw new Error("'maxIdle' must be a non-negative finite number.");
434
+ }
435
+ this.#maxIdle = ms;
436
+ }
437
+
438
+ /**
439
+ * Cleanup old/inactive groups with individual TTLs
205
440
  * @private
206
441
  */
207
442
  _cleanup() {
208
443
  const now = Date.now();
209
- for (const [userId, last] of this.lastSeen.entries()) {
210
- if (now - last > this.maxIdle) {
211
- this.userData.delete(userId);
212
- this.lastSeen.delete(userId);
444
+ for (const [groupId, last] of this.lastSeen.entries()) {
445
+ const ttl = this.getGroupTTL(groupId) ?? this.getMaxIdle();
446
+ if (now - last > ttl) {
447
+ this.groupFlags.delete(groupId);
448
+ this.groupData.delete(groupId);
449
+ this.lastSeen.delete(groupId);
450
+ this.groupTTL.delete(groupId);
451
+
452
+ // Notify subclass or external binding
453
+ if (typeof this.#onGroupExpired === 'function') {
454
+ this.#onGroupExpired(groupId);
455
+ }
213
456
  }
214
457
  }
215
458
  }
216
459
 
217
460
  /**
218
- * Destroy the rate limiter, stopping all intervals
461
+ * Get list of active group IDs
462
+ * @returns {string[]}
463
+ */
464
+ getActiveGroups() {
465
+ return Array.from(this.groupData.keys());
466
+ }
467
+
468
+ /**
469
+ * Get a shallow copy of all user-to-group mappings as a plain object
470
+ * @returns {Record<string, string>}
471
+ */
472
+ getAllUserMappings() {
473
+ return Object.fromEntries(this.userToGroup);
474
+ }
475
+
476
+ /**
477
+ * Get the interval window in milliseconds.
478
+ * @returns {number}
479
+ */
480
+ getInterval() {
481
+ if (typeof this.#interval !== 'number' || !Number.isFinite(this.#interval)) {
482
+ throw new Error("'interval' is not a valid finite number.");
483
+ }
484
+ return this.#interval;
485
+ }
486
+
487
+ /**
488
+ * Get the maximum number of allowed hits.
489
+ * @returns {number}
490
+ */
491
+ getMaxHits() {
492
+ if (typeof this.#maxHits !== 'number' || !Number.isFinite(this.#maxHits)) {
493
+ throw new Error("'maxHits' is not a valid finite number.");
494
+ }
495
+ return this.#maxHits;
496
+ }
497
+
498
+ /**
499
+ * Get the total number of hits recorded for a group.
500
+ * @param {string} groupId
501
+ * @returns {number}
502
+ */
503
+ getTotalHits(groupId) {
504
+ const history = this.groupData.get(groupId);
505
+ return Array.isArray(history) ? history.length : 0;
506
+ }
507
+
508
+ /**
509
+ * Get the timestamp of the last hit for a group.
510
+ * @param {string} groupId
511
+ * @returns {number|null}
512
+ */
513
+ getLastHit(groupId) {
514
+ const history = this.groupData.get(groupId);
515
+ return history?.length ? history[history.length - 1] : null;
516
+ }
517
+
518
+ /**
519
+ * Get milliseconds since the last hit for a group.
520
+ * @param {string} groupId
521
+ * @returns {number|null}
522
+ */
523
+ getTimeSinceLastHit(groupId) {
524
+ const last = this.getLastHit(groupId);
525
+ return last !== null ? Date.now() - last : null;
526
+ }
527
+
528
+ /**
529
+ * Internal utility to compute average spacing
530
+ * @private
531
+ * @param {number[]|undefined} history
532
+ * @returns {number|null}
533
+ */
534
+ _calculateAverageSpacing(history) {
535
+ if (!Array.isArray(history) || history.length < 2) return null;
536
+ let total = 0;
537
+ for (let i = 1; i < history.length; i++) {
538
+ total += history[i] - history[i - 1];
539
+ }
540
+ return total / (history.length - 1);
541
+ }
542
+
543
+ /**
544
+ * Get average time between hits for a group (ms).
545
+ * @param {string} groupId
546
+ * @returns {number|null}
547
+ */
548
+ getAverageHitSpacing(groupId) {
549
+ return this._calculateAverageSpacing(this.groupData.get(groupId));
550
+ }
551
+
552
+ /**
553
+ * Get metrics about a group's activity.
554
+ * @param {string} groupId
555
+ * @returns {{
556
+ * totalHits: number,
557
+ * lastHit: number|null,
558
+ * timeSinceLastHit: number|null,
559
+ * averageHitSpacing: number|null
560
+ * }}
561
+ */
562
+ getMetrics(groupId) {
563
+ const history = this.groupData.get(groupId);
564
+
565
+ if (!Array.isArray(history) || history.length === 0) {
566
+ return {
567
+ totalHits: 0,
568
+ lastHit: null,
569
+ timeSinceLastHit: null,
570
+ averageHitSpacing: null,
571
+ };
572
+ }
573
+
574
+ const totalHits = history.length;
575
+ const lastHit = history[totalHits - 1];
576
+ const timeSinceLastHit = Date.now() - lastHit;
577
+ const averageHitSpacing = this._calculateAverageSpacing(history);
578
+
579
+ return {
580
+ totalHits,
581
+ lastHit,
582
+ timeSinceLastHit,
583
+ averageHitSpacing,
584
+ };
585
+ }
586
+
587
+ /**
588
+ * Destroy the rate limiter, stopping cleanup and clearing data
219
589
  */
220
590
  destroy() {
221
- if (this._cleanupTimer) clearInterval(this._cleanupTimer);
222
- this.userData.clear();
591
+ if (this.#cleanupTimer) clearInterval(this.#cleanupTimer);
592
+ this._cleanup();
593
+ this.groupData.clear();
223
594
  this.lastSeen.clear();
595
+ this.userToGroup.clear();
596
+ this.groupTTL.clear();
597
+ this.groupFlags.clear();
224
598
  }
225
599
  }
226
600
 
@@ -1 +1 @@
1
- (()=>{"use strict";var t={d:(e,i)=>{for(var s in i)t.o(i,s)&&!t.o(e,s)&&Object.defineProperty(e,s,{enumerable:!0,get:i[s]})},o:(t,e)=>Object.prototype.hasOwnProperty.call(t,e)},e={};t.d(e,{TinyRateLimiter:()=>i});const i=class{constructor({maxHits:t,interval:e,cleanupInterval:i,maxIdle:s=3e5}){const r=t=>"number"==typeof t&&Number.isFinite(t)&&t>=1&&Number.isInteger(t),n=r(t),a=r(e),l=r(i),o=r(s);if(!n&&!a)throw new Error("RateLimiter requires at least one valid option: 'maxHits' or 'interval'.");if(void 0!==t&&!n)throw new Error("'maxHits' must be a positive integer if defined.");if(void 0!==e&&!a)throw new Error("'interval' must be a positive integer in milliseconds if defined.");if(void 0!==i&&!l)throw new Error("'cleanupInterval' must be a positive integer in milliseconds if defined.");if(!o)throw new Error("'maxIdle' must be a positive integer in milliseconds.");this.maxHits=n?t:null,this.interval=a?e:null,this.cleanupInterval=l?i:null,this.maxIdle=s,this.userData=new Map,this.lastSeen=new Map,null!==this.cleanupInterval&&(this._cleanupTimer=setInterval((()=>this._cleanup()),this.cleanupInterval))}getInterval(){if("number"!=typeof this.interval||!Number.isFinite(this.interval))throw new Error("'interval' is not a valid finite number.");return this.interval}getMaxHits(){if("number"!=typeof this.maxHits||!Number.isFinite(this.maxHits))throw new Error("'maxHits' is not a valid finite number.");return this.maxHits}hit(t){const e=Date.now();this.userData.has(t)||this.userData.set(t,[]);const i=this.userData.get(t);if(!i)throw new Error(`No data found for userId: ${t}`);if(i.push(e),this.lastSeen.set(t,e),null!==this.interval){const t=e-this.getInterval();for(;i.length&&i[0]<t;)i.shift()}if(null!==this.maxHits){const t=this.getMaxHits();i.length>t&&i.splice(0,i.length-t)}}isRateLimited(t){const e=Date.now();if(!this.userData.has(t))return!1;const i=this.userData.get(t);if(!i)throw new Error(`No data found for userId: ${t}`);if(null!==this.interval){const t=this.getInterval(),s=i.filter((i=>i>e-t));return null!==this.maxHits?s.length>=this.getMaxHits():s.length>0}return null!==this.maxHits&&i.length>=this.getMaxHits()}reset(t){this.userData.delete(t),this.lastSeen.delete(t)}setData(t,e){this.userData.set(t,e),this.lastSeen.set(t,Date.now())}getData(t){return this.userData.get(t)||[]}_cleanup(){const t=Date.now();for(const[e,i]of this.lastSeen.entries())t-i>this.maxIdle&&(this.userData.delete(e),this.lastSeen.delete(e))}destroy(){this._cleanupTimer&&clearInterval(this._cleanupTimer),this.userData.clear(),this.lastSeen.clear()}};window.TinyRateLimiter=e.TinyRateLimiter})();
1
+ (()=>{"use strict";var e={d:(t,r)=>{for(var i in r)e.o(r,i)&&!e.o(t,i)&&Object.defineProperty(t,i,{enumerable:!0,get:r[i]})},o:(e,t)=>Object.prototype.hasOwnProperty.call(e,t)},t={};e.d(t,{TinyRateLimiter:()=>r});const r=class{#e=null;#t=null;#r=null;#i=null;#s=null;#n=null;groupData=new Map;lastSeen=new Map;userToGroup=new Map;groupFlags=new Map;groupTTL=new Map;#o=null;setOnMemoryExceeded(e){if("function"!=typeof e)throw new Error("onMemoryExceeded must be a function");this.#o=e}clearOnMemoryExceeded(){this.#o=null}#a=null;setOnGroupExpired(e){if("function"!=typeof e)throw new Error("onGroupExpired must be a function");this.#a=e}clearOnGroupExpired(){this.#a=null}constructor({maxHits:e,interval:t,cleanupInterval:r,maxIdle:i=3e5,maxMemory:s=1e5}){const n=e=>"number"==typeof e&&Number.isFinite(e)&&e>=1&&Number.isInteger(e),o=n(e),a=n(t),u=n(r),l=n(i);if(!o&&!a)throw new Error("RateLimiter requires at least one valid option: 'maxHits' or 'interval'.");if(void 0!==e&&!o)throw new Error("'maxHits' must be a positive integer if defined.");if(void 0!==t&&!a)throw new Error("'interval' must be a positive integer in milliseconds if defined.");if(void 0!==r&&!u)throw new Error("'cleanupInterval' must be a positive integer in milliseconds if defined.");if(!l)throw new Error("'maxIdle' must be a positive integer in milliseconds.");if("number"==typeof s&&Number.isFinite(s)&&s>0)this.#e=Math.floor(s);else{if(null!=s)throw new Error("maxMemory must be a positive number or null");this.#e=null}this.#r=o?e:null,this.#i=a?t:null,this.#s=u?r:null,this.#n=i,null!==this.#s&&(this.#t=setInterval((()=>this._cleanup()),this.#s))}isGroupId(e){const t=this.groupFlags.get(e);return"boolean"==typeof t&&t}getUsersInGroup(e){const t=[];for(const[r,i]of this.userToGroup.entries())i===e&&t.push(r);return t}setGroupTTL(e,t){if("number"!=typeof t||!Number.isFinite(t)||t<=0)throw new Error("TTL must be a positive number in milliseconds");this.groupTTL.set(e,t)}getGroupTTL(e){return this.groupTTL.get(e)??null}deleteGroupTTL(e){this.groupTTL.delete(e)}assignToGroup(e,t){const r=this.userToGroup.get(e);if(r&&r!==t)throw new Error(`User ${e} is already assigned to group ${r}`);if(r===t)return;const i=this.groupData.get(e);if(this.isGroupId(e)){for(const[r,i]of this.userToGroup.entries())i===e&&this.userToGroup.set(r,t);this.userToGroup.delete(e)}else this.userToGroup.set(e,t);if(!i)return;const s=this.groupData.get(t);if(s)for(const e of i)s.push(e);else{const e=[];for(const t of i)e.push(t);this.groupData.set(t,e)}this.lastSeen.set(t,Date.now()),this.groupFlags.delete(e),this.groupData.delete(e),this.lastSeen.delete(e),this.groupTTL.delete(e),this.groupFlags.set(t,!0)}getGroupId(e){return this.userToGroup.get(e)||e}hit(e){const t=this.getGroupId(e),r=Date.now();this.groupData.has(t)||(this.groupData.set(t,[]),this.groupFlags.set(t,!1));const i=this.groupData.get(t);if(!i)throw new Error(`No data found for groupId: ${t}`);if(i.push(r),this.lastSeen.set(t,r),null!==this.#i){const e=r-this.getInterval();for(;i.length&&i[0]<e;)i.shift()}null!==this.#e&&"number"==typeof this.#e&&i.length>this.#e&&(i.splice(0,i.length-this.#e),"function"==typeof this.#o&&this.#o(t))}isRateLimited(e){const t=this.getGroupId(e);if(!this.groupData.has(t))return!1;const r=this.groupData.get(t);if(!r)throw new Error(`No data found for groupId: ${t}`);if(null!==this.#i){const e=Date.now()-this.getInterval();let t=0;for(let i=r.length-1;i>=0&&r[i]>e;i--)t++;return null!==this.#r?t>this.getMaxHits():t>0}return null!==this.#r&&r.length>this.getMaxHits()}resetGroup(e){this.groupFlags.delete(e),this.groupData.delete(e),this.lastSeen.delete(e),this.groupTTL.delete(e)}reset(e){return this.resetUserGroup(e)}resetUserGroup(e){this.userToGroup.delete(e)}setData(e,t){if(!Array.isArray(t))throw new Error("timestamps must be an array of numbers.");for(const e of t)if("number"!=typeof e||!Number.isFinite(e))throw new Error("All timestamps must be finite numbers.");this.groupData.has(e)||this.groupFlags.set(e,!1),this.groupData.set(e,t),this.lastSeen.set(e,Date.now())}hasData(e){return this.groupData.has(e)}getData(e){return this.groupData.get(e)||[]}getMaxIdle(){if("number"!=typeof this.#n||!Number.isFinite(this.#n)||this.#n<0)throw new Error("'maxIdle' must be a non-negative finite number.");return this.#n}setMaxIdle(e){if("number"!=typeof e||!Number.isFinite(e)||e<0)throw new Error("'maxIdle' must be a non-negative finite number.");this.#n=e}_cleanup(){const e=Date.now();for(const[t,r]of this.lastSeen.entries())e-r>(this.getGroupTTL(t)??this.getMaxIdle())&&(this.groupFlags.delete(t),this.groupData.delete(t),this.lastSeen.delete(t),this.groupTTL.delete(t),"function"==typeof this.#a&&this.#a(t))}getActiveGroups(){return Array.from(this.groupData.keys())}getAllUserMappings(){return Object.fromEntries(this.userToGroup)}getInterval(){if("number"!=typeof this.#i||!Number.isFinite(this.#i))throw new Error("'interval' is not a valid finite number.");return this.#i}getMaxHits(){if("number"!=typeof this.#r||!Number.isFinite(this.#r))throw new Error("'maxHits' is not a valid finite number.");return this.#r}getTotalHits(e){const t=this.groupData.get(e);return Array.isArray(t)?t.length:0}getLastHit(e){const t=this.groupData.get(e);return t?.length?t[t.length-1]:null}getTimeSinceLastHit(e){const t=this.getLastHit(e);return null!==t?Date.now()-t:null}_calculateAverageSpacing(e){if(!Array.isArray(e)||e.length<2)return null;let t=0;for(let r=1;r<e.length;r++)t+=e[r]-e[r-1];return t/(e.length-1)}getAverageHitSpacing(e){return this._calculateAverageSpacing(this.groupData.get(e))}getMetrics(e){const t=this.groupData.get(e);if(!Array.isArray(t)||0===t.length)return{totalHits:0,lastHit:null,timeSinceLastHit:null,averageHitSpacing:null};const r=t.length,i=t[r-1];return{totalHits:r,lastHit:i,timeSinceLastHit:Date.now()-i,averageHitSpacing:this._calculateAverageSpacing(t)}}destroy(){this.#t&&clearInterval(this.#t),this._cleanup(),this.groupData.clear(),this.lastSeen.clear(),this.userToGroup.clear(),this.groupTTL.clear(),this.groupFlags.clear()}};window.TinyRateLimiter=t.TinyRateLimiter})();
@@ -1,7 +1,7 @@
1
1
  'use strict';
2
2
 
3
3
  require('clone');
4
- require('buffer');
4
+ require('../../v1/basics/objFilter.cjs');
5
5
 
6
6
  // @ts-nocheck
7
7