tiny-essentials 1.9.2 → 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.
@@ -3738,24 +3738,109 @@ class TinyPromiseQueue {
3738
3738
  /* harmony default export */ const libs_TinyPromiseQueue = (TinyPromiseQueue);
3739
3739
 
3740
3740
  ;// ./src/v1/libs/TinyRateLimiter.mjs
3741
+ /** @typedef {(groupId: string) => void} OnMemoryExceeded */
3742
+
3743
+ /** @typedef {(groupId: string) => void} OnGroupExpired */
3744
+
3741
3745
  /**
3742
- * Class representing a flexible rate limiter per user.
3743
- *
3744
- * This rate limiter can be configured by maximum number of hits,
3745
- * time interval, or a combination of both. It supports automatic
3746
- * cleanup of inactive users to optimize memory usage.
3746
+ * Class representing a flexible rate limiter per user or group.
3747
3747
  *
3748
- * @class
3748
+ * This rate limiter supports limiting per user or per group by mapping
3749
+ * userIds to a common groupId. All users within the same group share
3750
+ * rate limits.
3749
3751
  */
3750
3752
  class TinyRateLimiter {
3753
+ /** @type {number|null} */
3754
+ #maxMemory = null;
3755
+
3756
+ /** @type {NodeJS.Timeout|null} */
3757
+ #cleanupTimer = null;
3758
+
3759
+ /** @type {number|null|undefined} */
3760
+ #maxHits = null;
3761
+
3762
+ /** @type {number|null|undefined} */
3763
+ #interval = null;
3764
+
3765
+ /** @type {number|null|undefined} */
3766
+ #cleanupInterval = null;
3767
+
3768
+ /** @type {number|null|undefined} */
3769
+ #maxIdle = null;
3770
+
3771
+ /** @type {Map<string, number[]>} */
3772
+ groupData = new Map(); // groupId -> timestamps[]
3773
+
3774
+ /** @type {Map<string, number>} */
3775
+ lastSeen = new Map(); // groupId -> timestamp
3776
+
3777
+ /** @type {Map<string, string>} */
3778
+ userToGroup = new Map(); // userId -> groupId
3779
+
3780
+ /** @type {Map<string, boolean>} */
3781
+ groupFlags = new Map(); // groupId -> boolean
3782
+
3783
+ /**
3784
+ * @type {Map<string, number>}
3785
+ * Stores TTL (in ms) for each groupId individually
3786
+ */
3787
+ groupTTL = new Map();
3788
+
3789
+ /**
3790
+ * @type {null|OnMemoryExceeded}
3791
+ */
3792
+ #onMemoryExceeded = null;
3793
+
3794
+ /**
3795
+ * Set the callback to be triggered when a group exceeds its limit
3796
+ * @param {OnMemoryExceeded} callback
3797
+ */
3798
+ setOnMemoryExceeded(callback) {
3799
+ if (typeof callback !== 'function') throw new Error('onMemoryExceeded must be a function');
3800
+ this.#onMemoryExceeded = callback;
3801
+ }
3802
+
3803
+ /**
3804
+ * Clear the onMemoryExceeded callback
3805
+ */
3806
+ clearOnMemoryExceeded() {
3807
+ this.#onMemoryExceeded = null;
3808
+ }
3809
+
3810
+ /**
3811
+ * @type {null|OnGroupExpired}
3812
+ */
3813
+ #onGroupExpired = null;
3814
+
3815
+ /**
3816
+ * Set the callback to be triggered when a group expires and is removed.
3817
+ *
3818
+ * This callback is called automatically during cleanup when a group
3819
+ * becomes inactive for longer than its TTL.
3820
+ *
3821
+ * @param {OnGroupExpired} callback - A function that receives the expired groupId.
3822
+ */
3823
+ setOnGroupExpired(callback) {
3824
+ if (typeof callback !== 'function') throw new Error('onGroupExpired must be a function');
3825
+ this.#onGroupExpired = callback;
3826
+ }
3827
+
3828
+ /**
3829
+ * Clear the onGroupExpired callback
3830
+ */
3831
+ clearOnGroupExpired() {
3832
+ this.#onGroupExpired = null;
3833
+ }
3834
+
3751
3835
  /**
3752
3836
  * @param {Object} options
3837
+ * @param {number|null} [options.maxMemory] - Max memory allowed
3753
3838
  * @param {number} [options.maxHits] - Max interactions allowed
3754
3839
  * @param {number} [options.interval] - Time window in milliseconds
3755
3840
  * @param {number} [options.cleanupInterval] - Interval for automatic cleanup (ms)
3756
3841
  * @param {number} [options.maxIdle=300000] - Max idle time for a user before being cleaned (ms)
3757
3842
  */
3758
- constructor({ maxHits, interval, cleanupInterval, maxIdle = 300000 }) {
3843
+ constructor({ maxHits, interval, cleanupInterval, maxIdle = 300000, maxMemory = 100000 }) {
3759
3844
  /** @param {number|undefined} val */
3760
3845
  const isPositiveInteger = (val) =>
3761
3846
  typeof val === 'number' && Number.isFinite(val) && val >= 1 && Number.isInteger(val);
@@ -3775,45 +3860,129 @@ class TinyRateLimiter {
3775
3860
  throw new Error("'cleanupInterval' must be a positive integer in milliseconds if defined.");
3776
3861
  if (!isMaxIdleValid) throw new Error("'maxIdle' must be a positive integer in milliseconds.");
3777
3862
 
3778
- this.maxHits = isMaxHitsValid ? maxHits : null;
3779
- this.interval = isIntervalValid ? interval : null;
3780
- this.cleanupInterval = isCleanupValid ? cleanupInterval : null;
3781
- this.maxIdle = maxIdle;
3782
-
3783
- /** @type {Map<string, number[]>} */
3784
- this.userData = new Map();
3863
+ if (typeof maxMemory === 'number' && Number.isFinite(maxMemory) && maxMemory > 0) {
3864
+ this.#maxMemory = Math.floor(maxMemory);
3865
+ } else if (maxMemory === null || maxMemory === undefined) {
3866
+ this.#maxMemory = null;
3867
+ } else {
3868
+ throw new Error('maxMemory must be a positive number or null');
3869
+ }
3785
3870
 
3786
- /** @type {Map<string, number>} */
3787
- this.lastSeen = new Map();
3871
+ this.#maxHits = isMaxHitsValid ? maxHits : null;
3872
+ this.#interval = isIntervalValid ? interval : null;
3873
+ this.#cleanupInterval = isCleanupValid ? cleanupInterval : null;
3874
+ this.#maxIdle = maxIdle;
3788
3875
 
3789
3876
  // Start automatic cleanup only if cleanupInterval is valid
3790
- if (this.cleanupInterval !== null)
3791
- this._cleanupTimer = setInterval(() => this._cleanup(), this.cleanupInterval);
3877
+ if (this.#cleanupInterval !== null)
3878
+ this.#cleanupTimer = setInterval(() => this._cleanup(), this.#cleanupInterval);
3792
3879
  }
3793
3880
 
3794
3881
  /**
3795
- * Get the interval window in milliseconds.
3796
- *
3797
- * @returns {number} The interval value.
3798
- * @throws {Error} If interval is not a valid finite number.
3882
+ * Check if a given ID is a groupId (not a userId)
3883
+ * @param {string} id
3884
+ * @returns {boolean}
3799
3885
  */
3800
- getInterval() {
3801
- if (typeof this.interval !== 'number' || !Number.isFinite(this.interval))
3802
- throw new Error("'interval' is not a valid finite number.");
3803
- return this.interval;
3886
+ isGroupId(id) {
3887
+ const result = this.groupFlags.get(id);
3888
+ return typeof result === 'boolean' ? result : false;
3804
3889
  }
3805
3890
 
3806
3891
  /**
3807
- * Get the maximum number of allowed hits.
3808
- *
3809
- * @returns {number} The maxHits value.
3810
- * @throws {Error} If maxHits is not a valid finite number.
3892
+ * Get all user IDs that belong to a given group.
3893
+ * @param {string} groupId
3894
+ * @returns {string[]}
3811
3895
  */
3812
- getMaxHits() {
3813
- if (typeof this.maxHits !== 'number' || !Number.isFinite(this.maxHits)) {
3814
- throw new Error("'maxHits' is not a valid finite number.");
3896
+ getUsersInGroup(groupId) {
3897
+ const users = [];
3898
+ for (const [userId, assignedGroup] of this.userToGroup.entries()) {
3899
+ if (assignedGroup === groupId) {
3900
+ users.push(userId);
3901
+ }
3902
+ }
3903
+
3904
+ return users;
3905
+ }
3906
+
3907
+ /**
3908
+ * Set TTL (in milliseconds) for a specific group
3909
+ * @param {string} groupId
3910
+ * @param {number} ttl
3911
+ */
3912
+ setGroupTTL(groupId, ttl) {
3913
+ if (typeof ttl !== 'number' || !Number.isFinite(ttl) || ttl <= 0)
3914
+ throw new Error('TTL must be a positive number in milliseconds');
3915
+ this.groupTTL.set(groupId, ttl);
3916
+ }
3917
+
3918
+ /**
3919
+ * Get TTL (in ms) for a specific group.
3920
+ * @param {string} groupId
3921
+ * @returns {number|null}
3922
+ */
3923
+ getGroupTTL(groupId) {
3924
+ return this.groupTTL.get(groupId) ?? null;
3925
+ }
3926
+
3927
+ /**
3928
+ * Delete the TTL setting for a specific group
3929
+ * @param {string} groupId
3930
+ */
3931
+ deleteGroupTTL(groupId) {
3932
+ this.groupTTL.delete(groupId);
3933
+ }
3934
+
3935
+ /**
3936
+ * Assign a userId to a groupId, with merge if user has existing data.
3937
+ * @param {string} userId
3938
+ * @param {string} groupId
3939
+ * @throws {Error} If userId is already assigned to a different group
3940
+ */
3941
+ assignToGroup(userId, groupId) {
3942
+ const existingGroup = this.userToGroup.get(userId);
3943
+ if (existingGroup && existingGroup !== groupId)
3944
+ throw new Error(`User ${userId} is already assigned to group ${existingGroup}`);
3945
+
3946
+ // If the user is already in the group, nothing needs to be done
3947
+ if (existingGroup === groupId) return;
3948
+ const userData = this.groupData.get(userId);
3949
+
3950
+ // Associates the user to the group
3951
+ if (this.isGroupId(userId)) {
3952
+ for (const [uid, gId] of this.userToGroup.entries())
3953
+ if (gId === userId) this.userToGroup.set(uid, groupId);
3954
+ this.userToGroup.delete(userId);
3955
+ } else this.userToGroup.set(userId, groupId);
3956
+
3957
+ // If the user has no data, nothing needs to be done
3958
+ if (!userData) return;
3959
+
3960
+ const groupData = this.groupData.get(groupId);
3961
+ if (groupData) {
3962
+ for (const item of userData) groupData.push(item);
3963
+ } else {
3964
+ const newData = [];
3965
+ for (const item of userData) newData.push(item);
3966
+ this.groupData.set(groupId, newData);
3815
3967
  }
3816
- return this.maxHits;
3968
+
3969
+ this.lastSeen.set(groupId, Date.now());
3970
+
3971
+ // Removes individual data as they are now in the group
3972
+ this.groupFlags.delete(userId);
3973
+ this.groupData.delete(userId);
3974
+ this.lastSeen.delete(userId);
3975
+ this.groupTTL.delete(userId);
3976
+ this.groupFlags.set(groupId, true);
3977
+ }
3978
+
3979
+ /**
3980
+ * Get the groupId for a given userId
3981
+ * @param {string} userId
3982
+ * @returns {string}
3983
+ */
3984
+ getGroupId(userId) {
3985
+ return this.userToGroup.get(userId) || userId; // fallback: use userId as own group
3817
3986
  }
3818
3987
 
3819
3988
  /**
@@ -3821,20 +3990,22 @@ class TinyRateLimiter {
3821
3990
  * @param {string} userId
3822
3991
  */
3823
3992
  hit(userId) {
3993
+ const groupId = this.getGroupId(userId);
3824
3994
  const now = Date.now();
3825
3995
 
3826
- if (!this.userData.has(userId)) {
3827
- this.userData.set(userId, []);
3996
+ if (!this.groupData.has(groupId)) {
3997
+ this.groupData.set(groupId, []);
3998
+ this.groupFlags.set(groupId, false);
3828
3999
  }
3829
4000
 
3830
- const history = this.userData.get(userId);
3831
- if (!history) throw new Error(`No data found for userId: ${userId}`);
4001
+ const history = this.groupData.get(groupId);
4002
+ if (!history) throw new Error(`No data found for groupId: ${groupId}`);
3832
4003
 
3833
4004
  history.push(now);
3834
- this.lastSeen.set(userId, now);
4005
+ this.lastSeen.set(groupId, now);
3835
4006
 
3836
4007
  // Clean up old entries
3837
- if (this.interval !== null) {
4008
+ if (this.#interval !== null) {
3838
4009
  const interval = this.getInterval();
3839
4010
  const cutoff = now - interval;
3840
4011
  while (history.length && history[0] < cutoff) {
@@ -3843,92 +4014,295 @@ class TinyRateLimiter {
3843
4014
  }
3844
4015
 
3845
4016
  // Optional: keep only the last N entries for memory optimization
3846
- if (this.maxHits !== null) {
3847
- const maxHits = this.getMaxHits();
3848
- if (history.length > maxHits) {
3849
- history.splice(0, history.length - maxHits);
4017
+ if (this.#maxMemory !== null && typeof this.#maxMemory === 'number') {
4018
+ if (history.length > this.#maxMemory) {
4019
+ history.splice(0, history.length - this.#maxMemory);
4020
+ if (typeof this.#onMemoryExceeded === 'function') this.#onMemoryExceeded(groupId);
3850
4021
  }
3851
4022
  }
3852
4023
  }
3853
4024
 
3854
4025
  /**
3855
- * Check if the user is currently rate limited
4026
+ * Check if the user (via their group) is currently rate limited
3856
4027
  * @param {string} userId
3857
4028
  * @returns {boolean}
3858
4029
  */
3859
4030
  isRateLimited(userId) {
3860
- const now = Date.now();
3861
-
3862
- if (!this.userData.has(userId)) return false;
4031
+ const groupId = this.getGroupId(userId);
4032
+ if (!this.groupData.has(groupId)) return false;
3863
4033
 
3864
- const history = this.userData.get(userId);
3865
- if (!history) throw new Error(`No data found for userId: ${userId}`);
4034
+ const history = this.groupData.get(groupId);
4035
+ if (!history) throw new Error(`No data found for groupId: ${groupId}`);
3866
4036
 
3867
- if (this.interval !== null) {
4037
+ if (this.#interval !== null) {
4038
+ const now = Date.now();
3868
4039
  const interval = this.getInterval();
3869
- const recent = history.filter((t) => t > now - interval);
3870
- if (this.maxHits !== null) {
3871
- return recent.length >= this.getMaxHits();
4040
+ const cutoff = now - interval;
4041
+ let count = 0;
4042
+ for (let i = history.length - 1; i >= 0; i--) {
4043
+ if (history[i] > cutoff) count++;
4044
+ else break;
3872
4045
  }
3873
- return recent.length > 0;
4046
+ if (this.#maxHits !== null) return count > this.getMaxHits();
4047
+ return count > 0;
3874
4048
  }
3875
4049
 
3876
- if (this.maxHits !== null) {
3877
- return history.length >= this.getMaxHits();
4050
+ if (this.#maxHits !== null) {
4051
+ return history.length > this.getMaxHits();
3878
4052
  }
3879
4053
 
3880
4054
  return false;
3881
4055
  }
3882
4056
 
3883
4057
  /**
3884
- * Manually reset user data
4058
+ * Manually reset group data
4059
+ * @param {string} groupId
4060
+ */
4061
+ resetGroup(groupId) {
4062
+ this.groupFlags.delete(groupId);
4063
+ this.groupData.delete(groupId);
4064
+ this.lastSeen.delete(groupId);
4065
+ this.groupTTL.delete(groupId);
4066
+ }
4067
+
4068
+ /**
4069
+ * Manually reset user data.
4070
+ *
4071
+ * @deprecated Use `resetUserGroup(userId)` instead. This method will be removed in future versions.
3885
4072
  * @param {string} userId
4073
+ * @returns {void}
3886
4074
  */
3887
4075
  reset(userId) {
3888
- this.userData.delete(userId);
3889
- this.lastSeen.delete(userId);
4076
+ if (false)
4077
+ {}
4078
+ return this.resetUserGroup(userId);
3890
4079
  }
3891
4080
 
3892
4081
  /**
3893
- * Set hit timestamps for a user
4082
+ * Manually reset a user mapping
3894
4083
  * @param {string} userId
4084
+ */
4085
+ resetUserGroup(userId) {
4086
+ this.userToGroup.delete(userId);
4087
+ }
4088
+
4089
+ /**
4090
+ * Set custom timestamps to a group
4091
+ * @param {string} groupId
3895
4092
  * @param {number[]} timestamps
3896
4093
  */
3897
- setData(userId, timestamps) {
3898
- this.userData.set(userId, timestamps);
3899
- this.lastSeen.set(userId, Date.now());
4094
+ setData(groupId, timestamps) {
4095
+ if (!Array.isArray(timestamps)) throw new Error('timestamps must be an array of numbers.');
4096
+ for (const t of timestamps) {
4097
+ if (typeof t !== 'number' || !Number.isFinite(t)) {
4098
+ throw new Error('All timestamps must be finite numbers.');
4099
+ }
4100
+ }
4101
+ if (!this.groupData.has(groupId)) this.groupFlags.set(groupId, false);
4102
+ this.groupData.set(groupId, timestamps);
4103
+ this.lastSeen.set(groupId, Date.now());
3900
4104
  }
3901
4105
 
3902
4106
  /**
3903
- * Get timestamps from user
3904
- * @param {string} userId
4107
+ * Check if a group has data
4108
+ * @param {string} groupId
4109
+ * @returns {boolean}
4110
+ */
4111
+ hasData(groupId) {
4112
+ return this.groupData.has(groupId);
4113
+ }
4114
+
4115
+ /**
4116
+ * Get timestamps from a group
4117
+ * @param {string} groupId
3905
4118
  * @returns {number[]}
3906
4119
  */
3907
- getData(userId) {
3908
- return this.userData.get(userId) || [];
4120
+ getData(groupId) {
4121
+ return this.groupData.get(groupId) || [];
4122
+ }
4123
+
4124
+ /**
4125
+ * Get the maximum idle time (in milliseconds) before a group is considered expired.
4126
+ * @returns {number}
4127
+ */
4128
+ getMaxIdle() {
4129
+ if (typeof this.#maxIdle !== 'number' || !Number.isFinite(this.#maxIdle) || this.#maxIdle < 0) {
4130
+ throw new Error("'maxIdle' must be a non-negative finite number.");
4131
+ }
4132
+ return this.#maxIdle;
4133
+ }
4134
+
4135
+ /**
4136
+ * Set the maximum idle time (in milliseconds) before a group is considered expired.
4137
+ * @param {number} ms
4138
+ */
4139
+ setMaxIdle(ms) {
4140
+ if (typeof ms !== 'number' || !Number.isFinite(ms) || ms < 0) {
4141
+ throw new Error("'maxIdle' must be a non-negative finite number.");
4142
+ }
4143
+ this.#maxIdle = ms;
3909
4144
  }
3910
4145
 
3911
4146
  /**
3912
- * Cleanup old/inactive users
4147
+ * Cleanup old/inactive groups with individual TTLs
3913
4148
  * @private
3914
4149
  */
3915
4150
  _cleanup() {
3916
4151
  const now = Date.now();
3917
- for (const [userId, last] of this.lastSeen.entries()) {
3918
- if (now - last > this.maxIdle) {
3919
- this.userData.delete(userId);
3920
- this.lastSeen.delete(userId);
4152
+ for (const [groupId, last] of this.lastSeen.entries()) {
4153
+ const ttl = this.getGroupTTL(groupId) ?? this.getMaxIdle();
4154
+ if (now - last > ttl) {
4155
+ this.groupFlags.delete(groupId);
4156
+ this.groupData.delete(groupId);
4157
+ this.lastSeen.delete(groupId);
4158
+ this.groupTTL.delete(groupId);
4159
+
4160
+ // Notify subclass or external binding
4161
+ if (typeof this.#onGroupExpired === 'function') {
4162
+ this.#onGroupExpired(groupId);
4163
+ }
3921
4164
  }
3922
4165
  }
3923
4166
  }
3924
4167
 
3925
4168
  /**
3926
- * Destroy the rate limiter, stopping all intervals
4169
+ * Get list of active group IDs
4170
+ * @returns {string[]}
4171
+ */
4172
+ getActiveGroups() {
4173
+ return Array.from(this.groupData.keys());
4174
+ }
4175
+
4176
+ /**
4177
+ * Get a shallow copy of all user-to-group mappings as a plain object
4178
+ * @returns {Record<string, string>}
4179
+ */
4180
+ getAllUserMappings() {
4181
+ return Object.fromEntries(this.userToGroup);
4182
+ }
4183
+
4184
+ /**
4185
+ * Get the interval window in milliseconds.
4186
+ * @returns {number}
4187
+ */
4188
+ getInterval() {
4189
+ if (typeof this.#interval !== 'number' || !Number.isFinite(this.#interval)) {
4190
+ throw new Error("'interval' is not a valid finite number.");
4191
+ }
4192
+ return this.#interval;
4193
+ }
4194
+
4195
+ /**
4196
+ * Get the maximum number of allowed hits.
4197
+ * @returns {number}
4198
+ */
4199
+ getMaxHits() {
4200
+ if (typeof this.#maxHits !== 'number' || !Number.isFinite(this.#maxHits)) {
4201
+ throw new Error("'maxHits' is not a valid finite number.");
4202
+ }
4203
+ return this.#maxHits;
4204
+ }
4205
+
4206
+ /**
4207
+ * Get the total number of hits recorded for a group.
4208
+ * @param {string} groupId
4209
+ * @returns {number}
4210
+ */
4211
+ getTotalHits(groupId) {
4212
+ const history = this.groupData.get(groupId);
4213
+ return Array.isArray(history) ? history.length : 0;
4214
+ }
4215
+
4216
+ /**
4217
+ * Get the timestamp of the last hit for a group.
4218
+ * @param {string} groupId
4219
+ * @returns {number|null}
4220
+ */
4221
+ getLastHit(groupId) {
4222
+ const history = this.groupData.get(groupId);
4223
+ return history?.length ? history[history.length - 1] : null;
4224
+ }
4225
+
4226
+ /**
4227
+ * Get milliseconds since the last hit for a group.
4228
+ * @param {string} groupId
4229
+ * @returns {number|null}
4230
+ */
4231
+ getTimeSinceLastHit(groupId) {
4232
+ const last = this.getLastHit(groupId);
4233
+ return last !== null ? Date.now() - last : null;
4234
+ }
4235
+
4236
+ /**
4237
+ * Internal utility to compute average spacing
4238
+ * @private
4239
+ * @param {number[]|undefined} history
4240
+ * @returns {number|null}
4241
+ */
4242
+ _calculateAverageSpacing(history) {
4243
+ if (!Array.isArray(history) || history.length < 2) return null;
4244
+ let total = 0;
4245
+ for (let i = 1; i < history.length; i++) {
4246
+ total += history[i] - history[i - 1];
4247
+ }
4248
+ return total / (history.length - 1);
4249
+ }
4250
+
4251
+ /**
4252
+ * Get average time between hits for a group (ms).
4253
+ * @param {string} groupId
4254
+ * @returns {number|null}
4255
+ */
4256
+ getAverageHitSpacing(groupId) {
4257
+ return this._calculateAverageSpacing(this.groupData.get(groupId));
4258
+ }
4259
+
4260
+ /**
4261
+ * Get metrics about a group's activity.
4262
+ * @param {string} groupId
4263
+ * @returns {{
4264
+ * totalHits: number,
4265
+ * lastHit: number|null,
4266
+ * timeSinceLastHit: number|null,
4267
+ * averageHitSpacing: number|null
4268
+ * }}
4269
+ */
4270
+ getMetrics(groupId) {
4271
+ const history = this.groupData.get(groupId);
4272
+
4273
+ if (!Array.isArray(history) || history.length === 0) {
4274
+ return {
4275
+ totalHits: 0,
4276
+ lastHit: null,
4277
+ timeSinceLastHit: null,
4278
+ averageHitSpacing: null,
4279
+ };
4280
+ }
4281
+
4282
+ const totalHits = history.length;
4283
+ const lastHit = history[totalHits - 1];
4284
+ const timeSinceLastHit = Date.now() - lastHit;
4285
+ const averageHitSpacing = this._calculateAverageSpacing(history);
4286
+
4287
+ return {
4288
+ totalHits,
4289
+ lastHit,
4290
+ timeSinceLastHit,
4291
+ averageHitSpacing,
4292
+ };
4293
+ }
4294
+
4295
+ /**
4296
+ * Destroy the rate limiter, stopping cleanup and clearing data
3927
4297
  */
3928
4298
  destroy() {
3929
- if (this._cleanupTimer) clearInterval(this._cleanupTimer);
3930
- this.userData.clear();
4299
+ if (this.#cleanupTimer) clearInterval(this.#cleanupTimer);
4300
+ this._cleanup();
4301
+ this.groupData.clear();
3931
4302
  this.lastSeen.clear();
4303
+ this.userToGroup.clear();
4304
+ this.groupTTL.clear();
4305
+ this.groupFlags.clear();
3932
4306
  }
3933
4307
  }
3934
4308