@mblaney/holster 1.0.10 → 1.0.12
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 +73 -28
- package/src/wire.js +96 -1
package/package.json
CHANGED
package/src/radisk.js
CHANGED
|
@@ -12,11 +12,13 @@ const unit = String.fromCharCode(31)
|
|
|
12
12
|
// opt.store interface.
|
|
13
13
|
const Radisk = opt => {
|
|
14
14
|
var u
|
|
15
|
-
|
|
15
|
+
|
|
16
|
+
// Multi-file cache for all parsed radix trees
|
|
17
|
+
const cache = new Map()
|
|
16
18
|
|
|
17
19
|
if (!opt) opt = {}
|
|
18
20
|
if (!opt.log) opt.log = console.log
|
|
19
|
-
if (!opt.batch) opt.batch =
|
|
21
|
+
if (!opt.batch) opt.batch = 100 // Balanced batch size for performance
|
|
20
22
|
if (!opt.write) opt.write = 1 // Wait time before write in milliseconds.
|
|
21
23
|
if (!opt.size) opt.size = 1024 * 1024 // File size on disk, default 1MB.
|
|
22
24
|
if (!opt.store) {
|
|
@@ -38,6 +40,17 @@ const Radisk = opt => {
|
|
|
38
40
|
return
|
|
39
41
|
}
|
|
40
42
|
|
|
43
|
+
// Performance logging
|
|
44
|
+
const perfLog = (operation, startTime, key, size) => {
|
|
45
|
+
const duration = Date.now() - startTime
|
|
46
|
+
if (duration > 500) {
|
|
47
|
+
// Log operations taking more than 500ms
|
|
48
|
+
console.log(
|
|
49
|
+
`[RADISK-SLOW] ${operation}: ${duration}ms for ${key}${size ? ` (${size} bytes)` : ""}`,
|
|
50
|
+
)
|
|
51
|
+
}
|
|
52
|
+
}
|
|
53
|
+
|
|
41
54
|
// Any and all storage adapters should:
|
|
42
55
|
// 1. Because writing to disk takes time, we should batch data to disk.
|
|
43
56
|
// This improves performance, and reduces potential disk corruption.
|
|
@@ -63,7 +76,11 @@ const Radisk = opt => {
|
|
|
63
76
|
}
|
|
64
77
|
}
|
|
65
78
|
|
|
66
|
-
|
|
79
|
+
const readStart = Date.now()
|
|
80
|
+
return radisk.read(key, (err, result) => {
|
|
81
|
+
perfLog("read", readStart, key)
|
|
82
|
+
cb(err, result)
|
|
83
|
+
})
|
|
67
84
|
}
|
|
68
85
|
|
|
69
86
|
// Otherwise store the value provided.
|
|
@@ -90,6 +107,7 @@ const Radisk = opt => {
|
|
|
90
107
|
return (radisk.thrash.more = true)
|
|
91
108
|
}
|
|
92
109
|
|
|
110
|
+
const thrashStart = Date.now()
|
|
93
111
|
clearTimeout(radisk.batch.timeout)
|
|
94
112
|
radisk.thrash.more = false
|
|
95
113
|
radisk.thrash.ing = true
|
|
@@ -105,6 +123,7 @@ const Radisk = opt => {
|
|
|
105
123
|
// file needs to be split.
|
|
106
124
|
if (++i > 1) return
|
|
107
125
|
|
|
126
|
+
perfLog("thrash", thrashStart, `batch-${radisk.batch.ed}`)
|
|
108
127
|
if (err) opt.log(err)
|
|
109
128
|
batch.acks.forEach(cb => cb(err))
|
|
110
129
|
radisk.thrash.at = null
|
|
@@ -143,9 +162,9 @@ const Radisk = opt => {
|
|
|
143
162
|
},
|
|
144
163
|
mix: (file, start, end) => {
|
|
145
164
|
save.start = save.end = save.file = u
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
165
|
+
// Use cache if available, otherwise parse from disk
|
|
166
|
+
if (cache.has(file)) {
|
|
167
|
+
const disk = cache.get(file)
|
|
149
168
|
Radix.map(rad, (value, key) => {
|
|
150
169
|
if (key < start) return
|
|
151
170
|
|
|
@@ -157,7 +176,23 @@ const Radisk = opt => {
|
|
|
157
176
|
disk(key, value)
|
|
158
177
|
})
|
|
159
178
|
radisk.write(file, disk, save.next)
|
|
160
|
-
}
|
|
179
|
+
} else {
|
|
180
|
+
radisk.parse(file, (err, disk) => {
|
|
181
|
+
if (err) return cb(err)
|
|
182
|
+
|
|
183
|
+
Radix.map(rad, (value, key) => {
|
|
184
|
+
if (key < start) return
|
|
185
|
+
|
|
186
|
+
if (end && end < key) {
|
|
187
|
+
save.start = key
|
|
188
|
+
return
|
|
189
|
+
}
|
|
190
|
+
|
|
191
|
+
disk(key, value)
|
|
192
|
+
})
|
|
193
|
+
radisk.write(file, disk, save.next)
|
|
194
|
+
})
|
|
195
|
+
}
|
|
161
196
|
},
|
|
162
197
|
next: err => {
|
|
163
198
|
if (err) return cb(err)
|
|
@@ -171,8 +206,8 @@ const Radisk = opt => {
|
|
|
171
206
|
}
|
|
172
207
|
|
|
173
208
|
radisk.write = (file, rad, cb) => {
|
|
174
|
-
// Invalidate cache on write
|
|
175
|
-
cache
|
|
209
|
+
// Invalidate cache entry for this file on write
|
|
210
|
+
cache.delete(file)
|
|
176
211
|
const write = {
|
|
177
212
|
text: "",
|
|
178
213
|
limit: "",
|
|
@@ -218,14 +253,14 @@ const Radisk = opt => {
|
|
|
218
253
|
Radix.map(rad, write.each, true)
|
|
219
254
|
// There is always accumulated write.text to store once write.each has
|
|
220
255
|
// finished.
|
|
221
|
-
|
|
256
|
+
const writeStart = Date.now()
|
|
257
|
+
opt.store.put(file, write.text, err => {
|
|
258
|
+
perfLog("file-write", writeStart, file, write.text.length)
|
|
259
|
+
cb(err)
|
|
260
|
+
})
|
|
222
261
|
}
|
|
223
262
|
|
|
224
263
|
radisk.read = (key, cb) => {
|
|
225
|
-
if (cache) {
|
|
226
|
-
let value = cache(key)
|
|
227
|
-
if (typeof value !== "undefined") return cb(u, value)
|
|
228
|
-
}
|
|
229
264
|
// Only the soul of the key is compared to filenames (see radisk.write).
|
|
230
265
|
const end = key.indexOf(enq)
|
|
231
266
|
const soul = end === -1 ? key : key.substring(0, end)
|
|
@@ -240,6 +275,14 @@ const Radisk = opt => {
|
|
|
240
275
|
return
|
|
241
276
|
}
|
|
242
277
|
|
|
278
|
+
// Check multi-file cache first
|
|
279
|
+
if (cache.has(read.file)) {
|
|
280
|
+
const cachedRadix = cache.get(read.file)
|
|
281
|
+
read.value = cachedRadix(key)
|
|
282
|
+
if (typeof read.value !== "undefined") {
|
|
283
|
+
return cb(u, read.value)
|
|
284
|
+
}
|
|
285
|
+
}
|
|
243
286
|
radisk.parse(read.file, read.it)
|
|
244
287
|
return
|
|
245
288
|
}
|
|
@@ -252,7 +295,8 @@ const Radisk = opt => {
|
|
|
252
295
|
it: (err, disk) => {
|
|
253
296
|
if (err) opt.log(err)
|
|
254
297
|
if (disk) {
|
|
255
|
-
|
|
298
|
+
// Store in multi-file cache
|
|
299
|
+
cache.set(read.file, disk)
|
|
256
300
|
read.value = disk(key)
|
|
257
301
|
}
|
|
258
302
|
cb(err, read.value)
|
|
@@ -303,18 +347,12 @@ const Radisk = opt => {
|
|
|
303
347
|
split: data => {
|
|
304
348
|
if (!data) return
|
|
305
349
|
|
|
306
|
-
|
|
307
|
-
|
|
308
|
-
let c = null
|
|
309
|
-
while ((c = data[++i])) {
|
|
310
|
-
if (c === unit) break
|
|
350
|
+
const i = data.indexOf(unit)
|
|
351
|
+
if (i === -1) return
|
|
311
352
|
|
|
312
|
-
|
|
313
|
-
}
|
|
353
|
+
const a = data.slice(0, i)
|
|
314
354
|
let o = {}
|
|
315
|
-
|
|
316
|
-
return [a, Radisk.decode(data.slice(i), o), data.slice(i + o.i)]
|
|
317
|
-
}
|
|
355
|
+
return [a, Radisk.decode(data.slice(i), o), data.slice(i + o.i)]
|
|
318
356
|
},
|
|
319
357
|
}
|
|
320
358
|
opt.store.get(file, parse.read)
|
|
@@ -355,21 +393,25 @@ Radisk.encode = data => {
|
|
|
355
393
|
}
|
|
356
394
|
|
|
357
395
|
Radisk.decode = (data, obj) => {
|
|
358
|
-
var text = ""
|
|
359
396
|
var i = -1
|
|
360
397
|
var n = 0
|
|
361
398
|
var current = null
|
|
362
399
|
var previous = null
|
|
400
|
+
var textStart = -1
|
|
401
|
+
var textEnd = -1
|
|
363
402
|
if (data[0] !== unit) return
|
|
364
403
|
|
|
365
404
|
// Find a control character previous to the text we want, skipping
|
|
366
405
|
// consecutive unit separator characters at the beginning of the data.
|
|
367
406
|
while ((current = data[++i])) {
|
|
368
407
|
if (previous) {
|
|
408
|
+
if (textStart === -1) textStart = i
|
|
369
409
|
if (current === unit) {
|
|
370
|
-
if (--n <= 0)
|
|
410
|
+
if (--n <= 0) {
|
|
411
|
+
textEnd = i
|
|
412
|
+
break
|
|
413
|
+
}
|
|
371
414
|
}
|
|
372
|
-
text += current
|
|
373
415
|
} else if (current === unit) {
|
|
374
416
|
n++
|
|
375
417
|
} else {
|
|
@@ -377,6 +419,9 @@ Radisk.decode = (data, obj) => {
|
|
|
377
419
|
}
|
|
378
420
|
}
|
|
379
421
|
|
|
422
|
+
const text =
|
|
423
|
+
textStart !== -1 ? data.slice(textStart, textEnd !== -1 ? textEnd : i) : ""
|
|
424
|
+
|
|
380
425
|
if (obj) obj.i = i + 1
|
|
381
426
|
|
|
382
427
|
let [value, state] = text.split(etx)
|
package/src/wire.js
CHANGED
|
@@ -424,6 +424,9 @@ const Wire = opt => {
|
|
|
424
424
|
|
|
425
425
|
// Generate unique client ID for rate limiting
|
|
426
426
|
const clientId = utils.text.random(9)
|
|
427
|
+
console.log(
|
|
428
|
+
`[HOLSTER-CONNECT] New WebSocket client connected: ${clientId}`,
|
|
429
|
+
)
|
|
427
430
|
|
|
428
431
|
ws.on("error", error => {
|
|
429
432
|
console.error("WebSocket error:", error)
|
|
@@ -431,6 +434,9 @@ const Wire = opt => {
|
|
|
431
434
|
})
|
|
432
435
|
|
|
433
436
|
ws.on("close", () => {
|
|
437
|
+
console.log(
|
|
438
|
+
`[HOLSTER-DISCONNECT] WebSocket client disconnected: ${clientId}`,
|
|
439
|
+
)
|
|
434
440
|
connectionManager.remove(ws)
|
|
435
441
|
})
|
|
436
442
|
|
|
@@ -456,6 +462,22 @@ const Wire = opt => {
|
|
|
456
462
|
|
|
457
463
|
// Check rate limit and get delay
|
|
458
464
|
const delay = rateLimiter.getDelay(clientId)
|
|
465
|
+
// Log rate limiting activity and enforce stricter limits
|
|
466
|
+
if (delay > 0) {
|
|
467
|
+
console.log(
|
|
468
|
+
`[HOLSTER-THROTTLE] Client ${clientId}: rate limit exceeded, delay would be ${delay}ms, dropping message`,
|
|
469
|
+
)
|
|
470
|
+
// Send throttle warning back to client instead of processing
|
|
471
|
+
ws.send(
|
|
472
|
+
JSON.stringify({
|
|
473
|
+
"#": dup.track(utils.text.random(9)),
|
|
474
|
+
"@": msg["#"],
|
|
475
|
+
err: `Rate limit exceeded. Slow down requests. Wait ${Math.ceil(delay / 1000)}s`,
|
|
476
|
+
throttle: delay,
|
|
477
|
+
}),
|
|
478
|
+
)
|
|
479
|
+
return
|
|
480
|
+
}
|
|
459
481
|
|
|
460
482
|
const processMessage = () => {
|
|
461
483
|
dup.track(msg["#"])
|
|
@@ -489,7 +511,51 @@ const Wire = opt => {
|
|
|
489
511
|
|
|
490
512
|
// Browser logic.
|
|
491
513
|
const peers = []
|
|
492
|
-
|
|
514
|
+
// Client-side throttling state
|
|
515
|
+
let clientThrottled = false
|
|
516
|
+
let throttleUntil = 0
|
|
517
|
+
let messageQueue = []
|
|
518
|
+
let queueProcessor = null
|
|
519
|
+
|
|
520
|
+
const processQueue = () => {
|
|
521
|
+
if (messageQueue.length === 0) {
|
|
522
|
+
queueProcessor = null
|
|
523
|
+
return
|
|
524
|
+
}
|
|
525
|
+
|
|
526
|
+
const now = Date.now()
|
|
527
|
+
if (clientThrottled && now < throttleUntil) {
|
|
528
|
+
// Still throttled, schedule next queue check
|
|
529
|
+
queueProcessor = setTimeout(
|
|
530
|
+
processQueue,
|
|
531
|
+
Math.min(1000, throttleUntil - now),
|
|
532
|
+
)
|
|
533
|
+
return
|
|
534
|
+
}
|
|
535
|
+
|
|
536
|
+
// Throttle period ended, resume processing
|
|
537
|
+
if (clientThrottled && now >= throttleUntil) {
|
|
538
|
+
console.log(
|
|
539
|
+
`[CLIENT-THROTTLED] Throttle period ended, resuming queue processing`,
|
|
540
|
+
)
|
|
541
|
+
clientThrottled = false
|
|
542
|
+
throttleUntil = 0
|
|
543
|
+
}
|
|
544
|
+
|
|
545
|
+
// Process next message in queue
|
|
546
|
+
const data = messageQueue.shift()
|
|
547
|
+
sendToPeers(data)
|
|
548
|
+
|
|
549
|
+
// Schedule processing of next message if queue not empty
|
|
550
|
+
if (messageQueue.length > 0) {
|
|
551
|
+
// Add small delay between messages to avoid immediate re-throttling
|
|
552
|
+
queueProcessor = setTimeout(processQueue, 100)
|
|
553
|
+
} else {
|
|
554
|
+
queueProcessor = null
|
|
555
|
+
}
|
|
556
|
+
}
|
|
557
|
+
|
|
558
|
+
const sendToPeers = data => {
|
|
493
559
|
peers.forEach(peer => {
|
|
494
560
|
if (peer && peer.readyState === WebSocket.OPEN) {
|
|
495
561
|
peer.send(data)
|
|
@@ -511,6 +577,27 @@ const Wire = opt => {
|
|
|
511
577
|
}
|
|
512
578
|
})
|
|
513
579
|
}
|
|
580
|
+
|
|
581
|
+
const send = data => {
|
|
582
|
+
// Check if client is throttled or queue is active
|
|
583
|
+
const now = Date.now()
|
|
584
|
+
if (clientThrottled || messageQueue.length > 0) {
|
|
585
|
+
// Add message to queue instead of sending immediately
|
|
586
|
+
messageQueue.push(data)
|
|
587
|
+
console.log(
|
|
588
|
+
`[CLIENT-THROTTLED] Queuing message (queue length: ${messageQueue.length})`,
|
|
589
|
+
)
|
|
590
|
+
|
|
591
|
+
// Start queue processor if not already running
|
|
592
|
+
if (!queueProcessor) {
|
|
593
|
+
queueProcessor = setTimeout(processQueue, 100)
|
|
594
|
+
}
|
|
595
|
+
return
|
|
596
|
+
}
|
|
597
|
+
|
|
598
|
+
// Normal case - send immediately
|
|
599
|
+
sendToPeers(data)
|
|
600
|
+
}
|
|
514
601
|
if (!(opt.peers instanceof Array)) {
|
|
515
602
|
opt.peers = [`ws://localhost:${opt.port || 8765}`]
|
|
516
603
|
}
|
|
@@ -544,6 +631,14 @@ const Wire = opt => {
|
|
|
544
631
|
const msg = JSON.parse(m.data)
|
|
545
632
|
if (dup.check(msg["#"])) return
|
|
546
633
|
|
|
634
|
+
// Handle throttle messages from server
|
|
635
|
+
if (msg.throttle && msg.err) {
|
|
636
|
+
console.log(`[CLIENT-THROTTLED] Server throttling: ${msg.err}`)
|
|
637
|
+
clientThrottled = true
|
|
638
|
+
throttleUntil = Date.now() + msg.throttle
|
|
639
|
+
return
|
|
640
|
+
}
|
|
641
|
+
|
|
547
642
|
dup.track(msg["#"])
|
|
548
643
|
if (msg.get) get(msg, send)
|
|
549
644
|
if (msg.put) {
|