@mblaney/holster 1.0.15 → 1.0.17
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/README.md +11 -6
- package/build/holster.js +3 -3
- package/build/holster.js.map +3 -3
- package/package.json +1 -1
- package/src/holster.js +78 -14
- package/src/radisk.js +1 -1
- package/src/wire.js +83 -57
- package/test/holster.user.on.test.js +38 -32
- package/test/system/basic.test.js +29 -0
- package/test/system/chained-on.test.js +41 -0
- package/test/system/concurrent-listeners.test.js +124 -0
- package/test/system/immediate-read.test.js +30 -0
- package/test/system/listener-before-data.test.js +50 -0
- package/test/system/promise-wrapper.test.js +47 -0
- package/test/system/user-chained-on.test.js +56 -0
- package/test/system/user-concurrent-listeners.test.js +140 -0
- package/test/system/user-listener-before-data.test.js +64 -0
package/package.json
CHANGED
package/src/holster.js
CHANGED
|
@@ -67,12 +67,22 @@ const Holster = opt => {
|
|
|
67
67
|
for (const key of Object.keys(msg.put[soul])) {
|
|
68
68
|
const id = utils.rel.is(msg.put[soul][key])
|
|
69
69
|
if (id) {
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
70
|
+
// Due to message queuing, the related node might not be available yet
|
|
71
|
+
// Retry with delays if we get null
|
|
72
|
+
const attemptRead = async (retries = 0) => {
|
|
73
|
+
const data = await new Promise(res => {
|
|
74
|
+
const _ctxid = utils.text.random()
|
|
75
|
+
allctx.set(_ctxid, {chain: [{item: null, soul: id}]})
|
|
76
|
+
api(_ctxid).next(null, res, _opt)
|
|
77
|
+
})
|
|
78
|
+
if (data !== null || retries >= 5) {
|
|
79
|
+
return data
|
|
80
|
+
}
|
|
81
|
+
// Data not ready, retry after delay
|
|
82
|
+
await new Promise(resolve => setTimeout(resolve, 50))
|
|
83
|
+
return attemptRead(retries + 1)
|
|
84
|
+
}
|
|
85
|
+
msg.put[soul][key] = await attemptRead()
|
|
76
86
|
}
|
|
77
87
|
}
|
|
78
88
|
ack(msg.put[soul])
|
|
@@ -547,8 +557,57 @@ const Holster = opt => {
|
|
|
547
557
|
allctx.set(ctxid, {chain: [{item: item, soul: soul}], on: true})
|
|
548
558
|
// Map the user's callback because it can also be passed to off,
|
|
549
559
|
// so need a reference to it to compare them.
|
|
550
|
-
|
|
551
|
-
|
|
560
|
+
// Create a new context for each listener invocation to avoid mutation
|
|
561
|
+
map.set(cb, () => {
|
|
562
|
+
// When listener fires, re-check if the item is now a rel.
|
|
563
|
+
wire.get(
|
|
564
|
+
{"#": soul, ".": item},
|
|
565
|
+
msg => {
|
|
566
|
+
const current = msg.put && msg.put[soul] && msg.put[soul][item]
|
|
567
|
+
const id = utils.rel.is(current)
|
|
568
|
+
if (id) {
|
|
569
|
+
// It's a rel, read the related node. It might not be in the graph
|
|
570
|
+
// yet when the listener fires, so we retry with delays.
|
|
571
|
+
const attemptRead = (retries = 0) => {
|
|
572
|
+
wire.get(
|
|
573
|
+
{"#": id},
|
|
574
|
+
relMsg => {
|
|
575
|
+
if (relMsg.put && relMsg.put[id]) {
|
|
576
|
+
delete relMsg.put[id]._
|
|
577
|
+
delete relMsg.put[id][utils.userPublicKey]
|
|
578
|
+
delete relMsg.put[id][utils.userSignature]
|
|
579
|
+
cb(relMsg.put[id])
|
|
580
|
+
} else if (retries < 5) {
|
|
581
|
+
// Data not ready yet, retry after a delay
|
|
582
|
+
setTimeout(() => attemptRead(retries + 1), 50)
|
|
583
|
+
} else {
|
|
584
|
+
// Give up after retries
|
|
585
|
+
cb(null)
|
|
586
|
+
}
|
|
587
|
+
},
|
|
588
|
+
_opt,
|
|
589
|
+
)
|
|
590
|
+
}
|
|
591
|
+
attemptRead()
|
|
592
|
+
} else {
|
|
593
|
+
// It's a direct property or null
|
|
594
|
+
cb(current !== undefined ? current : null)
|
|
595
|
+
}
|
|
596
|
+
},
|
|
597
|
+
_opt,
|
|
598
|
+
)
|
|
599
|
+
})
|
|
600
|
+
|
|
601
|
+
// Register listener immediately to avoid missing updates due to
|
|
602
|
+
// queueing. Initially register the soul without _get, then update if
|
|
603
|
+
// it's a rel.
|
|
604
|
+
let initialLex
|
|
605
|
+
if (lex) initialLex = utils.obj.put(lex, "#", soul)
|
|
606
|
+
else initialLex = {"#": soul, ".": item}
|
|
607
|
+
wire.on(initialLex, map.get(cb), false, _opt)
|
|
608
|
+
|
|
609
|
+
// Check if item is a rel and update listener if needed.
|
|
610
|
+
// This happens async but the listener is already registered above.
|
|
552
611
|
wire.get(
|
|
553
612
|
{"#": soul, ".": item},
|
|
554
613
|
msg => {
|
|
@@ -560,13 +619,18 @@ const Holster = opt => {
|
|
|
560
619
|
const current = msg.put && msg.put[soul] && msg.put[soul][item]
|
|
561
620
|
const id = utils.rel.is(current)
|
|
562
621
|
if (id) {
|
|
563
|
-
|
|
564
|
-
|
|
565
|
-
|
|
566
|
-
|
|
567
|
-
|
|
622
|
+
// It's a rel, need to switch listener to the related node
|
|
623
|
+
// First remove the initial listener
|
|
624
|
+
wire.off(initialLex, map.get(cb))
|
|
625
|
+
// Then add listener on the related node
|
|
626
|
+
let relLex
|
|
627
|
+
if (lex) relLex = utils.obj.put(lex, "#", id)
|
|
628
|
+
else relLex = {"#": id, ".": null}
|
|
629
|
+
wire.on(relLex, map.get(cb), _get, _opt)
|
|
630
|
+
} else if (_get) {
|
|
631
|
+
// Not a rel, but _get was requested, so trigger callback.
|
|
632
|
+
map.get(cb)()
|
|
568
633
|
}
|
|
569
|
-
wire.on(lex, map.get(cb), _get, _opt)
|
|
570
634
|
},
|
|
571
635
|
_opt,
|
|
572
636
|
)
|
package/src/radisk.js
CHANGED
|
@@ -58,7 +58,7 @@ const Radisk = opt => {
|
|
|
58
58
|
// Performance logging
|
|
59
59
|
const perfLog = (operation, startTime, key, size) => {
|
|
60
60
|
const duration = Date.now() - startTime
|
|
61
|
-
if (duration >
|
|
61
|
+
if (duration > 2000) {
|
|
62
62
|
console.log(
|
|
63
63
|
`[RADISK-SLOW] ${operation}: ${duration}ms for ${key}${size ? ` (${size} bytes)` : ""}`,
|
|
64
64
|
)
|
package/src/wire.js
CHANGED
|
@@ -13,13 +13,11 @@ if (typeof globalThis.WebSocket === "undefined") {
|
|
|
13
13
|
}
|
|
14
14
|
|
|
15
15
|
// Rate limiting with throttling
|
|
16
|
-
const createRateLimiter =
|
|
16
|
+
const createRateLimiter = isTestEnv => {
|
|
17
17
|
const clients = new Map()
|
|
18
|
-
const maxRequests =
|
|
19
|
-
const windowMs =
|
|
20
|
-
const disconnectThreshold =
|
|
21
|
-
// Check if test environment (mock-socket usage)
|
|
22
|
-
const isTestEnv = opt.wss && opt.wss.constructor.name === "Server"
|
|
18
|
+
const maxRequests = 300 // 5 requests per second
|
|
19
|
+
const windowMs = 60000 // 1 minute window
|
|
20
|
+
const disconnectThreshold = 10 // Disconnect after 10 violations
|
|
23
21
|
let cleanupInterval = null
|
|
24
22
|
|
|
25
23
|
const cleanup = () => {
|
|
@@ -180,6 +178,9 @@ const Wire = opt => {
|
|
|
180
178
|
// Track references we want but don't have yet
|
|
181
179
|
const pendingReferences = new Set()
|
|
182
180
|
|
|
181
|
+
// Track pending timeouts that should start when message is sent
|
|
182
|
+
const pendingTimeouts = new Map()
|
|
183
|
+
|
|
183
184
|
// Helper to check if we have a soul in memory or storage
|
|
184
185
|
const hasSoul = async soul => {
|
|
185
186
|
if (graph[soul]) return true
|
|
@@ -191,7 +192,9 @@ const Wire = opt => {
|
|
|
191
192
|
}
|
|
192
193
|
|
|
193
194
|
// Initialize rate limiting and connection management
|
|
194
|
-
|
|
195
|
+
// Check if test environment (mock-socket usage)
|
|
196
|
+
const isTestEnv = opt.wss && opt.wss.constructor.name === "Server"
|
|
197
|
+
const rateLimiter = createRateLimiter(isTestEnv)
|
|
195
198
|
const connectionManager = createConnectionManager(opt.maxConnections || 1000)
|
|
196
199
|
|
|
197
200
|
// The check function is required because user data must provide a public key
|
|
@@ -318,24 +321,20 @@ const Wire = opt => {
|
|
|
318
321
|
if (err) console.log(err)
|
|
319
322
|
|
|
320
323
|
queue[track] = cb
|
|
324
|
+
|
|
325
|
+
// Store timeout config to start after message is sent from queue
|
|
326
|
+
pendingTimeouts.set(track, {
|
|
327
|
+
lex: lex,
|
|
328
|
+
wait: _opt.wait || 100,
|
|
329
|
+
})
|
|
330
|
+
|
|
321
331
|
const sendResult = send(request)
|
|
322
332
|
if (sendResult && sendResult.err) {
|
|
323
333
|
cb({err: sendResult.err})
|
|
324
334
|
delete queue[track]
|
|
335
|
+
pendingTimeouts.delete(track)
|
|
325
336
|
return
|
|
326
337
|
}
|
|
327
|
-
|
|
328
|
-
// Respond to callback with null if no response.
|
|
329
|
-
setTimeout(() => {
|
|
330
|
-
const cb = queue[track]
|
|
331
|
-
if (cb) {
|
|
332
|
-
const id = lex["#"]
|
|
333
|
-
const ack = {[id]: null}
|
|
334
|
-
if (typeof lex["."] === "string") ack[id] = {[lex["."]]: null}
|
|
335
|
-
cb({put: ack})
|
|
336
|
-
delete queue[track]
|
|
337
|
-
}
|
|
338
|
-
}, _opt.wait || 100)
|
|
339
338
|
})
|
|
340
339
|
}
|
|
341
340
|
|
|
@@ -384,7 +383,7 @@ const Wire = opt => {
|
|
|
384
383
|
put: data,
|
|
385
384
|
}),
|
|
386
385
|
)
|
|
387
|
-
// Handle queue overflow error
|
|
386
|
+
// Handle queue overflow error.
|
|
388
387
|
if (sendResult && sendResult.err) {
|
|
389
388
|
if (cb) cb(sendResult.err)
|
|
390
389
|
return
|
|
@@ -432,6 +431,28 @@ const Wire = opt => {
|
|
|
432
431
|
}
|
|
433
432
|
|
|
434
433
|
const send = (data, isBinary) => {
|
|
434
|
+
// Start timeout for this message now that it's being sent
|
|
435
|
+
const msg = JSON.parse(data)
|
|
436
|
+
const trackId = msg["#"]
|
|
437
|
+
if (trackId && pendingTimeouts.has(trackId)) {
|
|
438
|
+
const timeoutConfig = pendingTimeouts.get(trackId)
|
|
439
|
+
pendingTimeouts.delete(trackId)
|
|
440
|
+
|
|
441
|
+
// Respond to callback with null if no response.
|
|
442
|
+
timeoutConfig.timeoutId = setTimeout(() => {
|
|
443
|
+
const cb = queue[trackId]
|
|
444
|
+
if (cb) {
|
|
445
|
+
const id = timeoutConfig.lex["#"]
|
|
446
|
+
const ack = {[id]: null}
|
|
447
|
+
if (typeof timeoutConfig.lex["."] === "string") {
|
|
448
|
+
ack[id] = {[timeoutConfig.lex["."]]: null}
|
|
449
|
+
}
|
|
450
|
+
cb({put: ack})
|
|
451
|
+
delete queue[trackId]
|
|
452
|
+
}
|
|
453
|
+
}, timeoutConfig.wait)
|
|
454
|
+
}
|
|
455
|
+
|
|
435
456
|
clients().forEach(client => {
|
|
436
457
|
if (client && client.readyState === WebSocket.OPEN) {
|
|
437
458
|
client.send(data, {binary: isBinary})
|
|
@@ -463,9 +484,7 @@ const Wire = opt => {
|
|
|
463
484
|
|
|
464
485
|
// Generate unique client ID for rate limiting
|
|
465
486
|
const clientId = utils.text.random(9)
|
|
466
|
-
console.log(
|
|
467
|
-
`[HOLSTER-CONNECT] New WebSocket client connected: ${clientId}`,
|
|
468
|
-
)
|
|
487
|
+
console.log(`New WebSocket client connected: ${clientId}`)
|
|
469
488
|
|
|
470
489
|
ws.on("error", error => {
|
|
471
490
|
console.log("WebSocket error:", error)
|
|
@@ -473,9 +492,7 @@ const Wire = opt => {
|
|
|
473
492
|
})
|
|
474
493
|
|
|
475
494
|
ws.on("close", () => {
|
|
476
|
-
console.log(
|
|
477
|
-
`[HOLSTER-DISCONNECT] WebSocket client disconnected: ${clientId}`,
|
|
478
|
-
)
|
|
495
|
+
console.log(`WebSocket client disconnected: ${clientId}`)
|
|
479
496
|
connectionManager.remove(ws)
|
|
480
497
|
})
|
|
481
498
|
|
|
@@ -505,14 +522,14 @@ const Wire = opt => {
|
|
|
505
522
|
if (delay > 0) {
|
|
506
523
|
const throttleCount = rateLimiter.getThrottleCount(clientId)
|
|
507
524
|
console.log(
|
|
508
|
-
`
|
|
525
|
+
`Client ${clientId}: rate limit exceeded, delay would be ${delay}ms, dropping message (throttle count: ${throttleCount})`,
|
|
509
526
|
)
|
|
510
527
|
// Check if we should disconnect the bad actor
|
|
511
528
|
if (rateLimiter.shouldDisconnect(clientId)) {
|
|
512
529
|
console.log(
|
|
513
|
-
`
|
|
530
|
+
`Client ${clientId}: Disconnecting after ${throttleCount} throttle violations`,
|
|
514
531
|
)
|
|
515
|
-
ws.close(1008, "Rate limit violations
|
|
532
|
+
ws.close(1008, "Rate limit violations")
|
|
516
533
|
return
|
|
517
534
|
}
|
|
518
535
|
|
|
@@ -565,7 +582,7 @@ const Wire = opt => {
|
|
|
565
582
|
let throttleUntil = 0
|
|
566
583
|
let messageQueue = []
|
|
567
584
|
let queueProcessor = null
|
|
568
|
-
const maxQueueLength = opt.maxQueueLength ||
|
|
585
|
+
const maxQueueLength = opt.maxQueueLength || 10000
|
|
569
586
|
|
|
570
587
|
const processQueue = () => {
|
|
571
588
|
if (messageQueue.length === 0) {
|
|
@@ -585,9 +602,7 @@ const Wire = opt => {
|
|
|
585
602
|
|
|
586
603
|
// Throttle period ended, resume processing
|
|
587
604
|
if (clientThrottled && now >= throttleUntil) {
|
|
588
|
-
console.log(
|
|
589
|
-
`[CLIENT-THROTTLED] Throttle period ended, resuming queue processing`,
|
|
590
|
-
)
|
|
605
|
+
console.log(`Throttle period ended, resuming queue processing`)
|
|
591
606
|
clientThrottled = false
|
|
592
607
|
throttleUntil = 0
|
|
593
608
|
}
|
|
@@ -596,10 +611,32 @@ const Wire = opt => {
|
|
|
596
611
|
const data = messageQueue.shift()
|
|
597
612
|
sendToPeers(data)
|
|
598
613
|
|
|
614
|
+
// Start timeout for this message now that it's been sent
|
|
615
|
+
const msg = JSON.parse(data)
|
|
616
|
+
const trackId = msg["#"]
|
|
617
|
+
if (trackId && pendingTimeouts.has(trackId)) {
|
|
618
|
+
const timeoutConfig = pendingTimeouts.get(trackId)
|
|
619
|
+
pendingTimeouts.delete(trackId)
|
|
620
|
+
|
|
621
|
+
// Respond to callback with null if no response.
|
|
622
|
+
timeoutConfig.timeoutId = setTimeout(() => {
|
|
623
|
+
const cb = queue[trackId]
|
|
624
|
+
if (cb) {
|
|
625
|
+
const id = timeoutConfig.lex["#"]
|
|
626
|
+
const ack = {[id]: null}
|
|
627
|
+
if (typeof timeoutConfig.lex["."] === "string") {
|
|
628
|
+
ack[id] = {[timeoutConfig.lex["."]]: null}
|
|
629
|
+
}
|
|
630
|
+
cb({put: ack})
|
|
631
|
+
delete queue[trackId]
|
|
632
|
+
}
|
|
633
|
+
}, timeoutConfig.wait)
|
|
634
|
+
}
|
|
635
|
+
|
|
599
636
|
// Schedule processing of next message if queue not empty
|
|
600
637
|
if (messageQueue.length > 0) {
|
|
601
|
-
// Add
|
|
602
|
-
queueProcessor = setTimeout(processQueue,
|
|
638
|
+
// Add delay between messages to respect rate limits (300 req/min = 200ms)
|
|
639
|
+
queueProcessor = setTimeout(processQueue, 250)
|
|
603
640
|
} else {
|
|
604
641
|
queueProcessor = null
|
|
605
642
|
}
|
|
@@ -629,30 +666,19 @@ const Wire = opt => {
|
|
|
629
666
|
}
|
|
630
667
|
|
|
631
668
|
const send = data => {
|
|
632
|
-
|
|
633
|
-
|
|
634
|
-
|
|
635
|
-
|
|
636
|
-
|
|
637
|
-
queueLength: messageQueue.length,
|
|
638
|
-
maxQueueLength: maxQueueLength,
|
|
639
|
-
}
|
|
669
|
+
if (messageQueue.length >= maxQueueLength) {
|
|
670
|
+
return {
|
|
671
|
+
err: `Message queue exceeded maximum length (${maxQueueLength}). Update query logic to request less data.`,
|
|
672
|
+
queueLength: messageQueue.length,
|
|
673
|
+
maxQueueLength: maxQueueLength,
|
|
640
674
|
}
|
|
641
|
-
|
|
642
|
-
// Add message to queue instead of sending immediately
|
|
643
|
-
messageQueue.push(data)
|
|
644
|
-
console.log(
|
|
645
|
-
`Queuing message (queue length: ${messageQueue.length}/${maxQueueLength})`,
|
|
646
|
-
)
|
|
647
|
-
// Start queue processor if not already running
|
|
648
|
-
if (!queueProcessor) {
|
|
649
|
-
queueProcessor = setTimeout(processQueue, 100)
|
|
650
|
-
}
|
|
651
|
-
return
|
|
652
675
|
}
|
|
653
676
|
|
|
654
|
-
|
|
655
|
-
|
|
677
|
+
messageQueue.push(data)
|
|
678
|
+
// Start queue processor if not already running
|
|
679
|
+
if (!queueProcessor) {
|
|
680
|
+
queueProcessor = setTimeout(processQueue, 250)
|
|
681
|
+
}
|
|
656
682
|
}
|
|
657
683
|
if (!(opt.peers instanceof Array)) {
|
|
658
684
|
opt.peers = [`ws://localhost:${opt.port || 8765}`]
|
|
@@ -689,7 +715,7 @@ const Wire = opt => {
|
|
|
689
715
|
|
|
690
716
|
// Handle throttle messages from server
|
|
691
717
|
if (msg.throttle && msg.err) {
|
|
692
|
-
console.log(`
|
|
718
|
+
console.log(`Server throttling: ${msg.err}`)
|
|
693
719
|
clientThrottled = true
|
|
694
720
|
throttleUntil = Date.now() + msg.throttle
|
|
695
721
|
return
|
|
@@ -35,11 +35,12 @@ describe("holster.user.on", () => {
|
|
|
35
35
|
user.get("key").put("value", err => {
|
|
36
36
|
assert.equal(err, null)
|
|
37
37
|
|
|
38
|
-
|
|
38
|
+
const callback = data => {
|
|
39
39
|
assert.equal(data, "update")
|
|
40
|
-
user.get("key").off()
|
|
40
|
+
user.get("key").off(callback)
|
|
41
41
|
done()
|
|
42
|
-
}
|
|
42
|
+
}
|
|
43
|
+
user.get("key").on(callback)
|
|
43
44
|
|
|
44
45
|
user.get("key").put("update", err => {
|
|
45
46
|
assert.equal(err, null)
|
|
@@ -64,15 +65,16 @@ describe("holster.user.on", () => {
|
|
|
64
65
|
assert.equal(err, null)
|
|
65
66
|
|
|
66
67
|
let first = true
|
|
67
|
-
|
|
68
|
+
const callback = data => {
|
|
68
69
|
if (first) {
|
|
69
70
|
assert.equal(data, "update1")
|
|
70
71
|
} else {
|
|
71
72
|
assert.equal(data, "update2")
|
|
72
|
-
user.get("key3").off()
|
|
73
|
+
user.get("key3").off(callback)
|
|
73
74
|
done()
|
|
74
75
|
}
|
|
75
|
-
}
|
|
76
|
+
}
|
|
77
|
+
user.get("key3").on(callback)
|
|
76
78
|
|
|
77
79
|
user.get("key3").put("update1", err => {
|
|
78
80
|
assert.equal(err, null)
|
|
@@ -103,11 +105,13 @@ describe("holster.user.on", () => {
|
|
|
103
105
|
setTimeout(async () => {
|
|
104
106
|
let count = 0
|
|
105
107
|
for (let i = 0; i < 5; i++) {
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
108
|
+
const idx = i // Capture value
|
|
109
|
+
const callback = data => {
|
|
110
|
+
assert.equal(data, "update" + idx)
|
|
111
|
+
user.get("for" + idx).off(callback)
|
|
109
112
|
count++
|
|
110
|
-
}
|
|
113
|
+
}
|
|
114
|
+
user.get("for" + idx).on(callback)
|
|
111
115
|
}
|
|
112
116
|
|
|
113
117
|
setTimeout(async () => {
|
|
@@ -130,11 +134,12 @@ describe("holster.user.on", () => {
|
|
|
130
134
|
assert.equal(err, null)
|
|
131
135
|
|
|
132
136
|
setTimeout(() => {
|
|
133
|
-
|
|
137
|
+
const callback = data => {
|
|
134
138
|
assert.equal(data, "value4")
|
|
135
|
-
user.get("key4").off()
|
|
139
|
+
user.get("key4").off(callback)
|
|
136
140
|
done()
|
|
137
|
-
}
|
|
141
|
+
}
|
|
142
|
+
user.get("key4").on(callback, true)
|
|
138
143
|
}, 100)
|
|
139
144
|
})
|
|
140
145
|
})
|
|
@@ -147,14 +152,12 @@ describe("holster.user.on", () => {
|
|
|
147
152
|
.put("ok", err => {
|
|
148
153
|
assert.equal(err, null)
|
|
149
154
|
|
|
150
|
-
|
|
151
|
-
.
|
|
152
|
-
.next("world!")
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
done()
|
|
157
|
-
})
|
|
155
|
+
const callback = data => {
|
|
156
|
+
assert.equal(data, "update")
|
|
157
|
+
user.get("hello").next("world!").off(callback)
|
|
158
|
+
done()
|
|
159
|
+
}
|
|
160
|
+
user.get("hello").next("world!").on(callback)
|
|
158
161
|
|
|
159
162
|
setTimeout(() => {
|
|
160
163
|
user
|
|
@@ -181,10 +184,12 @@ describe("holster.user.on", () => {
|
|
|
181
184
|
number: 42,
|
|
182
185
|
}
|
|
183
186
|
|
|
184
|
-
|
|
187
|
+
const callback = data => {
|
|
185
188
|
assert.deepEqual(data, update)
|
|
189
|
+
user.get("plain").off(callback)
|
|
186
190
|
done()
|
|
187
|
-
}
|
|
191
|
+
}
|
|
192
|
+
user.get("plain").on(callback)
|
|
188
193
|
|
|
189
194
|
setTimeout(() => {
|
|
190
195
|
user.get("plain").put(update, err => {
|
|
@@ -212,17 +217,18 @@ describe("holster.user.on", () => {
|
|
|
212
217
|
},
|
|
213
218
|
}
|
|
214
219
|
|
|
215
|
-
|
|
220
|
+
const parentCallback = data => {
|
|
216
221
|
assert.deepEqual(data, update)
|
|
217
|
-
}
|
|
222
|
+
}
|
|
223
|
+
user.get("nested").on(parentCallback)
|
|
218
224
|
|
|
219
|
-
|
|
220
|
-
.
|
|
221
|
-
.
|
|
222
|
-
.
|
|
223
|
-
|
|
224
|
-
|
|
225
|
-
|
|
225
|
+
const childCallback = data => {
|
|
226
|
+
assert.deepEqual(data, update.child)
|
|
227
|
+
user.get("nested").off(parentCallback)
|
|
228
|
+
user.get("nested").next("child").off(childCallback)
|
|
229
|
+
done()
|
|
230
|
+
}
|
|
231
|
+
user.get("nested").next("child").on(childCallback)
|
|
226
232
|
|
|
227
233
|
setTimeout(() => {
|
|
228
234
|
user.get("nested").put(update, err => {
|
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
import fs from "fs"
|
|
2
|
+
import {Server} from "mock-socket"
|
|
3
|
+
import {describe, test} from "node:test"
|
|
4
|
+
import assert from "node:assert/strict"
|
|
5
|
+
import Holster from "../../src/holster.js"
|
|
6
|
+
|
|
7
|
+
describe("system - basic setup", () => {
|
|
8
|
+
const wss = new Server("ws://localhost:9001")
|
|
9
|
+
const holster = Holster({file: "test/system/basic", wss: wss})
|
|
10
|
+
|
|
11
|
+
test("put and get data", (t, done) => {
|
|
12
|
+
holster.get("test").put({message: "hello"}, err => {
|
|
13
|
+
assert.equal(err, null)
|
|
14
|
+
|
|
15
|
+
holster.get("test", data => {
|
|
16
|
+
assert.notEqual(data, null)
|
|
17
|
+
assert.equal(data.message, "hello")
|
|
18
|
+
done()
|
|
19
|
+
})
|
|
20
|
+
})
|
|
21
|
+
})
|
|
22
|
+
|
|
23
|
+
test("cleanup", (t, done) => {
|
|
24
|
+
fs.rm("test/system/basic", {recursive: true, force: true}, err => {
|
|
25
|
+
assert.equal(err, null)
|
|
26
|
+
done()
|
|
27
|
+
})
|
|
28
|
+
})
|
|
29
|
+
})
|
|
@@ -0,0 +1,41 @@
|
|
|
1
|
+
import fs from "fs"
|
|
2
|
+
import {Server} from "mock-socket"
|
|
3
|
+
import {describe, test} from "node:test"
|
|
4
|
+
import assert from "node:assert/strict"
|
|
5
|
+
import Holster from "../../src/holster.js"
|
|
6
|
+
|
|
7
|
+
describe("system - chained get with on", () => {
|
|
8
|
+
const wss = new Server("ws://localhost:9002")
|
|
9
|
+
const holster = Holster({file: "test/system/chained-on", wss: wss})
|
|
10
|
+
|
|
11
|
+
test("on listener fires with null when no data exists", (t, done) => {
|
|
12
|
+
holster.get("key").on(data => {
|
|
13
|
+
// This is the actual behavior - on fires with null for non-existent keys
|
|
14
|
+
assert.equal(data, null)
|
|
15
|
+
done()
|
|
16
|
+
}, true)
|
|
17
|
+
})
|
|
18
|
+
|
|
19
|
+
test("put then on listener receives data", (t, done) => {
|
|
20
|
+
// Put data first
|
|
21
|
+
holster.get("key2").put({value: "test"}, err => {
|
|
22
|
+
assert.equal(err, null)
|
|
23
|
+
|
|
24
|
+
// Then set up listener - should get the existing data
|
|
25
|
+
setTimeout(() => {
|
|
26
|
+
holster.get("key2").on(data => {
|
|
27
|
+
assert.notEqual(data, null)
|
|
28
|
+
assert.equal(data.value, "test")
|
|
29
|
+
done()
|
|
30
|
+
}, true)
|
|
31
|
+
}, 50)
|
|
32
|
+
})
|
|
33
|
+
})
|
|
34
|
+
|
|
35
|
+
test("cleanup", (t, done) => {
|
|
36
|
+
fs.rm("test/system/chained-on", {recursive: true, force: true}, err => {
|
|
37
|
+
assert.equal(err, null)
|
|
38
|
+
done()
|
|
39
|
+
})
|
|
40
|
+
})
|
|
41
|
+
})
|