dankgrinder 4.9.9 → 5.0.1
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/lib/commands/adventure.js +25 -14
- package/lib/commands/beg.js +3 -1
- package/lib/commands/blackjack.js +31 -16
- package/lib/commands/crime.js +37 -13
- package/lib/commands/drops.js +29 -14
- package/lib/commands/fish.js +9 -4
- package/lib/commands/gamble.js +86 -27
- package/lib/commands/generic.js +19 -5
- package/lib/commands/highlow.js +20 -12
- package/lib/commands/index.js +2 -1
- package/lib/commands/inventory.js +54 -21
- package/lib/commands/postmemes.js +6 -4
- package/lib/commands/profile.js +5 -4
- package/lib/commands/search.js +41 -16
- package/lib/commands/shop.js +31 -8
- package/lib/commands/stream.js +1 -1
- package/lib/commands/trivia.js +6 -2
- package/lib/commands/utils.js +165 -81
- package/lib/commands/work.js +17 -8
- package/lib/grinder.js +812 -103
- package/lib/structures.js +725 -0
- package/package.json +3 -2
|
@@ -0,0 +1,725 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Advanced data structures used across the entire codebase.
|
|
3
|
+
*
|
|
4
|
+
* Contents:
|
|
5
|
+
* BloomFilter – O(1) probabilistic set membership (cooldown pre-check)
|
|
6
|
+
* LRUCache – O(1) get/set bounded cache (doubly-linked list + HashMap)
|
|
7
|
+
* RingBuffer – O(1) fixed-size FIFO, zero GC pressure (log storage)
|
|
8
|
+
* Trie – O(k) prefix search tree (item name matching)
|
|
9
|
+
* AhoCorasick – O(n+m+z) multi-pattern string matcher (item/pattern detection)
|
|
10
|
+
* TokenBucket – O(1) rate limiter (anti-rate-limit)
|
|
11
|
+
* EMA – O(1) exponential moving average (earnings/win-rate tracking)
|
|
12
|
+
* SlidingWindow – O(1) amortized time-based event counter (commands/sec)
|
|
13
|
+
* MinHeap – O(log n) priority queue (command scheduling)
|
|
14
|
+
* VoseAlias – O(1) weighted random sampling (safe button selection)
|
|
15
|
+
* StringPool – O(1) string interning / deduplication (memory savings)
|
|
16
|
+
* BitSet – O(1) compact boolean array (flag management)
|
|
17
|
+
* ObjectPool – O(1) reusable object allocator (reduce GC pressure)
|
|
18
|
+
*/
|
|
19
|
+
|
|
20
|
+
'use strict';
|
|
21
|
+
|
|
22
|
+
// ═══════════════════════════════════════════════════════════════
|
|
23
|
+
// BloomFilter – probabilistic set membership, O(1) insert/query
|
|
24
|
+
// False positives possible, false negatives impossible.
|
|
25
|
+
// Used for fast cooldown pre-check before hitting Redis.
|
|
26
|
+
// ═══════════════════════════════════════════════════════════════
|
|
27
|
+
class BloomFilter {
|
|
28
|
+
constructor(size = 1024, hashCount = 3) {
|
|
29
|
+
this.size = size;
|
|
30
|
+
this.hashCount = hashCount;
|
|
31
|
+
this.bits = new Uint32Array(Math.ceil(size / 32));
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
_hash(str, seed) {
|
|
35
|
+
let h = seed;
|
|
36
|
+
for (let i = 0; i < str.length; i++) {
|
|
37
|
+
h = (h * 31 + str.charCodeAt(i)) | 0;
|
|
38
|
+
}
|
|
39
|
+
return ((h & 0x7fffffff) % this.size) >>> 0;
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
add(key) {
|
|
43
|
+
for (let i = 0; i < this.hashCount; i++) {
|
|
44
|
+
const idx = this._hash(key, i * 0x9e3779b9);
|
|
45
|
+
this.bits[idx >>> 5] |= (1 << (idx & 31));
|
|
46
|
+
}
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
has(key) {
|
|
50
|
+
for (let i = 0; i < this.hashCount; i++) {
|
|
51
|
+
const idx = this._hash(key, i * 0x9e3779b9);
|
|
52
|
+
if ((this.bits[idx >>> 5] & (1 << (idx & 31))) === 0) return false;
|
|
53
|
+
}
|
|
54
|
+
return true;
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
clear() {
|
|
58
|
+
this.bits.fill(0);
|
|
59
|
+
}
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
// ═══════════════════════════════════════════════════════════════
|
|
63
|
+
// LRUCache – Least Recently Used cache with O(1) get/set/delete
|
|
64
|
+
// Uses a doubly-linked list for ordering + Map for O(1) lookup.
|
|
65
|
+
// ═══════════════════════════════════════════════════════════════
|
|
66
|
+
class LRUCache {
|
|
67
|
+
constructor(capacity = 128) {
|
|
68
|
+
this.capacity = capacity;
|
|
69
|
+
this.map = new Map();
|
|
70
|
+
this.head = { key: null, val: null, prev: null, next: null };
|
|
71
|
+
this.tail = { key: null, val: null, prev: null, next: null };
|
|
72
|
+
this.head.next = this.tail;
|
|
73
|
+
this.tail.prev = this.head;
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
_remove(node) {
|
|
77
|
+
node.prev.next = node.next;
|
|
78
|
+
node.next.prev = node.prev;
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
_addToFront(node) {
|
|
82
|
+
node.next = this.head.next;
|
|
83
|
+
node.prev = this.head;
|
|
84
|
+
this.head.next.prev = node;
|
|
85
|
+
this.head.next = node;
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
get(key) {
|
|
89
|
+
const node = this.map.get(key);
|
|
90
|
+
if (!node) return undefined;
|
|
91
|
+
this._remove(node);
|
|
92
|
+
this._addToFront(node);
|
|
93
|
+
return node.val;
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
set(key, val) {
|
|
97
|
+
if (this.map.has(key)) {
|
|
98
|
+
const node = this.map.get(key);
|
|
99
|
+
node.val = val;
|
|
100
|
+
this._remove(node);
|
|
101
|
+
this._addToFront(node);
|
|
102
|
+
return;
|
|
103
|
+
}
|
|
104
|
+
const node = { key, val, prev: null, next: null };
|
|
105
|
+
this.map.set(key, node);
|
|
106
|
+
this._addToFront(node);
|
|
107
|
+
if (this.map.size > this.capacity) {
|
|
108
|
+
const lru = this.tail.prev;
|
|
109
|
+
this._remove(lru);
|
|
110
|
+
this.map.delete(lru.key);
|
|
111
|
+
}
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
delete(key) {
|
|
115
|
+
const node = this.map.get(key);
|
|
116
|
+
if (!node) return false;
|
|
117
|
+
this._remove(node);
|
|
118
|
+
this.map.delete(key);
|
|
119
|
+
return true;
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
has(key) { return this.map.has(key); }
|
|
123
|
+
get size() { return this.map.size; }
|
|
124
|
+
clear() { this.map.clear(); this.head.next = this.tail; this.tail.prev = this.head; }
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
// ═══════════════════════════════════════════════════════════════
|
|
128
|
+
// RingBuffer – Fixed-size circular buffer, O(1) push/get
|
|
129
|
+
// No array shifting, no GC pressure from old elements.
|
|
130
|
+
// ═══════════════════════════════════════════════════════════════
|
|
131
|
+
class RingBuffer {
|
|
132
|
+
constructor(capacity = 64) {
|
|
133
|
+
this.buf = new Array(capacity);
|
|
134
|
+
this.capacity = capacity;
|
|
135
|
+
this.head = 0;
|
|
136
|
+
this.count = 0;
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
push(item) {
|
|
140
|
+
const idx = (this.head + this.count) % this.capacity;
|
|
141
|
+
this.buf[idx] = item;
|
|
142
|
+
if (this.count < this.capacity) {
|
|
143
|
+
this.count++;
|
|
144
|
+
} else {
|
|
145
|
+
this.head = (this.head + 1) % this.capacity;
|
|
146
|
+
}
|
|
147
|
+
}
|
|
148
|
+
|
|
149
|
+
get(i) {
|
|
150
|
+
if (i < 0 || i >= this.count) return undefined;
|
|
151
|
+
return this.buf[(this.head + i) % this.capacity];
|
|
152
|
+
}
|
|
153
|
+
|
|
154
|
+
toArray() {
|
|
155
|
+
const arr = new Array(this.count);
|
|
156
|
+
for (let i = 0; i < this.count; i++) {
|
|
157
|
+
arr[i] = this.buf[(this.head + i) % this.capacity];
|
|
158
|
+
}
|
|
159
|
+
return arr;
|
|
160
|
+
}
|
|
161
|
+
|
|
162
|
+
get length() { return this.count; }
|
|
163
|
+
|
|
164
|
+
last(n = 1) {
|
|
165
|
+
const start = Math.max(0, this.count - n);
|
|
166
|
+
const result = [];
|
|
167
|
+
for (let i = start; i < this.count; i++) {
|
|
168
|
+
result.push(this.get(i));
|
|
169
|
+
}
|
|
170
|
+
return result;
|
|
171
|
+
}
|
|
172
|
+
|
|
173
|
+
clear() { this.head = 0; this.count = 0; this.buf.fill(undefined); }
|
|
174
|
+
}
|
|
175
|
+
|
|
176
|
+
// ═══════════════════════════════════════════════════════════════
|
|
177
|
+
// Trie – Prefix search tree, O(k) insert/search/startsWith
|
|
178
|
+
// Used for fast item name matching and autocomplete.
|
|
179
|
+
// ═══════════════════════════════════════════════════════════════
|
|
180
|
+
class Trie {
|
|
181
|
+
constructor() {
|
|
182
|
+
this.root = { children: new Map(), value: null, isEnd: false };
|
|
183
|
+
}
|
|
184
|
+
|
|
185
|
+
insert(key, value = true) {
|
|
186
|
+
let node = this.root;
|
|
187
|
+
for (const ch of key.toLowerCase()) {
|
|
188
|
+
if (!node.children.has(ch)) {
|
|
189
|
+
node.children.set(ch, { children: new Map(), value: null, isEnd: false });
|
|
190
|
+
}
|
|
191
|
+
node = node.children.get(ch);
|
|
192
|
+
}
|
|
193
|
+
node.isEnd = true;
|
|
194
|
+
node.value = value;
|
|
195
|
+
}
|
|
196
|
+
|
|
197
|
+
search(key) {
|
|
198
|
+
let node = this.root;
|
|
199
|
+
for (const ch of key.toLowerCase()) {
|
|
200
|
+
if (!node.children.has(ch)) return null;
|
|
201
|
+
node = node.children.get(ch);
|
|
202
|
+
}
|
|
203
|
+
return node.isEnd ? node.value : null;
|
|
204
|
+
}
|
|
205
|
+
|
|
206
|
+
startsWith(prefix) {
|
|
207
|
+
let node = this.root;
|
|
208
|
+
for (const ch of prefix.toLowerCase()) {
|
|
209
|
+
if (!node.children.has(ch)) return [];
|
|
210
|
+
node = node.children.get(ch);
|
|
211
|
+
}
|
|
212
|
+
const results = [];
|
|
213
|
+
const dfs = (n, path) => {
|
|
214
|
+
if (n.isEnd) results.push({ key: path, value: n.value });
|
|
215
|
+
for (const [ch, child] of n.children) dfs(child, path + ch);
|
|
216
|
+
};
|
|
217
|
+
dfs(node, prefix.toLowerCase());
|
|
218
|
+
return results;
|
|
219
|
+
}
|
|
220
|
+
|
|
221
|
+
containsInText(text) {
|
|
222
|
+
const lower = text.toLowerCase();
|
|
223
|
+
const matches = [];
|
|
224
|
+
for (let i = 0; i < lower.length; i++) {
|
|
225
|
+
let node = this.root;
|
|
226
|
+
for (let j = i; j < lower.length; j++) {
|
|
227
|
+
if (!node.children.has(lower[j])) break;
|
|
228
|
+
node = node.children.get(lower[j]);
|
|
229
|
+
if (node.isEnd) {
|
|
230
|
+
matches.push({ start: i, end: j + 1, value: node.value });
|
|
231
|
+
}
|
|
232
|
+
}
|
|
233
|
+
}
|
|
234
|
+
return matches;
|
|
235
|
+
}
|
|
236
|
+
}
|
|
237
|
+
|
|
238
|
+
// ═══════════════════════════════════════════════════════════════
|
|
239
|
+
// AhoCorasick – Multi-pattern string matching, O(n + m + z)
|
|
240
|
+
// n = text length, m = total pattern chars, z = number of matches.
|
|
241
|
+
// Builds a finite automaton with failure links (like KMP for multiple patterns).
|
|
242
|
+
// Used to detect "need item", "cooldown", "captcha" etc. in one pass.
|
|
243
|
+
// ═══════════════════════════════════════════════════════════════
|
|
244
|
+
class AhoCorasick {
|
|
245
|
+
constructor() {
|
|
246
|
+
this.goto = [new Map()];
|
|
247
|
+
this.fail = [0];
|
|
248
|
+
this.output = [[]];
|
|
249
|
+
this.built = false;
|
|
250
|
+
}
|
|
251
|
+
|
|
252
|
+
addPattern(pattern, tag = pattern) {
|
|
253
|
+
let state = 0;
|
|
254
|
+
for (const ch of pattern.toLowerCase()) {
|
|
255
|
+
if (!this.goto[state].has(ch)) {
|
|
256
|
+
const next = this.goto.length;
|
|
257
|
+
this.goto.push(new Map());
|
|
258
|
+
this.fail.push(0);
|
|
259
|
+
this.output.push([]);
|
|
260
|
+
this.goto[state].set(ch, next);
|
|
261
|
+
}
|
|
262
|
+
state = this.goto[state].get(ch);
|
|
263
|
+
}
|
|
264
|
+
this.output[state].push(tag);
|
|
265
|
+
this.built = false;
|
|
266
|
+
}
|
|
267
|
+
|
|
268
|
+
build() {
|
|
269
|
+
const queue = [];
|
|
270
|
+
for (const [ch, s] of this.goto[0]) {
|
|
271
|
+
this.fail[s] = 0;
|
|
272
|
+
queue.push(s);
|
|
273
|
+
}
|
|
274
|
+
while (queue.length > 0) {
|
|
275
|
+
const r = queue.shift();
|
|
276
|
+
for (const [ch, s] of this.goto[r]) {
|
|
277
|
+
queue.push(s);
|
|
278
|
+
let state = this.fail[r];
|
|
279
|
+
while (state !== 0 && !this.goto[state].has(ch)) state = this.fail[state];
|
|
280
|
+
this.fail[s] = this.goto[state].has(ch) ? this.goto[state].get(ch) : 0;
|
|
281
|
+
if (this.fail[s] === s) this.fail[s] = 0;
|
|
282
|
+
this.output[s] = this.output[s].concat(this.output[this.fail[s]]);
|
|
283
|
+
}
|
|
284
|
+
}
|
|
285
|
+
this.built = true;
|
|
286
|
+
}
|
|
287
|
+
|
|
288
|
+
search(text) {
|
|
289
|
+
if (!this.built) this.build();
|
|
290
|
+
const lower = text.toLowerCase();
|
|
291
|
+
const results = [];
|
|
292
|
+
let state = 0;
|
|
293
|
+
for (let i = 0; i < lower.length; i++) {
|
|
294
|
+
const ch = lower[i];
|
|
295
|
+
while (state !== 0 && !this.goto[state].has(ch)) state = this.fail[state];
|
|
296
|
+
state = this.goto[state].has(ch) ? this.goto[state].get(ch) : 0;
|
|
297
|
+
for (const tag of this.output[state]) {
|
|
298
|
+
results.push({ index: i, tag });
|
|
299
|
+
}
|
|
300
|
+
}
|
|
301
|
+
return results;
|
|
302
|
+
}
|
|
303
|
+
|
|
304
|
+
hasAny(text) {
|
|
305
|
+
if (!this.built) this.build();
|
|
306
|
+
const lower = text.toLowerCase();
|
|
307
|
+
let state = 0;
|
|
308
|
+
for (let i = 0; i < lower.length; i++) {
|
|
309
|
+
const ch = lower[i];
|
|
310
|
+
while (state !== 0 && !this.goto[state].has(ch)) state = this.fail[state];
|
|
311
|
+
state = this.goto[state].has(ch) ? this.goto[state].get(ch) : 0;
|
|
312
|
+
if (this.output[state].length > 0) return this.output[state][0];
|
|
313
|
+
}
|
|
314
|
+
return null;
|
|
315
|
+
}
|
|
316
|
+
}
|
|
317
|
+
|
|
318
|
+
// ═══════════════════════════════════════════════════════════════
|
|
319
|
+
// TokenBucket – Rate limiter, O(1) consume/refill
|
|
320
|
+
// Allows burst up to capacity, refills at steady rate.
|
|
321
|
+
// ═══════════════════════════════════════════════════════════════
|
|
322
|
+
class TokenBucket {
|
|
323
|
+
constructor(capacity, refillRate, refillIntervalMs = 1000) {
|
|
324
|
+
this.capacity = capacity;
|
|
325
|
+
this.tokens = capacity;
|
|
326
|
+
this.refillRate = refillRate;
|
|
327
|
+
this.refillIntervalMs = refillIntervalMs;
|
|
328
|
+
this.lastRefill = Date.now();
|
|
329
|
+
}
|
|
330
|
+
|
|
331
|
+
_refill() {
|
|
332
|
+
const now = Date.now();
|
|
333
|
+
const elapsed = now - this.lastRefill;
|
|
334
|
+
const tokensToAdd = (elapsed / this.refillIntervalMs) * this.refillRate;
|
|
335
|
+
this.tokens = Math.min(this.capacity, this.tokens + tokensToAdd);
|
|
336
|
+
this.lastRefill = now;
|
|
337
|
+
}
|
|
338
|
+
|
|
339
|
+
consume(n = 1) {
|
|
340
|
+
this._refill();
|
|
341
|
+
if (this.tokens >= n) {
|
|
342
|
+
this.tokens -= n;
|
|
343
|
+
return true;
|
|
344
|
+
}
|
|
345
|
+
return false;
|
|
346
|
+
}
|
|
347
|
+
|
|
348
|
+
waitTime(n = 1) {
|
|
349
|
+
this._refill();
|
|
350
|
+
if (this.tokens >= n) return 0;
|
|
351
|
+
const deficit = n - this.tokens;
|
|
352
|
+
return Math.ceil((deficit / this.refillRate) * this.refillIntervalMs);
|
|
353
|
+
}
|
|
354
|
+
}
|
|
355
|
+
|
|
356
|
+
// ═══════════════════════════════════════════════════════════════
|
|
357
|
+
// EMA – Exponential Moving Average, O(1) update/read
|
|
358
|
+
// Smooths noisy data (earnings per command, win rates).
|
|
359
|
+
// ═══════════════════════════════════════════════════════════════
|
|
360
|
+
class EMA {
|
|
361
|
+
constructor(alpha = 0.1) {
|
|
362
|
+
this.alpha = alpha;
|
|
363
|
+
this.value = null;
|
|
364
|
+
this.count = 0;
|
|
365
|
+
}
|
|
366
|
+
|
|
367
|
+
update(sample) {
|
|
368
|
+
this.count++;
|
|
369
|
+
if (this.value === null) {
|
|
370
|
+
this.value = sample;
|
|
371
|
+
} else {
|
|
372
|
+
this.value = this.alpha * sample + (1 - this.alpha) * this.value;
|
|
373
|
+
}
|
|
374
|
+
return this.value;
|
|
375
|
+
}
|
|
376
|
+
|
|
377
|
+
get() { return this.value ?? 0; }
|
|
378
|
+
reset() { this.value = null; this.count = 0; }
|
|
379
|
+
}
|
|
380
|
+
|
|
381
|
+
// ═══════════════════════════════════════════════════════════════
|
|
382
|
+
// SlidingWindowCounter – Time-based event rate, O(1) amortized
|
|
383
|
+
// Counts events within a sliding window (e.g., commands per minute).
|
|
384
|
+
// Uses two half-windows for smooth approximation.
|
|
385
|
+
// ═══════════════════════════════════════════════════════════════
|
|
386
|
+
class SlidingWindowCounter {
|
|
387
|
+
constructor(windowMs = 60000) {
|
|
388
|
+
this.windowMs = windowMs;
|
|
389
|
+
this.currentCount = 0;
|
|
390
|
+
this.previousCount = 0;
|
|
391
|
+
this.currentStart = Date.now();
|
|
392
|
+
}
|
|
393
|
+
|
|
394
|
+
_advance() {
|
|
395
|
+
const now = Date.now();
|
|
396
|
+
const elapsed = now - this.currentStart;
|
|
397
|
+
if (elapsed >= this.windowMs) {
|
|
398
|
+
this.previousCount = this.currentCount;
|
|
399
|
+
this.currentCount = 0;
|
|
400
|
+
this.currentStart = now;
|
|
401
|
+
}
|
|
402
|
+
}
|
|
403
|
+
|
|
404
|
+
increment() {
|
|
405
|
+
this._advance();
|
|
406
|
+
this.currentCount++;
|
|
407
|
+
}
|
|
408
|
+
|
|
409
|
+
getRate() {
|
|
410
|
+
this._advance();
|
|
411
|
+
const elapsed = Date.now() - this.currentStart;
|
|
412
|
+
const weight = elapsed / this.windowMs;
|
|
413
|
+
return this.currentCount + this.previousCount * (1 - weight);
|
|
414
|
+
}
|
|
415
|
+
}
|
|
416
|
+
|
|
417
|
+
// ═══════════════════════════════════════════════════════════════
|
|
418
|
+
// MinHeap – Binary heap priority queue, O(log n) push/pop
|
|
419
|
+
// ═══════════════════════════════════════════════════════════════
|
|
420
|
+
class MinHeap {
|
|
421
|
+
constructor(comparator = (a, b) => a - b) {
|
|
422
|
+
this.heap = [];
|
|
423
|
+
this.cmp = comparator;
|
|
424
|
+
}
|
|
425
|
+
|
|
426
|
+
get size() { return this.heap.length; }
|
|
427
|
+
|
|
428
|
+
push(val) {
|
|
429
|
+
this.heap.push(val);
|
|
430
|
+
this._bubbleUp(this.heap.length - 1);
|
|
431
|
+
}
|
|
432
|
+
|
|
433
|
+
pop() {
|
|
434
|
+
if (this.heap.length === 0) return undefined;
|
|
435
|
+
const top = this.heap[0];
|
|
436
|
+
const last = this.heap.pop();
|
|
437
|
+
if (this.heap.length > 0) {
|
|
438
|
+
this.heap[0] = last;
|
|
439
|
+
this._sinkDown(0);
|
|
440
|
+
}
|
|
441
|
+
return top;
|
|
442
|
+
}
|
|
443
|
+
|
|
444
|
+
peek() { return this.heap[0]; }
|
|
445
|
+
|
|
446
|
+
_bubbleUp(i) {
|
|
447
|
+
while (i > 0) {
|
|
448
|
+
const parent = (i - 1) >>> 1;
|
|
449
|
+
if (this.cmp(this.heap[i], this.heap[parent]) < 0) {
|
|
450
|
+
[this.heap[i], this.heap[parent]] = [this.heap[parent], this.heap[i]];
|
|
451
|
+
i = parent;
|
|
452
|
+
} else break;
|
|
453
|
+
}
|
|
454
|
+
}
|
|
455
|
+
|
|
456
|
+
_sinkDown(i) {
|
|
457
|
+
const n = this.heap.length;
|
|
458
|
+
while (true) {
|
|
459
|
+
let smallest = i;
|
|
460
|
+
const left = 2 * i + 1;
|
|
461
|
+
const right = 2 * i + 2;
|
|
462
|
+
if (left < n && this.cmp(this.heap[left], this.heap[smallest]) < 0) smallest = left;
|
|
463
|
+
if (right < n && this.cmp(this.heap[right], this.heap[smallest]) < 0) smallest = right;
|
|
464
|
+
if (smallest !== i) {
|
|
465
|
+
[this.heap[i], this.heap[smallest]] = [this.heap[smallest], this.heap[i]];
|
|
466
|
+
i = smallest;
|
|
467
|
+
} else break;
|
|
468
|
+
}
|
|
469
|
+
}
|
|
470
|
+
}
|
|
471
|
+
|
|
472
|
+
// ═══════════════════════════════════════════════════════════════
|
|
473
|
+
// VoseAlias – O(1) weighted random sampling after O(n) setup
|
|
474
|
+
// Walker's alias method. Used for safe button selection with weights.
|
|
475
|
+
// ═══════════════════════════════════════════════════════════════
|
|
476
|
+
class VoseAlias {
|
|
477
|
+
constructor(weights) {
|
|
478
|
+
const n = weights.length;
|
|
479
|
+
const total = weights.reduce((a, b) => a + b, 0);
|
|
480
|
+
this.prob = new Float64Array(n);
|
|
481
|
+
this.alias = new Int32Array(n);
|
|
482
|
+
const scaled = weights.map(w => (w / total) * n);
|
|
483
|
+
const small = [], large = [];
|
|
484
|
+
for (let i = 0; i < n; i++) {
|
|
485
|
+
(scaled[i] < 1 ? small : large).push(i);
|
|
486
|
+
}
|
|
487
|
+
while (small.length > 0 && large.length > 0) {
|
|
488
|
+
const s = small.pop();
|
|
489
|
+
const l = large.pop();
|
|
490
|
+
this.prob[s] = scaled[s];
|
|
491
|
+
this.alias[s] = l;
|
|
492
|
+
scaled[l] = scaled[l] + scaled[s] - 1;
|
|
493
|
+
(scaled[l] < 1 ? small : large).push(l);
|
|
494
|
+
}
|
|
495
|
+
while (large.length > 0) this.prob[large.pop()] = 1;
|
|
496
|
+
while (small.length > 0) this.prob[small.pop()] = 1;
|
|
497
|
+
}
|
|
498
|
+
|
|
499
|
+
sample() {
|
|
500
|
+
const i = Math.floor(Math.random() * this.prob.length);
|
|
501
|
+
return Math.random() < this.prob[i] ? i : this.alias[i];
|
|
502
|
+
}
|
|
503
|
+
}
|
|
504
|
+
|
|
505
|
+
// ═══════════════════════════════════════════════════════════════
|
|
506
|
+
// StringPool – String interning / deduplication, O(1) lookup
|
|
507
|
+
// Repeated strings (Discord IDs, channel IDs) share one reference.
|
|
508
|
+
// ═══════════════════════════════════════════════════════════════
|
|
509
|
+
class StringPool {
|
|
510
|
+
constructor() {
|
|
511
|
+
this.pool = new Map();
|
|
512
|
+
}
|
|
513
|
+
|
|
514
|
+
intern(str) {
|
|
515
|
+
if (typeof str !== 'string') return str;
|
|
516
|
+
const existing = this.pool.get(str);
|
|
517
|
+
if (existing !== undefined) return existing;
|
|
518
|
+
this.pool.set(str, str);
|
|
519
|
+
return str;
|
|
520
|
+
}
|
|
521
|
+
|
|
522
|
+
get size() { return this.pool.size; }
|
|
523
|
+
clear() { this.pool.clear(); }
|
|
524
|
+
}
|
|
525
|
+
|
|
526
|
+
// ═══════════════════════════════════════════════════════════════
|
|
527
|
+
// BitSet – Compact boolean array using Uint32Array, O(1) ops
|
|
528
|
+
// ═══════════════════════════════════════════════════════════════
|
|
529
|
+
class BitSet {
|
|
530
|
+
constructor(size = 256) {
|
|
531
|
+
this.bits = new Uint32Array(Math.ceil(size / 32));
|
|
532
|
+
this.size = size;
|
|
533
|
+
}
|
|
534
|
+
|
|
535
|
+
set(i) { this.bits[i >>> 5] |= (1 << (i & 31)); }
|
|
536
|
+
clear(i) { this.bits[i >>> 5] &= ~(1 << (i & 31)); }
|
|
537
|
+
get(i) { return (this.bits[i >>> 5] & (1 << (i & 31))) !== 0; }
|
|
538
|
+
toggle(i) { this.bits[i >>> 5] ^= (1 << (i & 31)); }
|
|
539
|
+
clearAll() { this.bits.fill(0); }
|
|
540
|
+
|
|
541
|
+
popcount() {
|
|
542
|
+
let count = 0;
|
|
543
|
+
for (let i = 0; i < this.bits.length; i++) {
|
|
544
|
+
let v = this.bits[i];
|
|
545
|
+
v = v - ((v >> 1) & 0x55555555);
|
|
546
|
+
v = (v & 0x33333333) + ((v >> 2) & 0x33333333);
|
|
547
|
+
count += ((v + (v >> 4)) & 0x0f0f0f0f) * 0x01010101 >> 24;
|
|
548
|
+
}
|
|
549
|
+
return count;
|
|
550
|
+
}
|
|
551
|
+
}
|
|
552
|
+
|
|
553
|
+
// ═══════════════════════════════════════════════════════════════
|
|
554
|
+
// ObjectPool – Reusable object allocator, O(1) acquire/release
|
|
555
|
+
// Avoids GC pressure from frequent small object allocation.
|
|
556
|
+
// ═══════════════════════════════════════════════════════════════
|
|
557
|
+
class ObjectPool {
|
|
558
|
+
constructor(factory, resetFn, maxSize = 64) {
|
|
559
|
+
this.factory = factory;
|
|
560
|
+
this.resetFn = resetFn;
|
|
561
|
+
this.pool = [];
|
|
562
|
+
this.maxSize = maxSize;
|
|
563
|
+
}
|
|
564
|
+
|
|
565
|
+
acquire() {
|
|
566
|
+
return this.pool.length > 0 ? this.pool.pop() : this.factory();
|
|
567
|
+
}
|
|
568
|
+
|
|
569
|
+
release(obj) {
|
|
570
|
+
if (this.pool.length < this.maxSize) {
|
|
571
|
+
this.resetFn(obj);
|
|
572
|
+
this.pool.push(obj);
|
|
573
|
+
}
|
|
574
|
+
}
|
|
575
|
+
|
|
576
|
+
get available() { return this.pool.length; }
|
|
577
|
+
prewarm(n) { for (let i = 0; i < n; i++) this.pool.push(this.factory()); }
|
|
578
|
+
}
|
|
579
|
+
|
|
580
|
+
// ═══════════════════════════════════════════════════════════════
|
|
581
|
+
// TimerWheel – Hashed timer wheel, O(1) schedule/cancel
|
|
582
|
+
// At 10K accounts × 26 commands = 260K timers, individual setTimeout
|
|
583
|
+
// creates 260K heap objects. TimerWheel uses a single flat circular
|
|
584
|
+
// array — the same technique used by the Linux kernel and Kafka.
|
|
585
|
+
// ═══════════════════════════════════════════════════════════════
|
|
586
|
+
class TimerWheel {
|
|
587
|
+
constructor(buckets = 1024, tickMs = 100) {
|
|
588
|
+
this.buckets = buckets;
|
|
589
|
+
this.tickMs = tickMs;
|
|
590
|
+
this.wheel = new Array(buckets);
|
|
591
|
+
for (let i = 0; i < buckets; i++) this.wheel[i] = [];
|
|
592
|
+
this.current = 0;
|
|
593
|
+
this._nextId = 0;
|
|
594
|
+
this._interval = null;
|
|
595
|
+
this._pending = 0;
|
|
596
|
+
}
|
|
597
|
+
|
|
598
|
+
start() {
|
|
599
|
+
if (this._interval) return;
|
|
600
|
+
this._interval = setInterval(() => this._tick(), this.tickMs);
|
|
601
|
+
if (this._interval.unref) this._interval.unref();
|
|
602
|
+
}
|
|
603
|
+
|
|
604
|
+
stop() {
|
|
605
|
+
if (this._interval) { clearInterval(this._interval); this._interval = null; }
|
|
606
|
+
}
|
|
607
|
+
|
|
608
|
+
schedule(delayMs, callback) {
|
|
609
|
+
const ticks = Math.max(1, Math.round(delayMs / this.tickMs));
|
|
610
|
+
const slot = (this.current + ticks) & (this.buckets - 1);
|
|
611
|
+
const id = ++this._nextId;
|
|
612
|
+
this.wheel[slot].push({ cb: callback, id });
|
|
613
|
+
this._pending++;
|
|
614
|
+
return id;
|
|
615
|
+
}
|
|
616
|
+
|
|
617
|
+
cancel(id) {
|
|
618
|
+
for (let i = 0; i < this.buckets; i++) {
|
|
619
|
+
const bucket = this.wheel[i];
|
|
620
|
+
for (let j = 0; j < bucket.length; j++) {
|
|
621
|
+
if (bucket[j].id === id) {
|
|
622
|
+
bucket[j] = bucket[bucket.length - 1];
|
|
623
|
+
bucket.pop();
|
|
624
|
+
this._pending--;
|
|
625
|
+
return true;
|
|
626
|
+
}
|
|
627
|
+
}
|
|
628
|
+
}
|
|
629
|
+
return false;
|
|
630
|
+
}
|
|
631
|
+
|
|
632
|
+
_tick() {
|
|
633
|
+
this.current = (this.current + 1) & (this.buckets - 1);
|
|
634
|
+
const bucket = this.wheel[this.current];
|
|
635
|
+
if (bucket.length === 0) return;
|
|
636
|
+
const batch = bucket.splice(0);
|
|
637
|
+
this._pending -= batch.length;
|
|
638
|
+
for (let i = 0; i < batch.length; i++) {
|
|
639
|
+
try { batch[i].cb(); } catch {}
|
|
640
|
+
}
|
|
641
|
+
}
|
|
642
|
+
|
|
643
|
+
get pendingCount() { return this._pending; }
|
|
644
|
+
}
|
|
645
|
+
|
|
646
|
+
// ═══════════════════════════════════════════════════════════════
|
|
647
|
+
// AsyncBatchQueue – Coalesces async operations into batches
|
|
648
|
+
// At 10K accounts, individual Redis calls or API reports create
|
|
649
|
+
// massive overhead. BatchQueue accumulates items and flushes them
|
|
650
|
+
// in one call (e.g., Redis pipeline, bulk HTTP POST).
|
|
651
|
+
// ═══════════════════════════════════════════════════════════════
|
|
652
|
+
class AsyncBatchQueue {
|
|
653
|
+
constructor(flushFn, opts = {}) {
|
|
654
|
+
this.flushFn = flushFn;
|
|
655
|
+
this.maxSize = opts.maxSize || 50;
|
|
656
|
+
this.flushMs = opts.flushMs || 1000;
|
|
657
|
+
this.queue = [];
|
|
658
|
+
this._timer = null;
|
|
659
|
+
}
|
|
660
|
+
|
|
661
|
+
push(item) {
|
|
662
|
+
this.queue.push(item);
|
|
663
|
+
if (this.queue.length >= this.maxSize) {
|
|
664
|
+
this._flush();
|
|
665
|
+
} else if (!this._timer) {
|
|
666
|
+
this._timer = setTimeout(() => this._flush(), this.flushMs);
|
|
667
|
+
if (this._timer.unref) this._timer.unref();
|
|
668
|
+
}
|
|
669
|
+
}
|
|
670
|
+
|
|
671
|
+
_flush() {
|
|
672
|
+
if (this._timer) { clearTimeout(this._timer); this._timer = null; }
|
|
673
|
+
if (this.queue.length === 0) return;
|
|
674
|
+
const batch = this.queue.splice(0);
|
|
675
|
+
try { this.flushFn(batch); } catch {}
|
|
676
|
+
}
|
|
677
|
+
|
|
678
|
+
flush() { this._flush(); }
|
|
679
|
+
get pending() { return this.queue.length; }
|
|
680
|
+
destroy() { if (this._timer) clearTimeout(this._timer); this.queue.length = 0; }
|
|
681
|
+
}
|
|
682
|
+
|
|
683
|
+
// ═══════════════════════════════════════════════════════════════
|
|
684
|
+
// JitterBackoff – Decorrelated jitter (AWS-style), O(1)
|
|
685
|
+
// Standard exponential backoff causes "thundering herd" when 10K
|
|
686
|
+
// accounts retry simultaneously. Decorrelated jitter spreads them:
|
|
687
|
+
// sleep = min(cap, random_between(base, sleep_prev * 3))
|
|
688
|
+
// This is the recommended strategy from AWS Architecture Blog.
|
|
689
|
+
// ═══════════════════════════════════════════════════════════════
|
|
690
|
+
class JitterBackoff {
|
|
691
|
+
constructor(baseMs = 1000, capMs = 30000) {
|
|
692
|
+
this.baseMs = baseMs;
|
|
693
|
+
this.capMs = capMs;
|
|
694
|
+
this._sleep = baseMs;
|
|
695
|
+
this.attempt = 0;
|
|
696
|
+
}
|
|
697
|
+
|
|
698
|
+
next() {
|
|
699
|
+
this._sleep = Math.min(this.capMs, this.baseMs + Math.random() * (this._sleep * 3 - this.baseMs));
|
|
700
|
+
this.attempt++;
|
|
701
|
+
return this._sleep;
|
|
702
|
+
}
|
|
703
|
+
|
|
704
|
+
reset() { this._sleep = this.baseMs; this.attempt = 0; }
|
|
705
|
+
get current() { return this._sleep; }
|
|
706
|
+
}
|
|
707
|
+
|
|
708
|
+
module.exports = {
|
|
709
|
+
BloomFilter,
|
|
710
|
+
LRUCache,
|
|
711
|
+
RingBuffer,
|
|
712
|
+
Trie,
|
|
713
|
+
AhoCorasick,
|
|
714
|
+
TokenBucket,
|
|
715
|
+
EMA,
|
|
716
|
+
SlidingWindowCounter,
|
|
717
|
+
MinHeap,
|
|
718
|
+
VoseAlias,
|
|
719
|
+
StringPool,
|
|
720
|
+
BitSet,
|
|
721
|
+
ObjectPool,
|
|
722
|
+
TimerWheel,
|
|
723
|
+
AsyncBatchQueue,
|
|
724
|
+
JitterBackoff,
|
|
725
|
+
};
|