@mblaney/holster 1.0.11 → 1.0.13
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/build/holster.js +3 -3
- package/build/holster.js.map +3 -3
- package/package.json +1 -1
- package/src/radisk.js +31 -7
- package/src/wire.js +114 -17
package/package.json
CHANGED
package/src/radisk.js
CHANGED
|
@@ -15,6 +15,8 @@ const Radisk = opt => {
|
|
|
15
15
|
|
|
16
16
|
// Multi-file cache for all parsed radix trees
|
|
17
17
|
const cache = new Map()
|
|
18
|
+
// Pending reads queue to avoid duplicate reads for the same key
|
|
19
|
+
const pendingReads = new Map()
|
|
18
20
|
|
|
19
21
|
if (!opt) opt = {}
|
|
20
22
|
if (!opt.log) opt.log = console.log
|
|
@@ -43,8 +45,8 @@ const Radisk = opt => {
|
|
|
43
45
|
// Performance logging
|
|
44
46
|
const perfLog = (operation, startTime, key, size) => {
|
|
45
47
|
const duration = Date.now() - startTime
|
|
46
|
-
if (duration >
|
|
47
|
-
// Log operations taking more than
|
|
48
|
+
if (duration > 1000) {
|
|
49
|
+
// Log operations taking more than 1000ms
|
|
48
50
|
console.log(
|
|
49
51
|
`[RADISK-SLOW] ${operation}: ${duration}ms for ${key}${size ? ` (${size} bytes)` : ""}`,
|
|
50
52
|
)
|
|
@@ -76,10 +78,23 @@ const Radisk = opt => {
|
|
|
76
78
|
}
|
|
77
79
|
}
|
|
78
80
|
|
|
81
|
+
// Check if there's already a pending read for this key
|
|
82
|
+
if (pendingReads.has(key)) {
|
|
83
|
+
// Add callback to existing pending read
|
|
84
|
+
pendingReads.get(key).push(cb)
|
|
85
|
+
return
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
// Start new read and create callback queue
|
|
89
|
+
pendingReads.set(key, [cb])
|
|
90
|
+
|
|
79
91
|
const readStart = Date.now()
|
|
80
92
|
return radisk.read(key, (err, result) => {
|
|
81
93
|
perfLog("read", readStart, key)
|
|
82
|
-
|
|
94
|
+
// Execute all pending callbacks for this key
|
|
95
|
+
const callbacks = pendingReads.get(key) || []
|
|
96
|
+
pendingReads.delete(key)
|
|
97
|
+
callbacks.forEach(callback => callback(err, result))
|
|
83
98
|
})
|
|
84
99
|
}
|
|
85
100
|
|
|
@@ -123,7 +138,7 @@ const Radisk = opt => {
|
|
|
123
138
|
// file needs to be split.
|
|
124
139
|
if (++i > 1) return
|
|
125
140
|
|
|
126
|
-
perfLog("thrash", thrashStart, `batch-${
|
|
141
|
+
perfLog("thrash", thrashStart, `batch-${batch.ed}`)
|
|
127
142
|
if (err) opt.log(err)
|
|
128
143
|
batch.acks.forEach(cb => cb(err))
|
|
129
144
|
radisk.thrash.at = null
|
|
@@ -319,6 +334,7 @@ const Radisk = opt => {
|
|
|
319
334
|
if (!data) return cb(u, parse.disk)
|
|
320
335
|
|
|
321
336
|
let pre = []
|
|
337
|
+
let preString = ""
|
|
322
338
|
// Work though data by splitting into 3 values. The first value says
|
|
323
339
|
// if the second value is one of: the radix level for a key, the key
|
|
324
340
|
// iteself, or a value. The third is the rest of the data to work with.
|
|
@@ -330,15 +346,23 @@ const Radisk = opt => {
|
|
|
330
346
|
tmp = parse.split(tmp[2]) || ""
|
|
331
347
|
if (tmp[0] === "#") {
|
|
332
348
|
key = tmp[1]
|
|
333
|
-
|
|
334
|
-
if (i
|
|
349
|
+
// Optimize prefix building - avoid repeated joins
|
|
350
|
+
if (i < pre.length) {
|
|
351
|
+
pre.length = i
|
|
352
|
+
}
|
|
353
|
+
if (i <= pre.length) {
|
|
354
|
+
pre[i] = key
|
|
355
|
+
pre.length = i + 1
|
|
356
|
+
}
|
|
357
|
+
// Only rebuild preString when prefix changes
|
|
358
|
+
preString = pre.join("")
|
|
335
359
|
}
|
|
336
360
|
tmp = parse.split(tmp[2]) || ""
|
|
337
361
|
if (tmp[0] === "\n") continue
|
|
338
362
|
|
|
339
363
|
if (tmp[0] === "=") value = tmp[1]
|
|
340
364
|
if (typeof key !== "undefined" && typeof value !== "undefined") {
|
|
341
|
-
parse.disk(
|
|
365
|
+
parse.disk(preString, value)
|
|
342
366
|
}
|
|
343
367
|
tmp = parse.split(tmp[2])
|
|
344
368
|
}
|
package/src/wire.js
CHANGED
|
@@ -17,6 +17,7 @@ const createRateLimiter = opt => {
|
|
|
17
17
|
const clients = new Map()
|
|
18
18
|
const maxRequests = opt.maxRequestsPerMinute || 100
|
|
19
19
|
const windowMs = opt.rateLimitWindow || 60000
|
|
20
|
+
const disconnectThreshold = opt.disconnectThreshold || 10
|
|
20
21
|
// Check if test environment (mock-socket usage)
|
|
21
22
|
const isTestEnv = opt.wss && opt.wss.constructor.name === "Server"
|
|
22
23
|
let cleanupInterval = null
|
|
@@ -29,6 +30,10 @@ const createRateLimiter = opt => {
|
|
|
29
30
|
data.lastCleanup = now
|
|
30
31
|
}
|
|
31
32
|
data.requests = data.requests.filter(time => now - time < windowMs)
|
|
33
|
+
// Reset throttle counts periodically
|
|
34
|
+
if (now - data.lastCleanup > windowMs * 10) {
|
|
35
|
+
data.throttleCount = 0
|
|
36
|
+
}
|
|
32
37
|
}
|
|
33
38
|
}
|
|
34
39
|
|
|
@@ -42,6 +47,7 @@ const createRateLimiter = opt => {
|
|
|
42
47
|
const client = clients.get(clientId) || {
|
|
43
48
|
requests: [],
|
|
44
49
|
lastCleanup: now,
|
|
50
|
+
throttleCount: 0,
|
|
45
51
|
}
|
|
46
52
|
|
|
47
53
|
// Filter old requests
|
|
@@ -51,6 +57,9 @@ const createRateLimiter = opt => {
|
|
|
51
57
|
// Calculate delay based on oldest request that will expire
|
|
52
58
|
const oldestRequest = Math.min(...client.requests)
|
|
53
59
|
const delay = windowMs - (now - oldestRequest)
|
|
60
|
+
// Increment throttle count and update client data
|
|
61
|
+
client.throttleCount = (client.throttleCount || 0) + 1
|
|
62
|
+
clients.set(clientId, client)
|
|
54
63
|
return Math.max(0, delay)
|
|
55
64
|
}
|
|
56
65
|
|
|
@@ -71,6 +80,17 @@ const createRateLimiter = opt => {
|
|
|
71
80
|
return Math.max(0, maxRequests - validRequests.length)
|
|
72
81
|
},
|
|
73
82
|
|
|
83
|
+
getThrottleCount: clientId => {
|
|
84
|
+
const client = clients.get(clientId)
|
|
85
|
+
return client ? client.throttleCount || 0 : 0
|
|
86
|
+
},
|
|
87
|
+
|
|
88
|
+
shouldDisconnect: clientId => {
|
|
89
|
+
const client = clients.get(clientId)
|
|
90
|
+
if (!client) return false
|
|
91
|
+
return client.throttleCount >= disconnectThreshold
|
|
92
|
+
},
|
|
93
|
+
|
|
74
94
|
destroy: () => {
|
|
75
95
|
if (cleanupInterval) {
|
|
76
96
|
clearInterval(cleanupInterval)
|
|
@@ -274,7 +294,11 @@ const Wire = opt => {
|
|
|
274
294
|
|
|
275
295
|
if (ack) {
|
|
276
296
|
// Also send request on the wire to check for updates.
|
|
277
|
-
send(request)
|
|
297
|
+
const sendResult = send(request)
|
|
298
|
+
if (sendResult && sendResult.err) {
|
|
299
|
+
cb({err: sendResult.err})
|
|
300
|
+
return
|
|
301
|
+
}
|
|
278
302
|
cb({put: ack})
|
|
279
303
|
return
|
|
280
304
|
}
|
|
@@ -282,7 +306,11 @@ const Wire = opt => {
|
|
|
282
306
|
store.get(lex, (err, ack) => {
|
|
283
307
|
if (ack) {
|
|
284
308
|
// Also send request on the wire to check for updates.
|
|
285
|
-
send(request)
|
|
309
|
+
const sendResult = send(request)
|
|
310
|
+
if (sendResult && sendResult.err) {
|
|
311
|
+
cb({err: sendResult.err})
|
|
312
|
+
return
|
|
313
|
+
}
|
|
286
314
|
cb({put: ack, err: err})
|
|
287
315
|
return
|
|
288
316
|
}
|
|
@@ -290,7 +318,13 @@ const Wire = opt => {
|
|
|
290
318
|
if (err) console.log(err)
|
|
291
319
|
|
|
292
320
|
queue[track] = cb
|
|
293
|
-
send(request)
|
|
321
|
+
const sendResult = send(request)
|
|
322
|
+
if (sendResult && sendResult.err) {
|
|
323
|
+
cb({err: sendResult.err})
|
|
324
|
+
delete queue[track]
|
|
325
|
+
return
|
|
326
|
+
}
|
|
327
|
+
|
|
294
328
|
// Respond to callback with null if no response.
|
|
295
329
|
setTimeout(() => {
|
|
296
330
|
const cb = queue[track]
|
|
@@ -344,12 +378,17 @@ const Wire = opt => {
|
|
|
344
378
|
|
|
345
379
|
store.put(update.now, cb)
|
|
346
380
|
// Also put data on the wire spec.
|
|
347
|
-
send(
|
|
381
|
+
const sendResult = send(
|
|
348
382
|
JSON.stringify({
|
|
349
383
|
"#": dup.track(utils.text.random(9)),
|
|
350
384
|
put: data,
|
|
351
385
|
}),
|
|
352
386
|
)
|
|
387
|
+
// Handle queue overflow error
|
|
388
|
+
if (sendResult && sendResult.err) {
|
|
389
|
+
if (cb) cb(sendResult.err)
|
|
390
|
+
return
|
|
391
|
+
}
|
|
353
392
|
},
|
|
354
393
|
on: (lex, cb, _get, _opt) => {
|
|
355
394
|
const soul = lex && lex["#"]
|
|
@@ -464,9 +503,19 @@ const Wire = opt => {
|
|
|
464
503
|
const delay = rateLimiter.getDelay(clientId)
|
|
465
504
|
// Log rate limiting activity and enforce stricter limits
|
|
466
505
|
if (delay > 0) {
|
|
506
|
+
const throttleCount = rateLimiter.getThrottleCount(clientId)
|
|
467
507
|
console.log(
|
|
468
|
-
`[HOLSTER-THROTTLE] Client ${clientId}: rate limit exceeded, delay would be ${delay}ms, dropping message`,
|
|
508
|
+
`[HOLSTER-THROTTLE] Client ${clientId}: rate limit exceeded, delay would be ${delay}ms, dropping message (throttle count: ${throttleCount})`,
|
|
469
509
|
)
|
|
510
|
+
// Check if we should disconnect the bad actor
|
|
511
|
+
if (rateLimiter.shouldDisconnect(clientId)) {
|
|
512
|
+
console.error(
|
|
513
|
+
`[HOLSTER-DISCONNECT] Client ${clientId}: Disconnecting bad actor after ${throttleCount} throttle violations`,
|
|
514
|
+
)
|
|
515
|
+
ws.close(1008, "Rate limit violations - bad actor")
|
|
516
|
+
return
|
|
517
|
+
}
|
|
518
|
+
|
|
470
519
|
// Send throttle warning back to client instead of processing
|
|
471
520
|
ws.send(
|
|
472
521
|
JSON.stringify({
|
|
@@ -514,6 +563,47 @@ const Wire = opt => {
|
|
|
514
563
|
// Client-side throttling state
|
|
515
564
|
let clientThrottled = false
|
|
516
565
|
let throttleUntil = 0
|
|
566
|
+
let messageQueue = []
|
|
567
|
+
let queueProcessor = null
|
|
568
|
+
const maxQueueLength = opt.maxQueueLength || 1000
|
|
569
|
+
|
|
570
|
+
const processQueue = () => {
|
|
571
|
+
if (messageQueue.length === 0) {
|
|
572
|
+
queueProcessor = null
|
|
573
|
+
return
|
|
574
|
+
}
|
|
575
|
+
|
|
576
|
+
const now = Date.now()
|
|
577
|
+
if (clientThrottled && now < throttleUntil) {
|
|
578
|
+
// Still throttled, schedule next queue check
|
|
579
|
+
queueProcessor = setTimeout(
|
|
580
|
+
processQueue,
|
|
581
|
+
Math.min(1000, throttleUntil - now),
|
|
582
|
+
)
|
|
583
|
+
return
|
|
584
|
+
}
|
|
585
|
+
|
|
586
|
+
// Throttle period ended, resume processing
|
|
587
|
+
if (clientThrottled && now >= throttleUntil) {
|
|
588
|
+
console.log(
|
|
589
|
+
`[CLIENT-THROTTLED] Throttle period ended, resuming queue processing`,
|
|
590
|
+
)
|
|
591
|
+
clientThrottled = false
|
|
592
|
+
throttleUntil = 0
|
|
593
|
+
}
|
|
594
|
+
|
|
595
|
+
// Process next message in queue
|
|
596
|
+
const data = messageQueue.shift()
|
|
597
|
+
sendToPeers(data)
|
|
598
|
+
|
|
599
|
+
// Schedule processing of next message if queue not empty
|
|
600
|
+
if (messageQueue.length > 0) {
|
|
601
|
+
// Add small delay between messages to avoid immediate re-throttling
|
|
602
|
+
queueProcessor = setTimeout(processQueue, 100)
|
|
603
|
+
} else {
|
|
604
|
+
queueProcessor = null
|
|
605
|
+
}
|
|
606
|
+
}
|
|
517
607
|
|
|
518
608
|
const sendToPeers = data => {
|
|
519
609
|
peers.forEach(peer => {
|
|
@@ -539,22 +629,29 @@ const Wire = opt => {
|
|
|
539
629
|
}
|
|
540
630
|
|
|
541
631
|
const send = data => {
|
|
542
|
-
// Check if client is throttled
|
|
543
|
-
|
|
544
|
-
|
|
545
|
-
|
|
632
|
+
// Check if client is throttled or queue is active
|
|
633
|
+
if (clientThrottled || messageQueue.length > 0) {
|
|
634
|
+
if (messageQueue.length >= maxQueueLength) {
|
|
635
|
+
return {
|
|
636
|
+
err: `Message queue exceeded maximum length (${maxQueueLength}). Update query logic to request less data.`,
|
|
637
|
+
queueLength: messageQueue.length,
|
|
638
|
+
maxQueueLength: maxQueueLength,
|
|
639
|
+
}
|
|
640
|
+
}
|
|
641
|
+
|
|
642
|
+
// Add message to queue instead of sending immediately
|
|
643
|
+
messageQueue.push(data)
|
|
546
644
|
console.log(
|
|
547
|
-
`
|
|
645
|
+
`Queuing message (queue length: ${messageQueue.length}/${maxQueueLength})`,
|
|
548
646
|
)
|
|
549
|
-
|
|
647
|
+
// Start queue processor if not already running
|
|
648
|
+
if (!queueProcessor) {
|
|
649
|
+
queueProcessor = setTimeout(processQueue, 100)
|
|
650
|
+
}
|
|
550
651
|
return
|
|
551
|
-
} else if (clientThrottled && now >= throttleUntil) {
|
|
552
|
-
console.log(
|
|
553
|
-
`[CLIENT-THROTTLED] Throttle period ended, resuming normal operation`,
|
|
554
|
-
)
|
|
555
|
-
clientThrottled = false
|
|
556
|
-
throttleUntil = 0
|
|
557
652
|
}
|
|
653
|
+
|
|
654
|
+
// Normal case - send immediately
|
|
558
655
|
sendToPeers(data)
|
|
559
656
|
}
|
|
560
657
|
if (!(opt.peers instanceof Array)) {
|