@mblaney/holster 1.0.16 → 1.0.18
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 +72 -14
- package/src/radisk.js +1 -1
- package/src/wire.js +84 -33
- 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,51 @@ 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 = async (retries = 0) => {
|
|
572
|
+
const data = await new Promise(res => {
|
|
573
|
+
const _ctxid = utils.text.random()
|
|
574
|
+
allctx.set(_ctxid, {chain: [{item: null, soul: id}]})
|
|
575
|
+
api(_ctxid).next(null, res, _opt)
|
|
576
|
+
})
|
|
577
|
+
if (data !== null || retries >= 5) {
|
|
578
|
+
cb(data)
|
|
579
|
+
} else {
|
|
580
|
+
// Data not ready, retry after delay
|
|
581
|
+
await new Promise(resolve => setTimeout(resolve, 50))
|
|
582
|
+
attemptRead(retries + 1)
|
|
583
|
+
}
|
|
584
|
+
}
|
|
585
|
+
attemptRead()
|
|
586
|
+
} else {
|
|
587
|
+
// It's a direct property or null
|
|
588
|
+
cb(current !== undefined ? current : null)
|
|
589
|
+
}
|
|
590
|
+
},
|
|
591
|
+
_opt,
|
|
592
|
+
)
|
|
593
|
+
})
|
|
594
|
+
|
|
595
|
+
// Register listener immediately to avoid missing updates due to
|
|
596
|
+
// queueing. Initially register the soul without _get, then update if
|
|
597
|
+
// it's a rel.
|
|
598
|
+
let initialLex
|
|
599
|
+
if (lex) initialLex = utils.obj.put(lex, "#", soul)
|
|
600
|
+
else initialLex = {"#": soul, ".": item}
|
|
601
|
+
wire.on(initialLex, map.get(cb), false, _opt)
|
|
602
|
+
|
|
603
|
+
// Check if item is a rel and update listener if needed.
|
|
604
|
+
// This happens async but the listener is already registered above.
|
|
552
605
|
wire.get(
|
|
553
606
|
{"#": soul, ".": item},
|
|
554
607
|
msg => {
|
|
@@ -560,13 +613,18 @@ const Holster = opt => {
|
|
|
560
613
|
const current = msg.put && msg.put[soul] && msg.put[soul][item]
|
|
561
614
|
const id = utils.rel.is(current)
|
|
562
615
|
if (id) {
|
|
563
|
-
|
|
564
|
-
|
|
565
|
-
|
|
566
|
-
|
|
567
|
-
|
|
616
|
+
// It's a rel, need to switch listener to the related node
|
|
617
|
+
// First remove the initial listener
|
|
618
|
+
wire.off(initialLex, map.get(cb))
|
|
619
|
+
// Then add listener on the related node
|
|
620
|
+
let relLex
|
|
621
|
+
if (lex) relLex = utils.obj.put(lex, "#", id)
|
|
622
|
+
else relLex = {"#": id, ".": null}
|
|
623
|
+
wire.on(relLex, map.get(cb), _get, _opt)
|
|
624
|
+
} else if (_get) {
|
|
625
|
+
// Not a rel, but _get was requested, so trigger callback.
|
|
626
|
+
map.get(cb)()
|
|
568
627
|
}
|
|
569
|
-
wire.on(lex, map.get(cb), _get, _opt)
|
|
570
628
|
},
|
|
571
629
|
_opt,
|
|
572
630
|
)
|
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
|
@@ -178,6 +178,9 @@ const Wire = opt => {
|
|
|
178
178
|
// Track references we want but don't have yet
|
|
179
179
|
const pendingReferences = new Set()
|
|
180
180
|
|
|
181
|
+
// Track pending timeouts that should start when message is sent
|
|
182
|
+
const pendingTimeouts = new Map()
|
|
183
|
+
|
|
181
184
|
// Helper to check if we have a soul in memory or storage
|
|
182
185
|
const hasSoul = async soul => {
|
|
183
186
|
if (graph[soul]) return true
|
|
@@ -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
|
|
|
@@ -354,37 +353,37 @@ const Wire = opt => {
|
|
|
354
353
|
// used whereas wire spec needs to handle clock skew for updates
|
|
355
354
|
// across the network.
|
|
356
355
|
const update = await Ham.mix(data, graph, opt.secure, listen)
|
|
357
|
-
const none = Object.keys(update.now).length === 0
|
|
358
|
-
if (none) {
|
|
359
|
-
// Nothing to do.
|
|
360
|
-
cb(null)
|
|
361
|
-
return
|
|
362
|
-
}
|
|
363
356
|
|
|
364
|
-
if (
|
|
365
|
-
|
|
366
|
-
|
|
367
|
-
|
|
368
|
-
|
|
369
|
-
|
|
370
|
-
const
|
|
371
|
-
|
|
372
|
-
|
|
373
|
-
|
|
357
|
+
if (Object.keys(update.now).length !== 0) {
|
|
358
|
+
if (!(await check(update.now, send, cb))) return
|
|
359
|
+
|
|
360
|
+
// Seed pendingReferences with any new references from API calls
|
|
361
|
+
for (const [soul, node] of Object.entries(update.now)) {
|
|
362
|
+
if (node && typeof node === "object") {
|
|
363
|
+
for (const [key, value] of Object.entries(node)) {
|
|
364
|
+
const soulId = utils.rel.is(value)
|
|
365
|
+
if (soulId) {
|
|
366
|
+
// Add referenced soul to pending list
|
|
367
|
+
pendingReferences.add(soulId)
|
|
368
|
+
}
|
|
374
369
|
}
|
|
375
370
|
}
|
|
376
371
|
}
|
|
372
|
+
|
|
373
|
+
store.put(update.now, cb)
|
|
374
|
+
} else {
|
|
375
|
+
// No local update needed, but still callback
|
|
376
|
+
if (cb) cb(null)
|
|
377
377
|
}
|
|
378
378
|
|
|
379
|
-
|
|
380
|
-
// Also put data on the wire spec.
|
|
379
|
+
// Always put data on the wire spec even if no local update needed
|
|
381
380
|
const sendResult = send(
|
|
382
381
|
JSON.stringify({
|
|
383
382
|
"#": dup.track(utils.text.random(9)),
|
|
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})
|
|
@@ -587,8 +608,35 @@ const Wire = opt => {
|
|
|
587
608
|
}
|
|
588
609
|
|
|
589
610
|
// Process next message in queue
|
|
590
|
-
const data = messageQueue
|
|
591
|
-
sendToPeers(data)
|
|
611
|
+
const data = messageQueue[0]
|
|
612
|
+
const sent = sendToPeers(data)
|
|
613
|
+
|
|
614
|
+
if (sent) {
|
|
615
|
+
// Only remove from queue if successfully sent
|
|
616
|
+
messageQueue.shift()
|
|
617
|
+
|
|
618
|
+
// Start timeout for this message now that it's been sent
|
|
619
|
+
const msg = JSON.parse(data)
|
|
620
|
+
const trackId = msg["#"]
|
|
621
|
+
if (trackId && pendingTimeouts.has(trackId)) {
|
|
622
|
+
const timeoutConfig = pendingTimeouts.get(trackId)
|
|
623
|
+
pendingTimeouts.delete(trackId)
|
|
624
|
+
|
|
625
|
+
// Respond to callback with null if no response.
|
|
626
|
+
timeoutConfig.timeoutId = setTimeout(() => {
|
|
627
|
+
const cb = queue[trackId]
|
|
628
|
+
if (cb) {
|
|
629
|
+
const id = timeoutConfig.lex["#"]
|
|
630
|
+
const ack = {[id]: null}
|
|
631
|
+
if (typeof timeoutConfig.lex["."] === "string") {
|
|
632
|
+
ack[id] = {[timeoutConfig.lex["."]]: null}
|
|
633
|
+
}
|
|
634
|
+
cb({put: ack})
|
|
635
|
+
delete queue[trackId]
|
|
636
|
+
}
|
|
637
|
+
}, timeoutConfig.wait)
|
|
638
|
+
}
|
|
639
|
+
}
|
|
592
640
|
|
|
593
641
|
// Schedule processing of next message if queue not empty
|
|
594
642
|
if (messageQueue.length > 0) {
|
|
@@ -600,9 +648,11 @@ const Wire = opt => {
|
|
|
600
648
|
}
|
|
601
649
|
|
|
602
650
|
const sendToPeers = data => {
|
|
651
|
+
let sentToAtLeastOne = false
|
|
603
652
|
peers.forEach(peer => {
|
|
604
653
|
if (peer && peer.readyState === WebSocket.OPEN) {
|
|
605
654
|
peer.send(data)
|
|
655
|
+
sentToAtLeastOne = true
|
|
606
656
|
} else {
|
|
607
657
|
const retryHandler = peer._retryHandler || createRetryHandler()
|
|
608
658
|
peer._retryHandler = retryHandler
|
|
@@ -620,6 +670,7 @@ const Wire = opt => {
|
|
|
620
670
|
}
|
|
621
671
|
}
|
|
622
672
|
})
|
|
673
|
+
return sentToAtLeastOne
|
|
623
674
|
}
|
|
624
675
|
|
|
625
676
|
const send = data => {
|
|
@@ -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
|
+
})
|
|
@@ -0,0 +1,124 @@
|
|
|
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 - concurrent listeners", () => {
|
|
8
|
+
const wss = new Server("ws://localhost:9005")
|
|
9
|
+
const holster = Holster({file: "test/system/concurrent-listeners", wss: wss})
|
|
10
|
+
|
|
11
|
+
test("multiple listeners on same path receive data", (t, done) => {
|
|
12
|
+
let listener1Called = false
|
|
13
|
+
let listener2Called = false
|
|
14
|
+
|
|
15
|
+
holster.get("key").put({value: "test"}, err => {
|
|
16
|
+
assert.equal(err, null)
|
|
17
|
+
|
|
18
|
+
// Set up two listeners on the same path
|
|
19
|
+
holster.get("key").on(data => {
|
|
20
|
+
listener1Called = true
|
|
21
|
+
assert.notEqual(data, null)
|
|
22
|
+
assert.equal(data.value, "test")
|
|
23
|
+
checkComplete()
|
|
24
|
+
}, true)
|
|
25
|
+
|
|
26
|
+
holster.get("key").on(data => {
|
|
27
|
+
listener2Called = true
|
|
28
|
+
assert.notEqual(data, null)
|
|
29
|
+
assert.equal(data.value, "test")
|
|
30
|
+
checkComplete()
|
|
31
|
+
}, true)
|
|
32
|
+
|
|
33
|
+
function checkComplete() {
|
|
34
|
+
if (listener1Called && listener2Called) {
|
|
35
|
+
done()
|
|
36
|
+
}
|
|
37
|
+
}
|
|
38
|
+
})
|
|
39
|
+
})
|
|
40
|
+
|
|
41
|
+
test("nested listeners on parent and child paths", (t, done) => {
|
|
42
|
+
let parentCalled = false
|
|
43
|
+
let childCalled = false
|
|
44
|
+
|
|
45
|
+
holster.get("parent").put({value: "parent-value"}, err => {
|
|
46
|
+
assert.equal(err, null)
|
|
47
|
+
|
|
48
|
+
holster
|
|
49
|
+
.get("parent")
|
|
50
|
+
.next("child")
|
|
51
|
+
.put({value: "child-value"}, err => {
|
|
52
|
+
assert.equal(err, null)
|
|
53
|
+
|
|
54
|
+
// Listen on parent
|
|
55
|
+
holster.get("parent").on(data => {
|
|
56
|
+
parentCalled = true
|
|
57
|
+
assert.notEqual(data, null)
|
|
58
|
+
assert.equal(data.value, "parent-value")
|
|
59
|
+
checkComplete()
|
|
60
|
+
}, true)
|
|
61
|
+
|
|
62
|
+
// Listen on child
|
|
63
|
+
holster
|
|
64
|
+
.get("parent")
|
|
65
|
+
.next("child")
|
|
66
|
+
.on(data => {
|
|
67
|
+
childCalled = true
|
|
68
|
+
assert.notEqual(data, null)
|
|
69
|
+
assert.equal(data.value, "child-value")
|
|
70
|
+
checkComplete()
|
|
71
|
+
}, true)
|
|
72
|
+
|
|
73
|
+
function checkComplete() {
|
|
74
|
+
if (parentCalled && childCalled) {
|
|
75
|
+
done()
|
|
76
|
+
}
|
|
77
|
+
}
|
|
78
|
+
})
|
|
79
|
+
})
|
|
80
|
+
})
|
|
81
|
+
|
|
82
|
+
test("listener race condition - data before and after listener setup", (t, done) => {
|
|
83
|
+
const results = []
|
|
84
|
+
|
|
85
|
+
// Put data first
|
|
86
|
+
holster.get("race").put({value: "first"}, err => {
|
|
87
|
+
assert.equal(err, null)
|
|
88
|
+
|
|
89
|
+
// Set up listener with once=true to read existing data
|
|
90
|
+
holster.get("race").on(data => {
|
|
91
|
+
results.push(data?.value)
|
|
92
|
+
}, true)
|
|
93
|
+
|
|
94
|
+
// Immediately put new data
|
|
95
|
+
setTimeout(() => {
|
|
96
|
+
holster.get("race").put({value: "second"}, err => {
|
|
97
|
+
assert.equal(err, null)
|
|
98
|
+
|
|
99
|
+
setTimeout(() => {
|
|
100
|
+
// Should have received both values
|
|
101
|
+
assert.equal(
|
|
102
|
+
results.length >= 1,
|
|
103
|
+
true,
|
|
104
|
+
"Should receive at least initial value",
|
|
105
|
+
)
|
|
106
|
+
assert.equal(results[0], "first", "First value should be 'first'")
|
|
107
|
+
done()
|
|
108
|
+
}, 100)
|
|
109
|
+
})
|
|
110
|
+
}, 50)
|
|
111
|
+
})
|
|
112
|
+
})
|
|
113
|
+
|
|
114
|
+
test("cleanup", (t, done) => {
|
|
115
|
+
fs.rm(
|
|
116
|
+
"test/system/concurrent-listeners",
|
|
117
|
+
{recursive: true, force: true},
|
|
118
|
+
err => {
|
|
119
|
+
assert.equal(err, null)
|
|
120
|
+
done()
|
|
121
|
+
},
|
|
122
|
+
)
|
|
123
|
+
})
|
|
124
|
+
})
|
|
@@ -0,0 +1,30 @@
|
|
|
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 - immediate read after write", () => {
|
|
8
|
+
const wss = new Server("ws://localhost:9008")
|
|
9
|
+
const holster = Holster({file: "test/system/immediate-read", wss: wss})
|
|
10
|
+
|
|
11
|
+
test("get immediately after put returns data despite queue delays", (t, done) => {
|
|
12
|
+
holster.get("testkey").put({value: "test"}, err => {
|
|
13
|
+
assert.equal(err, null)
|
|
14
|
+
|
|
15
|
+
// Immediate get after put - validates retry logic handles queued writes
|
|
16
|
+
holster.get("testkey", data => {
|
|
17
|
+
assert.notEqual(data, null)
|
|
18
|
+
assert.equal(data.value, "test")
|
|
19
|
+
done()
|
|
20
|
+
})
|
|
21
|
+
})
|
|
22
|
+
})
|
|
23
|
+
|
|
24
|
+
test("cleanup", (t, done) => {
|
|
25
|
+
fs.rm("test/system/immediate-read", {recursive: true, force: true}, err => {
|
|
26
|
+
assert.equal(err, null)
|
|
27
|
+
done()
|
|
28
|
+
})
|
|
29
|
+
})
|
|
30
|
+
})
|