@mblaney/holster 1.0.16 → 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 +56 -13
- 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
|
@@ -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
|
|
|
@@ -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})
|
|
@@ -590,6 +611,28 @@ const Wire = opt => {
|
|
|
590
611
|
const data = messageQueue.shift()
|
|
591
612
|
sendToPeers(data)
|
|
592
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
|
+
|
|
593
636
|
// Schedule processing of next message if queue not empty
|
|
594
637
|
if (messageQueue.length > 0) {
|
|
595
638
|
// Add delay between messages to respect rate limits (300 req/min = 200ms)
|
|
@@ -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
|
+
})
|
|
@@ -0,0 +1,50 @@
|
|
|
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 - listener before data", () => {
|
|
8
|
+
const wss = new Server("ws://localhost:9007")
|
|
9
|
+
const holster = Holster({file: "test/system/listener-before-data", wss: wss})
|
|
10
|
+
|
|
11
|
+
test("listener set up before data exists receives data when populated", (t, done) => {
|
|
12
|
+
const results = []
|
|
13
|
+
|
|
14
|
+
// Set up listener first - this is the browser Display.js pattern
|
|
15
|
+
holster.get("testkey").on(data => {
|
|
16
|
+
results.push(data)
|
|
17
|
+
})
|
|
18
|
+
|
|
19
|
+
// Give listener time to set up
|
|
20
|
+
setTimeout(() => {
|
|
21
|
+
holster.get("testkey").put({value: "test"}, err => {
|
|
22
|
+
assert.equal(err, null)
|
|
23
|
+
|
|
24
|
+
// Give listener time to fire
|
|
25
|
+
setTimeout(() => {
|
|
26
|
+
// Should have received null (initial) and the test value
|
|
27
|
+
const nonNull = results.filter(r => r !== null)
|
|
28
|
+
assert.equal(
|
|
29
|
+
nonNull.length >= 1,
|
|
30
|
+
true,
|
|
31
|
+
"Should have at least one non-null result",
|
|
32
|
+
)
|
|
33
|
+
assert.equal(nonNull[0].value, "test")
|
|
34
|
+
done()
|
|
35
|
+
}, 200)
|
|
36
|
+
})
|
|
37
|
+
}, 100)
|
|
38
|
+
})
|
|
39
|
+
|
|
40
|
+
test("cleanup", (t, done) => {
|
|
41
|
+
fs.rm(
|
|
42
|
+
"test/system/listener-before-data",
|
|
43
|
+
{recursive: true, force: true},
|
|
44
|
+
err => {
|
|
45
|
+
assert.equal(err, null)
|
|
46
|
+
done()
|
|
47
|
+
},
|
|
48
|
+
)
|
|
49
|
+
})
|
|
50
|
+
})
|