@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/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@mblaney/holster",
3
- "version": "1.0.10",
3
+ "version": "1.0.12",
4
4
  "description": "Holster is a service that allows synchronising data between devices using Node, Deno, Bun or the browser.",
5
5
  "type": "module",
6
6
  "main": "index.js",
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
- var cache = null
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 = 10 * 1000
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
- return radisk.read(key, cb)
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
- radisk.parse(file, (err, disk) => {
147
- if (err) return cb(err)
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 = null
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
- opt.store.put(file, write.text, cb)
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
- cache = disk
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
- let i = -1
307
- let a = ""
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
- a += c
313
- }
353
+ const a = data.slice(0, i)
314
354
  let o = {}
315
- if (c) {
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) break
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
- const send = data => {
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) {