tiny-essentials 1.8.5 → 1.9.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,7 @@
1
+ 'use strict';
2
+
3
+ var TinyRateLimiter = require('../libs/TinyRateLimiter.cjs');
4
+
5
+
6
+
7
+ exports.TinyRateLimiter = TinyRateLimiter;
@@ -0,0 +1,3 @@
1
+ export { TinyRateLimiter };
2
+ import TinyRateLimiter from '../libs/TinyRateLimiter.mjs';
3
+ //# sourceMappingURL=TinyRateLimiter.d.mts.map
@@ -0,0 +1,2 @@
1
+ import TinyRateLimiter from '../libs/TinyRateLimiter.mjs';
2
+ export { TinyRateLimiter };
package/dist/v1/index.cjs CHANGED
@@ -10,6 +10,7 @@ var simpleMath = require('./basics/simpleMath.cjs');
10
10
  var text = require('./basics/text.cjs');
11
11
  var ColorSafeStringify = require('./libs/ColorSafeStringify.cjs');
12
12
  var TinyPromiseQueue = require('./libs/TinyPromiseQueue.cjs');
13
+ var TinyRateLimiter = require('./libs/TinyRateLimiter.cjs');
13
14
 
14
15
 
15
16
 
@@ -36,3 +37,4 @@ exports.toTitleCase = text.toTitleCase;
36
37
  exports.toTitleCaseLowerFirst = text.toTitleCaseLowerFirst;
37
38
  exports.ColorSafeStringify = ColorSafeStringify;
38
39
  exports.TinyPromiseQueue = TinyPromiseQueue;
40
+ exports.TinyRateLimiter = TinyRateLimiter;
@@ -1,3 +1,4 @@
1
+ import TinyRateLimiter from './libs/TinyRateLimiter.mjs';
1
2
  import ColorSafeStringify from './libs/ColorSafeStringify.mjs';
2
3
  import TinyPromiseQueue from './libs/TinyPromiseQueue.mjs';
3
4
  import TinyLevelUp from '../legacy/libs/userLevel.mjs';
@@ -21,5 +22,5 @@ import { getTimeDuration } from './basics/clock.mjs';
21
22
  import { shuffleArray } from './basics/array.mjs';
22
23
  import { toTitleCase } from './basics/text.mjs';
23
24
  import { toTitleCaseLowerFirst } from './basics/text.mjs';
24
- export { ColorSafeStringify, TinyPromiseQueue, TinyLevelUp, arraySortPositions, formatBytes, addAiMarkerShortcut, extendObjType, reorderObjTypeOrder, cloneObjTypeOrder, countObj, checkObj, objType, ruleOfThree, getSimplePerc, asyncReplace, getAge, formatCustomTimer, formatDayTimer, formatTimer, getTimeDuration, shuffleArray, toTitleCase, toTitleCaseLowerFirst };
25
+ export { TinyRateLimiter, ColorSafeStringify, TinyPromiseQueue, TinyLevelUp, arraySortPositions, formatBytes, addAiMarkerShortcut, extendObjType, reorderObjTypeOrder, cloneObjTypeOrder, countObj, checkObj, objType, ruleOfThree, getSimplePerc, asyncReplace, getAge, formatCustomTimer, formatDayTimer, formatTimer, getTimeDuration, shuffleArray, toTitleCase, toTitleCaseLowerFirst };
25
26
  //# sourceMappingURL=index.d.mts.map
package/dist/v1/index.mjs CHANGED
@@ -8,4 +8,5 @@ import { formatBytes, getAge, getSimplePerc, ruleOfThree } from './basics/simple
8
8
  import { addAiMarkerShortcut, toTitleCase, toTitleCaseLowerFirst } from './basics/text.mjs';
9
9
  import ColorSafeStringify from './libs/ColorSafeStringify.mjs';
10
10
  import TinyPromiseQueue from './libs/TinyPromiseQueue.mjs';
11
- export { ColorSafeStringify, TinyPromiseQueue, TinyLevelUp, arraySortPositions, formatBytes, addAiMarkerShortcut, extendObjType, reorderObjTypeOrder, cloneObjTypeOrder, countObj, checkObj, objType, ruleOfThree, getSimplePerc, asyncReplace, getAge, formatCustomTimer, formatDayTimer, formatTimer, getTimeDuration, shuffleArray, toTitleCase, toTitleCaseLowerFirst, };
11
+ import TinyRateLimiter from './libs/TinyRateLimiter.mjs';
12
+ export { TinyRateLimiter, ColorSafeStringify, TinyPromiseQueue, TinyLevelUp, arraySortPositions, formatBytes, addAiMarkerShortcut, extendObjType, reorderObjTypeOrder, cloneObjTypeOrder, countObj, checkObj, objType, ruleOfThree, getSimplePerc, asyncReplace, getAge, formatCustomTimer, formatDayTimer, formatTimer, getTimeDuration, shuffleArray, toTitleCase, toTitleCaseLowerFirst, };
@@ -0,0 +1,196 @@
1
+ 'use strict';
2
+
3
+ /**
4
+ * Class representing a flexible rate limiter per user.
5
+ *
6
+ * This rate limiter can be configured by maximum number of hits,
7
+ * time interval, or a combination of both. It supports automatic
8
+ * cleanup of inactive users to optimize memory usage.
9
+ *
10
+ * @class
11
+ */
12
+ class TinyRateLimiter {
13
+ /**
14
+ * @param {Object} options
15
+ * @param {number} [options.maxHits] - Max interactions allowed
16
+ * @param {number} [options.interval] - Time window in milliseconds
17
+ * @param {number} [options.cleanupInterval=60000] - Interval for automatic cleanup (ms)
18
+ * @param {number} [options.maxIdle=300000] - Max idle time for a user before being cleaned (ms)
19
+ */
20
+ constructor({ maxHits, interval, cleanupInterval = 60000, maxIdle = 300000 }) {
21
+ /** @param {number|undefined} val */
22
+ const isPositiveInteger = (val) =>
23
+ typeof val === 'number' && Number.isFinite(val) && val >= 1 && Number.isInteger(val);
24
+
25
+ const isMaxHitsValid = isPositiveInteger(maxHits);
26
+ const isIntervalValid = isPositiveInteger(interval);
27
+ const isCleanupValid = isPositiveInteger(cleanupInterval);
28
+ const isMaxIdleValid = isPositiveInteger(maxIdle);
29
+
30
+ if (!isMaxHitsValid && !isIntervalValid)
31
+ throw new Error("RateLimiter requires at least one valid option: 'maxHits' or 'interval'.");
32
+ if (maxHits !== undefined && !isMaxHitsValid)
33
+ throw new Error("'maxHits' must be a positive integer if defined.");
34
+ if (interval !== undefined && !isIntervalValid)
35
+ throw new Error("'interval' must be a positive integer in milliseconds if defined.");
36
+ if (!isCleanupValid)
37
+ throw new Error("'cleanupInterval' must be a positive integer in milliseconds.");
38
+ if (!isMaxIdleValid) throw new Error("'maxIdle' must be a positive integer in milliseconds.");
39
+
40
+ this.maxHits = isMaxHitsValid ? maxHits : null;
41
+ this.interval = isIntervalValid ? interval : null;
42
+ this.cleanupInterval = cleanupInterval;
43
+ this.maxIdle = maxIdle;
44
+
45
+ /** @type {Map<string, number[]>} */
46
+ this.userData = new Map();
47
+
48
+ /** @type {Map<string, number>} */
49
+ this.lastSeen = new Map();
50
+
51
+ // Start automatic cleanup
52
+ this._cleanupTimer = setInterval(() => this._cleanup(), this.cleanupInterval);
53
+ }
54
+
55
+ /**
56
+ * Get the interval window in milliseconds.
57
+ *
58
+ * @returns {number} The interval value.
59
+ * @throws {Error} If interval is not a valid finite number.
60
+ */
61
+ getInterval() {
62
+ if (typeof this.interval !== 'number' || !Number.isFinite(this.interval))
63
+ throw new Error("'interval' is not a valid finite number.");
64
+ return this.interval;
65
+ }
66
+
67
+ /**
68
+ * Get the maximum number of allowed hits.
69
+ *
70
+ * @returns {number} The maxHits value.
71
+ * @throws {Error} If maxHits is not a valid finite number.
72
+ */
73
+ getMaxHits() {
74
+ if (typeof this.maxHits !== 'number' || !Number.isFinite(this.maxHits)) {
75
+ throw new Error("'maxHits' is not a valid finite number.");
76
+ }
77
+ return this.maxHits;
78
+ }
79
+
80
+ /**
81
+ * Register a hit for a specific user
82
+ * @param {string} userId
83
+ */
84
+ hit(userId) {
85
+ const now = Date.now();
86
+
87
+ if (!this.userData.has(userId)) {
88
+ this.userData.set(userId, []);
89
+ }
90
+
91
+ const history = this.userData.get(userId);
92
+ if (!history) throw new Error(`No data found for userId: ${userId}`);
93
+
94
+ history.push(now);
95
+ this.lastSeen.set(userId, now);
96
+
97
+ // Clean up old entries
98
+ if (this.interval !== null) {
99
+ const interval = this.getInterval();
100
+ const cutoff = now - interval;
101
+ while (history.length && history[0] < cutoff) {
102
+ history.shift();
103
+ }
104
+ }
105
+
106
+ // Optional: keep only the last N entries for memory optimization
107
+ if (this.maxHits !== null) {
108
+ const maxHits = this.getMaxHits();
109
+ if (history.length > maxHits) {
110
+ history.splice(0, history.length - maxHits);
111
+ }
112
+ }
113
+ }
114
+
115
+ /**
116
+ * Check if the user is currently rate limited
117
+ * @param {string} userId
118
+ * @returns {boolean}
119
+ */
120
+ isRateLimited(userId) {
121
+ const now = Date.now();
122
+
123
+ if (!this.userData.has(userId)) return false;
124
+
125
+ const history = this.userData.get(userId);
126
+ if (!history) throw new Error(`No data found for userId: ${userId}`);
127
+
128
+ if (this.interval !== null) {
129
+ const interval = this.getInterval();
130
+ const recent = history.filter((t) => t > now - interval);
131
+ if (this.maxHits !== null) {
132
+ return recent.length >= this.getMaxHits();
133
+ }
134
+ return recent.length > 0;
135
+ }
136
+
137
+ if (this.maxHits !== null) {
138
+ return history.length >= this.getMaxHits();
139
+ }
140
+
141
+ return false;
142
+ }
143
+
144
+ /**
145
+ * Manually reset user data
146
+ * @param {string} userId
147
+ */
148
+ reset(userId) {
149
+ this.userData.delete(userId);
150
+ this.lastSeen.delete(userId);
151
+ }
152
+
153
+ /**
154
+ * Set hit timestamps for a user
155
+ * @param {string} userId
156
+ * @param {number[]} timestamps
157
+ */
158
+ setData(userId, timestamps) {
159
+ this.userData.set(userId, timestamps);
160
+ this.lastSeen.set(userId, Date.now());
161
+ }
162
+
163
+ /**
164
+ * Get timestamps from user
165
+ * @param {string} userId
166
+ * @returns {number[]}
167
+ */
168
+ getData(userId) {
169
+ return this.userData.get(userId) || [];
170
+ }
171
+
172
+ /**
173
+ * Cleanup old/inactive users
174
+ * @private
175
+ */
176
+ _cleanup() {
177
+ const now = Date.now();
178
+ for (const [userId, last] of this.lastSeen.entries()) {
179
+ if (now - last > this.maxIdle) {
180
+ this.userData.delete(userId);
181
+ this.lastSeen.delete(userId);
182
+ }
183
+ }
184
+ }
185
+
186
+ /**
187
+ * Destroy the rate limiter, stopping all intervals
188
+ */
189
+ destroy() {
190
+ clearInterval(this._cleanupTimer);
191
+ this.userData.clear();
192
+ this.lastSeen.clear();
193
+ }
194
+ }
195
+
196
+ module.exports = TinyRateLimiter;
@@ -0,0 +1,86 @@
1
+ export default TinyRateLimiter;
2
+ /**
3
+ * Class representing a flexible rate limiter per user.
4
+ *
5
+ * This rate limiter can be configured by maximum number of hits,
6
+ * time interval, or a combination of both. It supports automatic
7
+ * cleanup of inactive users to optimize memory usage.
8
+ *
9
+ * @class
10
+ */
11
+ declare class TinyRateLimiter {
12
+ /**
13
+ * @param {Object} options
14
+ * @param {number} [options.maxHits] - Max interactions allowed
15
+ * @param {number} [options.interval] - Time window in milliseconds
16
+ * @param {number} [options.cleanupInterval=60000] - Interval for automatic cleanup (ms)
17
+ * @param {number} [options.maxIdle=300000] - Max idle time for a user before being cleaned (ms)
18
+ */
19
+ constructor({ maxHits, interval, cleanupInterval, maxIdle }: {
20
+ maxHits?: number | undefined;
21
+ interval?: number | undefined;
22
+ cleanupInterval?: number | undefined;
23
+ maxIdle?: number | undefined;
24
+ });
25
+ maxHits: number | null | undefined;
26
+ interval: number | null | undefined;
27
+ cleanupInterval: number;
28
+ maxIdle: number;
29
+ /** @type {Map<string, number[]>} */
30
+ userData: Map<string, number[]>;
31
+ /** @type {Map<string, number>} */
32
+ lastSeen: Map<string, number>;
33
+ _cleanupTimer: NodeJS.Timeout;
34
+ /**
35
+ * Get the interval window in milliseconds.
36
+ *
37
+ * @returns {number} The interval value.
38
+ * @throws {Error} If interval is not a valid finite number.
39
+ */
40
+ getInterval(): number;
41
+ /**
42
+ * Get the maximum number of allowed hits.
43
+ *
44
+ * @returns {number} The maxHits value.
45
+ * @throws {Error} If maxHits is not a valid finite number.
46
+ */
47
+ getMaxHits(): number;
48
+ /**
49
+ * Register a hit for a specific user
50
+ * @param {string} userId
51
+ */
52
+ hit(userId: string): void;
53
+ /**
54
+ * Check if the user is currently rate limited
55
+ * @param {string} userId
56
+ * @returns {boolean}
57
+ */
58
+ isRateLimited(userId: string): boolean;
59
+ /**
60
+ * Manually reset user data
61
+ * @param {string} userId
62
+ */
63
+ reset(userId: string): void;
64
+ /**
65
+ * Set hit timestamps for a user
66
+ * @param {string} userId
67
+ * @param {number[]} timestamps
68
+ */
69
+ setData(userId: string, timestamps: number[]): void;
70
+ /**
71
+ * Get timestamps from user
72
+ * @param {string} userId
73
+ * @returns {number[]}
74
+ */
75
+ getData(userId: string): number[];
76
+ /**
77
+ * Cleanup old/inactive users
78
+ * @private
79
+ */
80
+ private _cleanup;
81
+ /**
82
+ * Destroy the rate limiter, stopping all intervals
83
+ */
84
+ destroy(): void;
85
+ }
86
+ //# sourceMappingURL=TinyRateLimit.d.mts.map
@@ -0,0 +1,171 @@
1
+ /**
2
+ * Class representing a flexible rate limiter per user.
3
+ *
4
+ * This rate limiter can be configured by maximum number of hits,
5
+ * time interval, or a combination of both. It supports automatic
6
+ * cleanup of inactive users to optimize memory usage.
7
+ *
8
+ * @class
9
+ */
10
+ class TinyRateLimiter {
11
+ /**
12
+ * @param {Object} options
13
+ * @param {number} [options.maxHits] - Max interactions allowed
14
+ * @param {number} [options.interval] - Time window in milliseconds
15
+ * @param {number} [options.cleanupInterval=60000] - Interval for automatic cleanup (ms)
16
+ * @param {number} [options.maxIdle=300000] - Max idle time for a user before being cleaned (ms)
17
+ */
18
+ constructor({ maxHits, interval, cleanupInterval = 60000, maxIdle = 300000 }) {
19
+ /** @param {number|undefined} val */
20
+ const isPositiveInteger = (val) => typeof val === 'number' && Number.isFinite(val) && val >= 1 && Number.isInteger(val);
21
+ const isMaxHitsValid = isPositiveInteger(maxHits);
22
+ const isIntervalValid = isPositiveInteger(interval);
23
+ const isCleanupValid = isPositiveInteger(cleanupInterval);
24
+ const isMaxIdleValid = isPositiveInteger(maxIdle);
25
+ if (!isMaxHitsValid && !isIntervalValid)
26
+ throw new Error("RateLimiter requires at least one valid option: 'maxHits' or 'interval'.");
27
+ if (maxHits !== undefined && !isMaxHitsValid)
28
+ throw new Error("'maxHits' must be a positive integer if defined.");
29
+ if (interval !== undefined && !isIntervalValid)
30
+ throw new Error("'interval' must be a positive integer in milliseconds if defined.");
31
+ if (!isCleanupValid)
32
+ throw new Error("'cleanupInterval' must be a positive integer in milliseconds.");
33
+ if (!isMaxIdleValid)
34
+ throw new Error("'maxIdle' must be a positive integer in milliseconds.");
35
+ this.maxHits = isMaxHitsValid ? maxHits : null;
36
+ this.interval = isIntervalValid ? interval : null;
37
+ this.cleanupInterval = cleanupInterval;
38
+ this.maxIdle = maxIdle;
39
+ /** @type {Map<string, number[]>} */
40
+ this.userData = new Map();
41
+ /** @type {Map<string, number>} */
42
+ this.lastSeen = new Map();
43
+ // Start automatic cleanup
44
+ this._cleanupTimer = setInterval(() => this._cleanup(), this.cleanupInterval);
45
+ }
46
+ /**
47
+ * Get the interval window in milliseconds.
48
+ *
49
+ * @returns {number} The interval value.
50
+ * @throws {Error} If interval is not a valid finite number.
51
+ */
52
+ getInterval() {
53
+ if (typeof this.interval !== 'number' || !Number.isFinite(this.interval))
54
+ throw new Error("'interval' is not a valid finite number.");
55
+ return this.interval;
56
+ }
57
+ /**
58
+ * Get the maximum number of allowed hits.
59
+ *
60
+ * @returns {number} The maxHits value.
61
+ * @throws {Error} If maxHits is not a valid finite number.
62
+ */
63
+ getMaxHits() {
64
+ if (typeof this.maxHits !== 'number' || !Number.isFinite(this.maxHits)) {
65
+ throw new Error("'maxHits' is not a valid finite number.");
66
+ }
67
+ return this.maxHits;
68
+ }
69
+ /**
70
+ * Register a hit for a specific user
71
+ * @param {string} userId
72
+ */
73
+ hit(userId) {
74
+ const now = Date.now();
75
+ if (!this.userData.has(userId)) {
76
+ this.userData.set(userId, []);
77
+ }
78
+ const history = this.userData.get(userId);
79
+ if (!history)
80
+ throw new Error(`No data found for userId: ${userId}`);
81
+ history.push(now);
82
+ this.lastSeen.set(userId, now);
83
+ // Clean up old entries
84
+ if (this.interval !== null) {
85
+ const interval = this.getInterval();
86
+ const cutoff = now - interval;
87
+ while (history.length && history[0] < cutoff) {
88
+ history.shift();
89
+ }
90
+ }
91
+ // Optional: keep only the last N entries for memory optimization
92
+ if (this.maxHits !== null) {
93
+ const maxHits = this.getMaxHits();
94
+ if (history.length > maxHits) {
95
+ history.splice(0, history.length - maxHits);
96
+ }
97
+ }
98
+ }
99
+ /**
100
+ * Check if the user is currently rate limited
101
+ * @param {string} userId
102
+ * @returns {boolean}
103
+ */
104
+ isRateLimited(userId) {
105
+ const now = Date.now();
106
+ if (!this.userData.has(userId))
107
+ return false;
108
+ const history = this.userData.get(userId);
109
+ if (!history)
110
+ throw new Error(`No data found for userId: ${userId}`);
111
+ if (this.interval !== null) {
112
+ const interval = this.getInterval();
113
+ const recent = history.filter((t) => t > now - interval);
114
+ if (this.maxHits !== null) {
115
+ return recent.length >= this.getMaxHits();
116
+ }
117
+ return recent.length > 0;
118
+ }
119
+ if (this.maxHits !== null) {
120
+ return history.length >= this.getMaxHits();
121
+ }
122
+ return false;
123
+ }
124
+ /**
125
+ * Manually reset user data
126
+ * @param {string} userId
127
+ */
128
+ reset(userId) {
129
+ this.userData.delete(userId);
130
+ this.lastSeen.delete(userId);
131
+ }
132
+ /**
133
+ * Set hit timestamps for a user
134
+ * @param {string} userId
135
+ * @param {number[]} timestamps
136
+ */
137
+ setData(userId, timestamps) {
138
+ this.userData.set(userId, timestamps);
139
+ this.lastSeen.set(userId, Date.now());
140
+ }
141
+ /**
142
+ * Get timestamps from user
143
+ * @param {string} userId
144
+ * @returns {number[]}
145
+ */
146
+ getData(userId) {
147
+ return this.userData.get(userId) || [];
148
+ }
149
+ /**
150
+ * Cleanup old/inactive users
151
+ * @private
152
+ */
153
+ _cleanup() {
154
+ const now = Date.now();
155
+ for (const [userId, last] of this.lastSeen.entries()) {
156
+ if (now - last > this.maxIdle) {
157
+ this.userData.delete(userId);
158
+ this.lastSeen.delete(userId);
159
+ }
160
+ }
161
+ }
162
+ /**
163
+ * Destroy the rate limiter, stopping all intervals
164
+ */
165
+ destroy() {
166
+ clearInterval(this._cleanupTimer);
167
+ this.userData.clear();
168
+ this.lastSeen.clear();
169
+ }
170
+ }
171
+ export default TinyRateLimiter;