@mblaney/holster 1.0.12 → 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 +64 -8
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({
|
|
@@ -516,6 +565,7 @@ const Wire = opt => {
|
|
|
516
565
|
let throttleUntil = 0
|
|
517
566
|
let messageQueue = []
|
|
518
567
|
let queueProcessor = null
|
|
568
|
+
const maxQueueLength = opt.maxQueueLength || 1000
|
|
519
569
|
|
|
520
570
|
const processQueue = () => {
|
|
521
571
|
if (messageQueue.length === 0) {
|
|
@@ -580,14 +630,20 @@ const Wire = opt => {
|
|
|
580
630
|
|
|
581
631
|
const send = data => {
|
|
582
632
|
// Check if client is throttled or queue is active
|
|
583
|
-
const now = Date.now()
|
|
584
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
|
+
|
|
585
642
|
// Add message to queue instead of sending immediately
|
|
586
643
|
messageQueue.push(data)
|
|
587
644
|
console.log(
|
|
588
|
-
`
|
|
645
|
+
`Queuing message (queue length: ${messageQueue.length}/${maxQueueLength})`,
|
|
589
646
|
)
|
|
590
|
-
|
|
591
647
|
// Start queue processor if not already running
|
|
592
648
|
if (!queueProcessor) {
|
|
593
649
|
queueProcessor = setTimeout(processQueue, 100)
|