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.
- package/dist/TinyBasicsEs.js +155 -98
- package/dist/TinyBasicsEs.min.js +1 -1
- package/dist/TinyEssentials.js +605 -174
- package/dist/TinyEssentials.min.js +1 -1
- package/dist/TinyRateLimiter.js +450 -76
- package/dist/TinyRateLimiter.min.js +1 -1
- package/dist/legacy/firebase/mySQL.cjs +1 -1
- package/dist/v1/basics/objFilter.cjs +155 -98
- package/dist/v1/basics/objFilter.d.mts +16 -26
- package/dist/v1/basics/objFilter.mjs +146 -77
- package/dist/v1/libs/TinyRateLimiter.cjs +450 -76
- package/dist/v1/libs/TinyRateLimiter.d.mts +186 -31
- package/dist/v1/libs/TinyRateLimiter.mjs +420 -75
- package/docs/basics/objFilter.md +7 -0
- package/docs/libs/TinyRateLimiter.md +167 -15
- package/package.json +1 -1
package/dist/TinyEssentials.js
CHANGED
|
@@ -2727,6 +2727,8 @@ var buffer = __webpack_require__(287);
|
|
|
2727
2727
|
;// ./src/v1/basics/objFilter.mjs
|
|
2728
2728
|
|
|
2729
2729
|
|
|
2730
|
+
const isBrowser = typeof window !== 'undefined' && typeof window.document !== 'undefined';
|
|
2731
|
+
|
|
2730
2732
|
/**
|
|
2731
2733
|
* An object containing type validation functions and their evaluation order.
|
|
2732
2734
|
*
|
|
@@ -2736,111 +2738,27 @@ var buffer = __webpack_require__(287);
|
|
|
2736
2738
|
* The `order` array defines the priority in which types should be checked,
|
|
2737
2739
|
* which can be useful for functions that infer types in a consistent manner.
|
|
2738
2740
|
*
|
|
2739
|
-
* @typedef {Object} TypeValidator
|
|
2740
|
-
* @property {Object.<string, (val: any) => boolean>} items - A dictionary of type validation functions.
|
|
2741
|
-
* @property {string[]} order - The order in which types should be evaluated.
|
|
2742
|
-
*/
|
|
2743
|
-
|
|
2744
|
-
/**
|
|
2745
|
-
* Validates values against specific types using predefined functions.
|
|
2746
|
-
*
|
|
2747
|
-
* @type {TypeValidator}
|
|
2748
2741
|
*/
|
|
2749
2742
|
const typeValidator = {
|
|
2750
|
-
items: {
|
|
2751
|
-
|
|
2752
|
-
|
|
2753
|
-
|
|
2754
|
-
|
|
2755
|
-
|
|
2756
|
-
|
|
2757
|
-
/** @param {*} val @returns {val is boolean} */
|
|
2758
|
-
boolean: (val) => typeof val === 'boolean',
|
|
2759
|
-
|
|
2760
|
-
/** @param {*} val @returns {val is number} */
|
|
2761
|
-
number: (val) => typeof val === 'number' && !isNaN(val),
|
|
2762
|
-
|
|
2763
|
-
/** @param {*} val @returns {val is bigint} */
|
|
2764
|
-
bigint: (val) => typeof val === 'bigint',
|
|
2765
|
-
|
|
2766
|
-
/** @param {*} val @returns {val is string} */
|
|
2767
|
-
string: (val) => typeof val === 'string',
|
|
2768
|
-
|
|
2769
|
-
/** @param {*} val @returns {val is symbol} */
|
|
2770
|
-
symbol: (val) => typeof val === 'symbol',
|
|
2771
|
-
|
|
2772
|
-
/** @param {*} val @returns {val is Function} */
|
|
2773
|
-
function: (val) => typeof val === 'function',
|
|
2774
|
-
|
|
2775
|
-
/** @param {*} val @returns {val is Array} */
|
|
2776
|
-
array: (val) => Array.isArray(val),
|
|
2777
|
-
|
|
2778
|
-
/** @param {*} val @returns {val is Date} */
|
|
2779
|
-
date: (val) => val instanceof Date,
|
|
2780
|
-
|
|
2781
|
-
/** @param {*} val @returns {val is RegExp} */
|
|
2782
|
-
regexp: (val) => val instanceof RegExp,
|
|
2783
|
-
|
|
2784
|
-
/** @param {*} val @returns {val is Map} */
|
|
2785
|
-
map: (val) => val instanceof Map,
|
|
2786
|
-
|
|
2787
|
-
/** @param {*} val @returns {val is Set} */
|
|
2788
|
-
set: (val) => val instanceof Set,
|
|
2789
|
-
|
|
2790
|
-
/** @param {*} val @returns {val is WeakMap} */
|
|
2791
|
-
weakmap: (val) => val instanceof WeakMap,
|
|
2792
|
-
|
|
2793
|
-
/** @param {*} val @returns {val is WeakSet} */
|
|
2794
|
-
weakset: (val) => val instanceof WeakSet,
|
|
2795
|
-
|
|
2796
|
-
/** @param {*} val @returns {val is Promise} */
|
|
2797
|
-
promise: (val) => val instanceof Promise,
|
|
2798
|
-
|
|
2799
|
-
/** @param {*} val @returns {val is Buffer} */
|
|
2800
|
-
buffer: (val) => typeof buffer/* Buffer */.hp !== 'undefined' && buffer/* Buffer */.hp.isBuffer(val),
|
|
2801
|
-
|
|
2802
|
-
/** @param {*} val @returns {val is File} */
|
|
2803
|
-
file: (val) => typeof File !== 'undefined' && val instanceof File,
|
|
2804
|
-
|
|
2805
|
-
/** @param {*} val @returns {val is HTMLElement} */
|
|
2806
|
-
htmlelement: (val) => typeof HTMLElement !== 'undefined' && val instanceof HTMLElement,
|
|
2807
|
-
|
|
2808
|
-
/** @param {*} val @returns {val is object} */
|
|
2809
|
-
object: (val) => typeof val === 'object' && val !== null && !Array.isArray(val),
|
|
2810
|
-
},
|
|
2811
|
-
|
|
2812
|
-
/** Evaluation order of the type checkers. */
|
|
2813
|
-
order: [
|
|
2814
|
-
'undefined',
|
|
2815
|
-
'null',
|
|
2816
|
-
'boolean',
|
|
2817
|
-
'number',
|
|
2818
|
-
'bigint',
|
|
2819
|
-
'string',
|
|
2820
|
-
'symbol',
|
|
2821
|
-
'function',
|
|
2822
|
-
'array',
|
|
2823
|
-
'buffer',
|
|
2824
|
-
'file',
|
|
2825
|
-
'date',
|
|
2826
|
-
'regexp',
|
|
2827
|
-
'map',
|
|
2828
|
-
'set',
|
|
2829
|
-
'weakmap',
|
|
2830
|
-
'weakset',
|
|
2831
|
-
'promise',
|
|
2832
|
-
'htmlelement',
|
|
2833
|
-
'object',
|
|
2834
|
-
],
|
|
2743
|
+
items: {},
|
|
2744
|
+
/**
|
|
2745
|
+
* Evaluation order of the type checkers.
|
|
2746
|
+
* @type {string[]}
|
|
2747
|
+
* */
|
|
2748
|
+
order: [],
|
|
2835
2749
|
};
|
|
2836
2750
|
|
|
2751
|
+
/** @typedef {Object.<string, (val: any) => *>} ExtendObjType */
|
|
2752
|
+
/** @typedef {Array<[string, (val: any) => *]>} ExtendObjTypeArray */
|
|
2753
|
+
|
|
2837
2754
|
/**
|
|
2838
2755
|
* Adds new type checkers to the typeValidator without overwriting existing ones.
|
|
2839
2756
|
*
|
|
2840
|
-
*
|
|
2757
|
+
* Accepts either an object with named functions or an array of [key, fn] arrays.
|
|
2841
2758
|
* If no index is provided, the type is inserted just before 'object' (if it exists), or at the end.
|
|
2842
2759
|
*
|
|
2843
|
-
* @param {
|
|
2760
|
+
* @param {ExtendObjType|ExtendObjTypeArray} newItems
|
|
2761
|
+
* - New type validators to be added.
|
|
2844
2762
|
* @param {number} [index] - Optional. Position at which to insert each new type. Ignored if the type already exists.
|
|
2845
2763
|
* @returns {string[]} - A list of successfully added type names.
|
|
2846
2764
|
*
|
|
@@ -2848,12 +2766,20 @@ const typeValidator = {
|
|
|
2848
2766
|
* extendObjType({
|
|
2849
2767
|
* htmlElement2: val => typeof HTMLElement !== 'undefined' && val instanceof HTMLElement
|
|
2850
2768
|
* });
|
|
2769
|
+
*
|
|
2770
|
+
* @example
|
|
2771
|
+
* extendObjType([
|
|
2772
|
+
* ['alpha', val => typeof val === 'string'],
|
|
2773
|
+
* ['beta', val => Array.isArray(val)]
|
|
2774
|
+
* ]);
|
|
2851
2775
|
*/
|
|
2852
2776
|
function extendObjType(newItems, index) {
|
|
2853
2777
|
const added = [];
|
|
2854
2778
|
|
|
2855
|
-
|
|
2779
|
+
const entries = Array.isArray(newItems) ? newItems : Object.entries(newItems);
|
|
2780
|
+
for (const [key, fn] of entries) {
|
|
2856
2781
|
if (!typeValidator.items.hasOwnProperty(key)) {
|
|
2782
|
+
// @ts-ignore
|
|
2857
2783
|
typeValidator.items[key] = fn;
|
|
2858
2784
|
|
|
2859
2785
|
let insertAt = typeof index === 'number' ? index : -1; // Default to -1 if index isn't provided
|
|
@@ -2931,9 +2857,12 @@ function cloneObjTypeOrder() {
|
|
|
2931
2857
|
*/
|
|
2932
2858
|
const getType = (val) => {
|
|
2933
2859
|
if (val === null) return 'null';
|
|
2934
|
-
|
|
2860
|
+
// @ts-ignore
|
|
2861
|
+
for (const name of typeValidator.order) {
|
|
2862
|
+
// @ts-ignore
|
|
2935
2863
|
if (typeof typeValidator.items[name] !== 'function' || typeValidator.items[name](val))
|
|
2936
2864
|
return name;
|
|
2865
|
+
}
|
|
2937
2866
|
return 'unknown';
|
|
2938
2867
|
};
|
|
2939
2868
|
|
|
@@ -2968,7 +2897,9 @@ function checkObj(obj) {
|
|
|
2968
2897
|
/** @type {{ valid:*; type: string | null }} */
|
|
2969
2898
|
const data = { valid: null, type: null };
|
|
2970
2899
|
for (const name of typeValidator.order) {
|
|
2900
|
+
// @ts-ignore
|
|
2971
2901
|
if (typeof typeValidator.items[name] === 'function') {
|
|
2902
|
+
// @ts-ignore
|
|
2972
2903
|
const result = typeValidator.items[name](obj);
|
|
2973
2904
|
if (result) {
|
|
2974
2905
|
data.valid = result;
|
|
@@ -3008,6 +2939,132 @@ function countObj(obj) {
|
|
|
3008
2939
|
return 0;
|
|
3009
2940
|
}
|
|
3010
2941
|
|
|
2942
|
+
// Insert obj types
|
|
2943
|
+
|
|
2944
|
+
extendObjType([
|
|
2945
|
+
[
|
|
2946
|
+
'undefined',
|
|
2947
|
+
/** @param {*} val @returns {val is undefined} */
|
|
2948
|
+
(val) => typeof val === 'undefined',
|
|
2949
|
+
],
|
|
2950
|
+
[
|
|
2951
|
+
'null',
|
|
2952
|
+
/** @param {*} val @returns {val is null} */
|
|
2953
|
+
(val) => val === null,
|
|
2954
|
+
],
|
|
2955
|
+
[
|
|
2956
|
+
'boolean',
|
|
2957
|
+
/** @param {*} val @returns {val is boolean} */
|
|
2958
|
+
(val) => typeof val === 'boolean',
|
|
2959
|
+
],
|
|
2960
|
+
[
|
|
2961
|
+
'number',
|
|
2962
|
+
/** @param {*} val @returns {val is number} */
|
|
2963
|
+
(val) => typeof val === 'number' && !Number.isNaN(val),
|
|
2964
|
+
],
|
|
2965
|
+
[
|
|
2966
|
+
'bigint',
|
|
2967
|
+
/** @param {*} val @returns {val is bigint} */
|
|
2968
|
+
(val) => typeof val === 'bigint',
|
|
2969
|
+
],
|
|
2970
|
+
[
|
|
2971
|
+
'string',
|
|
2972
|
+
/** @param {*} val @returns {val is string} */
|
|
2973
|
+
(val) => typeof val === 'string',
|
|
2974
|
+
],
|
|
2975
|
+
[
|
|
2976
|
+
'symbol',
|
|
2977
|
+
/** @param {*} val @returns {val is symbol} */
|
|
2978
|
+
(val) => typeof val === 'symbol',
|
|
2979
|
+
],
|
|
2980
|
+
[
|
|
2981
|
+
'function',
|
|
2982
|
+
/** @param {*} val @returns {val is Function} */
|
|
2983
|
+
(val) => typeof val === 'function',
|
|
2984
|
+
],
|
|
2985
|
+
[
|
|
2986
|
+
'array',
|
|
2987
|
+
/** @param {*} val @returns {val is any[]} */
|
|
2988
|
+
(val) => Array.isArray(val),
|
|
2989
|
+
],
|
|
2990
|
+
]);
|
|
2991
|
+
|
|
2992
|
+
if (!isBrowser) {
|
|
2993
|
+
extendObjType([
|
|
2994
|
+
[
|
|
2995
|
+
'buffer',
|
|
2996
|
+
/** @param {*} val @returns {val is Buffer} */
|
|
2997
|
+
(val) => typeof buffer/* Buffer */.hp !== 'undefined' && buffer/* Buffer */.hp.isBuffer(val),
|
|
2998
|
+
],
|
|
2999
|
+
]);
|
|
3000
|
+
}
|
|
3001
|
+
|
|
3002
|
+
if (isBrowser) {
|
|
3003
|
+
extendObjType([
|
|
3004
|
+
[
|
|
3005
|
+
'file',
|
|
3006
|
+
/** @param {*} val @returns {val is File} */
|
|
3007
|
+
(val) => typeof File !== 'undefined' && val instanceof File,
|
|
3008
|
+
],
|
|
3009
|
+
]);
|
|
3010
|
+
}
|
|
3011
|
+
|
|
3012
|
+
extendObjType([
|
|
3013
|
+
[
|
|
3014
|
+
'date',
|
|
3015
|
+
/** @param {*} val @returns {val is Date} */
|
|
3016
|
+
(val) => val instanceof Date,
|
|
3017
|
+
],
|
|
3018
|
+
[
|
|
3019
|
+
'regexp',
|
|
3020
|
+
/** @param {*} val @returns {val is RegExp} */
|
|
3021
|
+
(val) => val instanceof RegExp,
|
|
3022
|
+
],
|
|
3023
|
+
[
|
|
3024
|
+
'map',
|
|
3025
|
+
/** @param {*} val @returns {val is Map<any, any>} */
|
|
3026
|
+
(val) => val instanceof Map,
|
|
3027
|
+
],
|
|
3028
|
+
[
|
|
3029
|
+
'set',
|
|
3030
|
+
/** @param {*} val @returns {val is Set<any>} */
|
|
3031
|
+
(val) => val instanceof Set,
|
|
3032
|
+
],
|
|
3033
|
+
[
|
|
3034
|
+
'weakmap',
|
|
3035
|
+
/** @param {*} val @returns {val is WeakMap<any, any>} */
|
|
3036
|
+
(val) => val instanceof WeakMap,
|
|
3037
|
+
],
|
|
3038
|
+
[
|
|
3039
|
+
'weakset',
|
|
3040
|
+
/** @param {*} val @returns {val is WeakSet<any>} */
|
|
3041
|
+
(val) => val instanceof WeakSet,
|
|
3042
|
+
],
|
|
3043
|
+
[
|
|
3044
|
+
'promise',
|
|
3045
|
+
/** @param {*} val @returns {val is Promise<any>} */
|
|
3046
|
+
(val) => val instanceof Promise,
|
|
3047
|
+
],
|
|
3048
|
+
]);
|
|
3049
|
+
|
|
3050
|
+
if (isBrowser) {
|
|
3051
|
+
extendObjType([
|
|
3052
|
+
[
|
|
3053
|
+
'htmlelement',
|
|
3054
|
+
/** @param {*} val @returns {val is HTMLElement} */
|
|
3055
|
+
(val) => typeof HTMLElement !== 'undefined' && val instanceof HTMLElement,
|
|
3056
|
+
],
|
|
3057
|
+
]);
|
|
3058
|
+
}
|
|
3059
|
+
|
|
3060
|
+
extendObjType([
|
|
3061
|
+
[
|
|
3062
|
+
'object',
|
|
3063
|
+
/** @param {*} val @returns {val is object} */
|
|
3064
|
+
(val) => typeof val === 'object' && val !== null && !Array.isArray(val),
|
|
3065
|
+
],
|
|
3066
|
+
]);
|
|
3067
|
+
|
|
3011
3068
|
;// ./src/v1/basics/simpleMath.mjs
|
|
3012
3069
|
/**
|
|
3013
3070
|
* Executes a Rule of Three calculation.
|
|
@@ -3681,24 +3738,109 @@ class TinyPromiseQueue {
|
|
|
3681
3738
|
/* harmony default export */ const libs_TinyPromiseQueue = (TinyPromiseQueue);
|
|
3682
3739
|
|
|
3683
3740
|
;// ./src/v1/libs/TinyRateLimiter.mjs
|
|
3741
|
+
/** @typedef {(groupId: string) => void} OnMemoryExceeded */
|
|
3742
|
+
|
|
3743
|
+
/** @typedef {(groupId: string) => void} OnGroupExpired */
|
|
3744
|
+
|
|
3684
3745
|
/**
|
|
3685
|
-
* Class representing a flexible rate limiter per user.
|
|
3746
|
+
* Class representing a flexible rate limiter per user or group.
|
|
3686
3747
|
*
|
|
3687
|
-
* This rate limiter
|
|
3688
|
-
*
|
|
3689
|
-
*
|
|
3690
|
-
*
|
|
3691
|
-
* @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.
|
|
3692
3751
|
*/
|
|
3693
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
|
+
|
|
3694
3835
|
/**
|
|
3695
3836
|
* @param {Object} options
|
|
3837
|
+
* @param {number|null} [options.maxMemory] - Max memory allowed
|
|
3696
3838
|
* @param {number} [options.maxHits] - Max interactions allowed
|
|
3697
3839
|
* @param {number} [options.interval] - Time window in milliseconds
|
|
3698
3840
|
* @param {number} [options.cleanupInterval] - Interval for automatic cleanup (ms)
|
|
3699
3841
|
* @param {number} [options.maxIdle=300000] - Max idle time for a user before being cleaned (ms)
|
|
3700
3842
|
*/
|
|
3701
|
-
constructor({ maxHits, interval, cleanupInterval, maxIdle = 300000 }) {
|
|
3843
|
+
constructor({ maxHits, interval, cleanupInterval, maxIdle = 300000, maxMemory = 100000 }) {
|
|
3702
3844
|
/** @param {number|undefined} val */
|
|
3703
3845
|
const isPositiveInteger = (val) =>
|
|
3704
3846
|
typeof val === 'number' && Number.isFinite(val) && val >= 1 && Number.isInteger(val);
|
|
@@ -3718,45 +3860,129 @@ class TinyRateLimiter {
|
|
|
3718
3860
|
throw new Error("'cleanupInterval' must be a positive integer in milliseconds if defined.");
|
|
3719
3861
|
if (!isMaxIdleValid) throw new Error("'maxIdle' must be a positive integer in milliseconds.");
|
|
3720
3862
|
|
|
3721
|
-
|
|
3722
|
-
|
|
3723
|
-
|
|
3724
|
-
|
|
3725
|
-
|
|
3726
|
-
|
|
3727
|
-
|
|
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
|
+
}
|
|
3728
3870
|
|
|
3729
|
-
|
|
3730
|
-
this
|
|
3871
|
+
this.#maxHits = isMaxHitsValid ? maxHits : null;
|
|
3872
|
+
this.#interval = isIntervalValid ? interval : null;
|
|
3873
|
+
this.#cleanupInterval = isCleanupValid ? cleanupInterval : null;
|
|
3874
|
+
this.#maxIdle = maxIdle;
|
|
3731
3875
|
|
|
3732
3876
|
// Start automatic cleanup only if cleanupInterval is valid
|
|
3733
|
-
if (this
|
|
3734
|
-
this
|
|
3877
|
+
if (this.#cleanupInterval !== null)
|
|
3878
|
+
this.#cleanupTimer = setInterval(() => this._cleanup(), this.#cleanupInterval);
|
|
3735
3879
|
}
|
|
3736
3880
|
|
|
3737
3881
|
/**
|
|
3738
|
-
*
|
|
3739
|
-
*
|
|
3740
|
-
* @returns {
|
|
3741
|
-
* @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}
|
|
3742
3885
|
*/
|
|
3743
|
-
|
|
3744
|
-
|
|
3745
|
-
|
|
3746
|
-
return this.interval;
|
|
3886
|
+
isGroupId(id) {
|
|
3887
|
+
const result = this.groupFlags.get(id);
|
|
3888
|
+
return typeof result === 'boolean' ? result : false;
|
|
3747
3889
|
}
|
|
3748
3890
|
|
|
3749
3891
|
/**
|
|
3750
|
-
* Get
|
|
3751
|
-
*
|
|
3752
|
-
* @returns {
|
|
3753
|
-
* @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[]}
|
|
3754
3895
|
*/
|
|
3755
|
-
|
|
3756
|
-
|
|
3757
|
-
|
|
3896
|
+
getUsersInGroup(groupId) {
|
|
3897
|
+
const users = [];
|
|
3898
|
+
for (const [userId, assignedGroup] of this.userToGroup.entries()) {
|
|
3899
|
+
if (assignedGroup === groupId) {
|
|
3900
|
+
users.push(userId);
|
|
3901
|
+
}
|
|
3758
3902
|
}
|
|
3759
|
-
|
|
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);
|
|
3967
|
+
}
|
|
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
|
|
3760
3986
|
}
|
|
3761
3987
|
|
|
3762
3988
|
/**
|
|
@@ -3764,20 +3990,22 @@ class TinyRateLimiter {
|
|
|
3764
3990
|
* @param {string} userId
|
|
3765
3991
|
*/
|
|
3766
3992
|
hit(userId) {
|
|
3993
|
+
const groupId = this.getGroupId(userId);
|
|
3767
3994
|
const now = Date.now();
|
|
3768
3995
|
|
|
3769
|
-
if (!this.
|
|
3770
|
-
this.
|
|
3996
|
+
if (!this.groupData.has(groupId)) {
|
|
3997
|
+
this.groupData.set(groupId, []);
|
|
3998
|
+
this.groupFlags.set(groupId, false);
|
|
3771
3999
|
}
|
|
3772
4000
|
|
|
3773
|
-
const history = this.
|
|
3774
|
-
if (!history) throw new Error(`No data found for
|
|
4001
|
+
const history = this.groupData.get(groupId);
|
|
4002
|
+
if (!history) throw new Error(`No data found for groupId: ${groupId}`);
|
|
3775
4003
|
|
|
3776
4004
|
history.push(now);
|
|
3777
|
-
this.lastSeen.set(
|
|
4005
|
+
this.lastSeen.set(groupId, now);
|
|
3778
4006
|
|
|
3779
4007
|
// Clean up old entries
|
|
3780
|
-
if (this
|
|
4008
|
+
if (this.#interval !== null) {
|
|
3781
4009
|
const interval = this.getInterval();
|
|
3782
4010
|
const cutoff = now - interval;
|
|
3783
4011
|
while (history.length && history[0] < cutoff) {
|
|
@@ -3786,92 +4014,295 @@ class TinyRateLimiter {
|
|
|
3786
4014
|
}
|
|
3787
4015
|
|
|
3788
4016
|
// Optional: keep only the last N entries for memory optimization
|
|
3789
|
-
if (this
|
|
3790
|
-
|
|
3791
|
-
|
|
3792
|
-
|
|
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);
|
|
3793
4021
|
}
|
|
3794
4022
|
}
|
|
3795
4023
|
}
|
|
3796
4024
|
|
|
3797
4025
|
/**
|
|
3798
|
-
* Check if the user is currently rate limited
|
|
4026
|
+
* Check if the user (via their group) is currently rate limited
|
|
3799
4027
|
* @param {string} userId
|
|
3800
4028
|
* @returns {boolean}
|
|
3801
4029
|
*/
|
|
3802
4030
|
isRateLimited(userId) {
|
|
3803
|
-
const
|
|
3804
|
-
|
|
3805
|
-
if (!this.userData.has(userId)) return false;
|
|
4031
|
+
const groupId = this.getGroupId(userId);
|
|
4032
|
+
if (!this.groupData.has(groupId)) return false;
|
|
3806
4033
|
|
|
3807
|
-
const history = this.
|
|
3808
|
-
if (!history) throw new Error(`No data found for
|
|
4034
|
+
const history = this.groupData.get(groupId);
|
|
4035
|
+
if (!history) throw new Error(`No data found for groupId: ${groupId}`);
|
|
3809
4036
|
|
|
3810
|
-
if (this
|
|
4037
|
+
if (this.#interval !== null) {
|
|
4038
|
+
const now = Date.now();
|
|
3811
4039
|
const interval = this.getInterval();
|
|
3812
|
-
const
|
|
3813
|
-
|
|
3814
|
-
|
|
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;
|
|
3815
4045
|
}
|
|
3816
|
-
return
|
|
4046
|
+
if (this.#maxHits !== null) return count > this.getMaxHits();
|
|
4047
|
+
return count > 0;
|
|
3817
4048
|
}
|
|
3818
4049
|
|
|
3819
|
-
if (this
|
|
3820
|
-
return history.length
|
|
4050
|
+
if (this.#maxHits !== null) {
|
|
4051
|
+
return history.length > this.getMaxHits();
|
|
3821
4052
|
}
|
|
3822
4053
|
|
|
3823
4054
|
return false;
|
|
3824
4055
|
}
|
|
3825
4056
|
|
|
3826
4057
|
/**
|
|
3827
|
-
* Manually reset
|
|
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.
|
|
3828
4072
|
* @param {string} userId
|
|
4073
|
+
* @returns {void}
|
|
3829
4074
|
*/
|
|
3830
4075
|
reset(userId) {
|
|
3831
|
-
|
|
3832
|
-
|
|
4076
|
+
if (false)
|
|
4077
|
+
{}
|
|
4078
|
+
return this.resetUserGroup(userId);
|
|
3833
4079
|
}
|
|
3834
4080
|
|
|
3835
4081
|
/**
|
|
3836
|
-
*
|
|
4082
|
+
* Manually reset a user mapping
|
|
3837
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
|
|
3838
4092
|
* @param {number[]} timestamps
|
|
3839
4093
|
*/
|
|
3840
|
-
setData(
|
|
3841
|
-
|
|
3842
|
-
|
|
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());
|
|
3843
4104
|
}
|
|
3844
4105
|
|
|
3845
4106
|
/**
|
|
3846
|
-
*
|
|
3847
|
-
* @param {string}
|
|
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
|
|
3848
4118
|
* @returns {number[]}
|
|
3849
4119
|
*/
|
|
3850
|
-
getData(
|
|
3851
|
-
return this.
|
|
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;
|
|
3852
4133
|
}
|
|
3853
4134
|
|
|
3854
4135
|
/**
|
|
3855
|
-
*
|
|
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;
|
|
4144
|
+
}
|
|
4145
|
+
|
|
4146
|
+
/**
|
|
4147
|
+
* Cleanup old/inactive groups with individual TTLs
|
|
3856
4148
|
* @private
|
|
3857
4149
|
*/
|
|
3858
4150
|
_cleanup() {
|
|
3859
4151
|
const now = Date.now();
|
|
3860
|
-
for (const [
|
|
3861
|
-
|
|
3862
|
-
|
|
3863
|
-
this.
|
|
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
|
+
}
|
|
3864
4164
|
}
|
|
3865
4165
|
}
|
|
3866
4166
|
}
|
|
3867
4167
|
|
|
3868
4168
|
/**
|
|
3869
|
-
*
|
|
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
|
|
3870
4297
|
*/
|
|
3871
4298
|
destroy() {
|
|
3872
|
-
if (this
|
|
3873
|
-
this.
|
|
4299
|
+
if (this.#cleanupTimer) clearInterval(this.#cleanupTimer);
|
|
4300
|
+
this._cleanup();
|
|
4301
|
+
this.groupData.clear();
|
|
3874
4302
|
this.lastSeen.clear();
|
|
4303
|
+
this.userToGroup.clear();
|
|
4304
|
+
this.groupTTL.clear();
|
|
4305
|
+
this.groupFlags.clear();
|
|
3875
4306
|
}
|
|
3876
4307
|
}
|
|
3877
4308
|
|