@mblaney/holster 1.0.9 → 1.0.11

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.9",
3
+ "version": "1.0.11",
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/ham.js CHANGED
@@ -164,7 +164,7 @@ Ham.mix = async (change, graph, secure, listen) => {
164
164
  return {soul, maxState}
165
165
  })
166
166
  .sort((a, b) => a.maxState - b.maxState)
167
- const remove = soulsByAge.slice(0, souls.length - maxSize)
167
+ const remove = soulsByAge.slice(0, souls.length - MAX_GRAPH_SIZE)
168
168
  remove.forEach(({soul}) => delete graph[soul])
169
169
  }
170
170
 
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,11 @@ 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
+
518
+ const sendToPeers = data => {
493
519
  peers.forEach(peer => {
494
520
  if (peer && peer.readyState === WebSocket.OPEN) {
495
521
  peer.send(data)
@@ -511,6 +537,26 @@ const Wire = opt => {
511
537
  }
512
538
  })
513
539
  }
540
+
541
+ const send = data => {
542
+ // Check if client is throttled before sending
543
+ const now = Date.now()
544
+ if (clientThrottled && now < throttleUntil) {
545
+ const delay = throttleUntil - now
546
+ console.log(
547
+ `[CLIENT-THROTTLED] Delaying message for ${delay}ms, throttled until ${new Date(throttleUntil).toLocaleTimeString()}`,
548
+ )
549
+ setTimeout(() => sendToPeers(data), delay)
550
+ 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
+ }
558
+ sendToPeers(data)
559
+ }
514
560
  if (!(opt.peers instanceof Array)) {
515
561
  opt.peers = [`ws://localhost:${opt.port || 8765}`]
516
562
  }
@@ -544,6 +590,14 @@ const Wire = opt => {
544
590
  const msg = JSON.parse(m.data)
545
591
  if (dup.check(msg["#"])) return
546
592
 
593
+ // Handle throttle messages from server
594
+ if (msg.throttle && msg.err) {
595
+ console.log(`[CLIENT-THROTTLED] Server throttling: ${msg.err}`)
596
+ clientThrottled = true
597
+ throttleUntil = Date.now() + msg.throttle
598
+ return
599
+ }
600
+
547
601
  dup.track(msg["#"])
548
602
  if (msg.get) get(msg, send)
549
603
  if (msg.put) {