@mblaney/holster 0.1.0
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/.github/workflows/node.js.yml +31 -0
- package/LICENSE.md +55 -0
- package/README.md +24 -0
- package/build/holster.js +4 -0
- package/build/holster.js.map +7 -0
- package/examples/index.html +33 -0
- package/examples/index.js +49 -0
- package/examples/user.html +31 -0
- package/examples/user.js +88 -0
- package/examples/wire.html +27 -0
- package/examples/wire.js +55 -0
- package/package.json +44 -0
- package/src/array.js +34 -0
- package/src/buffer.js +83 -0
- package/src/dup.js +23 -0
- package/src/get.js +19 -0
- package/src/ham.js +151 -0
- package/src/holster.js +502 -0
- package/src/index.js +21 -0
- package/src/radisk.js +438 -0
- package/src/radix.js +106 -0
- package/src/sea-utils.js +65 -0
- package/src/sea.js +224 -0
- package/src/store.js +225 -0
- package/src/user.js +330 -0
- package/src/utils.js +124 -0
- package/src/wire.js +290 -0
- package/test/holster.get.test.js +60 -0
- package/test/holster.lex.test.js +196 -0
- package/test/holster.off.test.js +53 -0
- package/test/holster.on.test.js +168 -0
- package/test/holster.put.test.js +278 -0
- package/test/holster.secure.test.js +210 -0
- package/test/holster.user.get.test.js +86 -0
- package/test/holster.user.lex.test.js +148 -0
- package/test/holster.user.off.test.js +72 -0
- package/test/holster.user.on.test.js +185 -0
- package/test/holster.user.put.test.js +381 -0
- package/test/kitty/! +9 -0
- package/test/names.js +225 -0
- package/test/radisk.test.js +191 -0
- package/test/radix.test.js +117 -0
- package/test/sea.test.js +284 -0
- package/test/store.test.js +316 -0
- package/test/user.test.js +173 -0
- package/test/wire.test.js +159 -0
package/src/wire.js
ADDED
|
@@ -0,0 +1,290 @@
|
|
|
1
|
+
import Dup from "./dup.js"
|
|
2
|
+
import Get from "./get.js"
|
|
3
|
+
import Ham from "./ham.js"
|
|
4
|
+
import Store from "./store.js"
|
|
5
|
+
import * as utils from "./utils.js"
|
|
6
|
+
|
|
7
|
+
const isNode = typeof document === "undefined"
|
|
8
|
+
|
|
9
|
+
const wsModule = isNode ? await import("ws") : undefined
|
|
10
|
+
|
|
11
|
+
if (typeof globalThis.WebSocket === "undefined") {
|
|
12
|
+
globalThis.WebSocket = wsModule?.WebSocket
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
// Wire starts a websocket client or server and returns get and put methods
|
|
16
|
+
// for access to the wire spec and storage.
|
|
17
|
+
const Wire = opt => {
|
|
18
|
+
if (!utils.obj.is(opt)) opt = {}
|
|
19
|
+
|
|
20
|
+
const dup = Dup(opt.maxAge)
|
|
21
|
+
const store = Store(opt)
|
|
22
|
+
const graph = {}
|
|
23
|
+
const queue = {}
|
|
24
|
+
const listen = {}
|
|
25
|
+
|
|
26
|
+
// The check function is required because user data must provide a public key
|
|
27
|
+
// so that it can be verified. The public key might verify the provided
|
|
28
|
+
// signature but not actually match the user under which the data is being
|
|
29
|
+
// stored. To avoid this, the current data on a soul needs to be checked to
|
|
30
|
+
// make sure the stored public key matches the one provided with the update.
|
|
31
|
+
const check = async (data, send, cb) => {
|
|
32
|
+
for (const soul of Object.keys(data)) {
|
|
33
|
+
const msg = await new Promise(res => {
|
|
34
|
+
getWithCallback({"#": soul}, res, send)
|
|
35
|
+
})
|
|
36
|
+
if (msg.err) {
|
|
37
|
+
if (cb) cb(msg.err)
|
|
38
|
+
return false
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
const node = data[soul]
|
|
42
|
+
const pub = node._ ? node._.p : undefined
|
|
43
|
+
const key = utils.userPublicKey
|
|
44
|
+
// If there is no current node then the data is ok to write without
|
|
45
|
+
// matching public keys, as the provided soul also needs a rel on the
|
|
46
|
+
// parent node which then also requires checking. Otherwise publc keys
|
|
47
|
+
// need to match for existing data.
|
|
48
|
+
if (!msg.put || !msg.put[soul] || msg.put[soul][key] === pub) {
|
|
49
|
+
continue
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
// If a soul exists but does not have a public key, then one should not be
|
|
53
|
+
// added because the node is not user data. The above check fails in this
|
|
54
|
+
// case if a public key is provided. Note that this is only an error case
|
|
55
|
+
// if called via the API, which is when a callback is provided here.
|
|
56
|
+
// (The wire spec can fetch and put data on the wire without a signature
|
|
57
|
+
// or public key and this can be ignored.)
|
|
58
|
+
if (cb) {
|
|
59
|
+
cb(`error in wire check public key does not match for soul: ${soul}`)
|
|
60
|
+
}
|
|
61
|
+
return false
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
return true
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
const get = (msg, send) => {
|
|
68
|
+
const ack = Get(msg.get, graph)
|
|
69
|
+
if (ack) {
|
|
70
|
+
send(
|
|
71
|
+
JSON.stringify({
|
|
72
|
+
"#": dup.track(utils.text.random(9)),
|
|
73
|
+
"@": msg["#"],
|
|
74
|
+
put: ack,
|
|
75
|
+
}),
|
|
76
|
+
)
|
|
77
|
+
} else {
|
|
78
|
+
store.get(msg.get, (err, ack) => {
|
|
79
|
+
send(
|
|
80
|
+
JSON.stringify({
|
|
81
|
+
"#": dup.track(utils.text.random(9)),
|
|
82
|
+
"@": msg["#"],
|
|
83
|
+
put: ack,
|
|
84
|
+
err: err,
|
|
85
|
+
}),
|
|
86
|
+
)
|
|
87
|
+
})
|
|
88
|
+
}
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
const put = async (msg, send) => {
|
|
92
|
+
// Store updates returned from Ham.mix and defer updates if required.
|
|
93
|
+
const update = await Ham.mix(msg.put, graph, opt.secure, listen)
|
|
94
|
+
if (Object.keys(update.now).length) {
|
|
95
|
+
if (!(await check(update.now, send))) return
|
|
96
|
+
|
|
97
|
+
store.put(update.now, err => {
|
|
98
|
+
send(
|
|
99
|
+
JSON.stringify({
|
|
100
|
+
"#": dup.track(utils.text.random(9)),
|
|
101
|
+
"@": msg["#"],
|
|
102
|
+
err: err,
|
|
103
|
+
}),
|
|
104
|
+
)
|
|
105
|
+
})
|
|
106
|
+
}
|
|
107
|
+
if (Object.keys(update.defer).length) {
|
|
108
|
+
setTimeout(() => put({put: update.defer}, send), update.wait)
|
|
109
|
+
}
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
const getWithCallback = (lex, cb, send, opt) => {
|
|
113
|
+
if (!cb) return
|
|
114
|
+
|
|
115
|
+
if (!utils.obj.is(opt)) opt = {}
|
|
116
|
+
const ack = Get(lex, graph)
|
|
117
|
+
if (ack) {
|
|
118
|
+
cb({put: ack})
|
|
119
|
+
return
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
store.get(lex, (err, ack) => {
|
|
123
|
+
if (ack) {
|
|
124
|
+
cb({put: ack, err: err})
|
|
125
|
+
return
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
if (err) console.log(err)
|
|
129
|
+
|
|
130
|
+
const track = utils.text.random(9)
|
|
131
|
+
queue[track] = cb
|
|
132
|
+
send(
|
|
133
|
+
JSON.stringify({
|
|
134
|
+
"#": dup.track(track),
|
|
135
|
+
get: lex,
|
|
136
|
+
}),
|
|
137
|
+
)
|
|
138
|
+
// Respond to callback with null if no response.
|
|
139
|
+
setTimeout(() => {
|
|
140
|
+
const cb = queue[track]
|
|
141
|
+
if (cb) {
|
|
142
|
+
const id = lex["#"]
|
|
143
|
+
const ack = {[id]: null}
|
|
144
|
+
if (typeof lex["."] === "string") ack[id] = {[lex["."]]: null}
|
|
145
|
+
cb({put: ack})
|
|
146
|
+
delete queue[track]
|
|
147
|
+
}
|
|
148
|
+
}, opt.wait || 100)
|
|
149
|
+
})
|
|
150
|
+
}
|
|
151
|
+
|
|
152
|
+
const api = send => {
|
|
153
|
+
return {
|
|
154
|
+
get: (lex, cb, opt) => {
|
|
155
|
+
getWithCallback(lex, cb, send, opt)
|
|
156
|
+
},
|
|
157
|
+
put: async (data, cb) => {
|
|
158
|
+
// Deferred updates are only stored using wire spec, they're ignored
|
|
159
|
+
// here using the api. This is ok because correct timestamps should be
|
|
160
|
+
// used whereas wire spec needs to handle clock skew for updates
|
|
161
|
+
// across the network.
|
|
162
|
+
const update = await Ham.mix(data, graph, opt.secure, listen)
|
|
163
|
+
const none = Object.keys(update.now).length === 0
|
|
164
|
+
if (none || !(await check(update.now, send, cb))) return
|
|
165
|
+
|
|
166
|
+
store.put(update.now, cb)
|
|
167
|
+
// Also put data on the wire spec.
|
|
168
|
+
// TODO: Note that this means all clients now receive all updates, so
|
|
169
|
+
// need to filter what should be stored, both in graph and on disk.
|
|
170
|
+
send(
|
|
171
|
+
JSON.stringify({
|
|
172
|
+
"#": dup.track(utils.text.random(9)),
|
|
173
|
+
put: data,
|
|
174
|
+
}),
|
|
175
|
+
)
|
|
176
|
+
},
|
|
177
|
+
on: (lex, cb) => {
|
|
178
|
+
const soul = lex && lex["#"]
|
|
179
|
+
if (!soul || !cb) return
|
|
180
|
+
|
|
181
|
+
if (listen[soul]) {
|
|
182
|
+
listen[soul].push({".": lex["."], cb: cb})
|
|
183
|
+
} else {
|
|
184
|
+
listen[soul] = [{".": lex["."], cb: cb}]
|
|
185
|
+
}
|
|
186
|
+
},
|
|
187
|
+
off: (lex, cb) => {
|
|
188
|
+
const soul = lex && lex["#"]
|
|
189
|
+
if (!soul || !listen[soul]) return
|
|
190
|
+
|
|
191
|
+
if (cb) {
|
|
192
|
+
const found = listen[soul].find(l => l.cb === cb)
|
|
193
|
+
if (found) {
|
|
194
|
+
listen[soul].splice(listen[soul].indexOf(found), 1)
|
|
195
|
+
}
|
|
196
|
+
} else {
|
|
197
|
+
// Remove all callbacks when none provided.
|
|
198
|
+
delete listen[soul]
|
|
199
|
+
}
|
|
200
|
+
},
|
|
201
|
+
}
|
|
202
|
+
}
|
|
203
|
+
|
|
204
|
+
if (isNode) {
|
|
205
|
+
let wss = opt.wss
|
|
206
|
+
// Node's websocket server provides clients as an array, whereas
|
|
207
|
+
// mock-sockets provides clients as a function that returns an array.
|
|
208
|
+
let clients = () => wss.clients()
|
|
209
|
+
if (!wss) {
|
|
210
|
+
wss = new wsModule.WebSocketServer({port: 8080})
|
|
211
|
+
clients = () => wss.clients
|
|
212
|
+
}
|
|
213
|
+
|
|
214
|
+
const send = (data, isBinary) => {
|
|
215
|
+
clients().forEach(client => {
|
|
216
|
+
if (client.readyState === WebSocket.OPEN) {
|
|
217
|
+
client.send(data, {binary: isBinary})
|
|
218
|
+
}
|
|
219
|
+
})
|
|
220
|
+
}
|
|
221
|
+
wss.on("connection", ws => {
|
|
222
|
+
ws.on("error", console.error)
|
|
223
|
+
|
|
224
|
+
ws.on("message", (data, isBinary) => {
|
|
225
|
+
const msg = JSON.parse(data)
|
|
226
|
+
if (dup.check(msg["#"])) return
|
|
227
|
+
|
|
228
|
+
dup.track(msg["#"])
|
|
229
|
+
if (msg.get) get(msg, send)
|
|
230
|
+
if (msg.put) put(msg, send)
|
|
231
|
+
send(data, isBinary)
|
|
232
|
+
|
|
233
|
+
const id = msg["@"]
|
|
234
|
+
const cb = queue[id]
|
|
235
|
+
if (cb) {
|
|
236
|
+
delete msg["#"]
|
|
237
|
+
delete msg["@"]
|
|
238
|
+
cb(msg)
|
|
239
|
+
|
|
240
|
+
delete queue[id]
|
|
241
|
+
}
|
|
242
|
+
})
|
|
243
|
+
})
|
|
244
|
+
return api(send)
|
|
245
|
+
}
|
|
246
|
+
|
|
247
|
+
let ws = new WebSocket("ws://localhost:8080")
|
|
248
|
+
const send = data => {
|
|
249
|
+
if (!ws || ws.readyState !== WebSocket.OPEN) {
|
|
250
|
+
console.log("websocket not available")
|
|
251
|
+
return
|
|
252
|
+
}
|
|
253
|
+
|
|
254
|
+
ws.send(data)
|
|
255
|
+
}
|
|
256
|
+
const start = () => {
|
|
257
|
+
if (!ws) ws = new WebSocket("ws://localhost:8080")
|
|
258
|
+
ws.onclose = c => {
|
|
259
|
+
ws = null
|
|
260
|
+
setTimeout(start, Math.floor(Math.random() * 5000))
|
|
261
|
+
}
|
|
262
|
+
ws.onerror = e => {
|
|
263
|
+
console.error(e)
|
|
264
|
+
}
|
|
265
|
+
ws.onmessage = m => {
|
|
266
|
+
const msg = JSON.parse(m.data)
|
|
267
|
+
if (dup.check(msg["#"])) return
|
|
268
|
+
|
|
269
|
+
dup.track(msg["#"])
|
|
270
|
+
if (msg.get) get(msg, send)
|
|
271
|
+
if (msg.put) put(msg, send)
|
|
272
|
+
send(m.data)
|
|
273
|
+
|
|
274
|
+
const id = msg["@"]
|
|
275
|
+
const cb = queue[id]
|
|
276
|
+
if (cb) {
|
|
277
|
+
delete msg["#"]
|
|
278
|
+
delete msg["@"]
|
|
279
|
+
cb(msg)
|
|
280
|
+
|
|
281
|
+
delete queue[id]
|
|
282
|
+
}
|
|
283
|
+
}
|
|
284
|
+
}
|
|
285
|
+
|
|
286
|
+
start()
|
|
287
|
+
return api(send)
|
|
288
|
+
}
|
|
289
|
+
|
|
290
|
+
export default Wire
|
|
@@ -0,0 +1,60 @@
|
|
|
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("holster.get", () => {
|
|
8
|
+
const wss = new Server("ws://localhost:1234")
|
|
9
|
+
const holster = Holster({file: "test/holster.get", wss: wss, maxAge: 100})
|
|
10
|
+
|
|
11
|
+
test("empty key callback null", (t, done) => {
|
|
12
|
+
holster.get("", data => {
|
|
13
|
+
assert.equal(data, null)
|
|
14
|
+
done()
|
|
15
|
+
})
|
|
16
|
+
})
|
|
17
|
+
|
|
18
|
+
test("null key callback null", (t, done) => {
|
|
19
|
+
holster.get(null, data => {
|
|
20
|
+
assert.equal(data, null)
|
|
21
|
+
done()
|
|
22
|
+
})
|
|
23
|
+
})
|
|
24
|
+
|
|
25
|
+
test("underscore as key callback null", (t, done) => {
|
|
26
|
+
holster.get("_", data => {
|
|
27
|
+
assert.equal(data, null)
|
|
28
|
+
done()
|
|
29
|
+
})
|
|
30
|
+
})
|
|
31
|
+
|
|
32
|
+
test("get unknown key callback null", (t, done) => {
|
|
33
|
+
holster.get("unknown", data => {
|
|
34
|
+
assert.equal(data, null)
|
|
35
|
+
done()
|
|
36
|
+
})
|
|
37
|
+
})
|
|
38
|
+
|
|
39
|
+
// Chaining get creates a new context for each call.
|
|
40
|
+
test("get chained unknown keys callback null", (t, done) => {
|
|
41
|
+
holster.get("chained").get("unknown", data => {
|
|
42
|
+
assert.equal(data, null)
|
|
43
|
+
done()
|
|
44
|
+
})
|
|
45
|
+
})
|
|
46
|
+
|
|
47
|
+
test("next chained unknown keys callback null", (t, done) => {
|
|
48
|
+
holster.get("chained").next("unknown", data => {
|
|
49
|
+
assert.equal(data, null)
|
|
50
|
+
done()
|
|
51
|
+
})
|
|
52
|
+
})
|
|
53
|
+
|
|
54
|
+
test("cleanup", (t, done) => {
|
|
55
|
+
fs.rm("test/holster.get", {recursive: true, force: true}, err => {
|
|
56
|
+
assert.equal(err, null)
|
|
57
|
+
done()
|
|
58
|
+
})
|
|
59
|
+
})
|
|
60
|
+
})
|
|
@@ -0,0 +1,196 @@
|
|
|
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("holster.lex", () => {
|
|
8
|
+
const wss = new Server("ws://localhost:1234")
|
|
9
|
+
const holster = Holster({file: "test/holster.lex", wss: wss, maxAge: 100})
|
|
10
|
+
|
|
11
|
+
test("object on root in graph format", (t, done) => {
|
|
12
|
+
const plain = {
|
|
13
|
+
key: "plain value",
|
|
14
|
+
true: true,
|
|
15
|
+
false: false,
|
|
16
|
+
number: 42,
|
|
17
|
+
}
|
|
18
|
+
holster.get("plain").put(plain, err => {
|
|
19
|
+
assert.equal(err, null)
|
|
20
|
+
|
|
21
|
+
// Prefix tests. (Need to be sequential to match return values.)
|
|
22
|
+
holster.get("plain", {".": {"*": "k"}}, data => {
|
|
23
|
+
assert.deepEqual(data, {key: "plain value"})
|
|
24
|
+
|
|
25
|
+
holster.get("plain", {".": {"*": "t"}}, data => {
|
|
26
|
+
assert.deepEqual(data, {true: true})
|
|
27
|
+
|
|
28
|
+
holster.get("plain", {".": {"*": "f"}}, data => {
|
|
29
|
+
assert.deepEqual(data, {false: false})
|
|
30
|
+
|
|
31
|
+
holster.get("plain", {".": {"*": "n"}}, data => {
|
|
32
|
+
assert.deepEqual(data, {number: 42})
|
|
33
|
+
})
|
|
34
|
+
|
|
35
|
+
// Both less than and greater than.
|
|
36
|
+
holster.get("plain", {".": {"<": "n", ">": "falsy"}}, data => {
|
|
37
|
+
assert.deepEqual(data, {key: "plain value"})
|
|
38
|
+
|
|
39
|
+
// Only less than.
|
|
40
|
+
holster.get("plain", {".": {"<": "k"}}, data => {
|
|
41
|
+
assert.deepEqual(data, {false: false})
|
|
42
|
+
|
|
43
|
+
// Only greater than.
|
|
44
|
+
holster.get("plain", {".": {">": "numbers"}}, data => {
|
|
45
|
+
assert.deepEqual(data, {true: true})
|
|
46
|
+
done()
|
|
47
|
+
})
|
|
48
|
+
})
|
|
49
|
+
})
|
|
50
|
+
})
|
|
51
|
+
})
|
|
52
|
+
})
|
|
53
|
+
})
|
|
54
|
+
})
|
|
55
|
+
|
|
56
|
+
test("put and get nested object", (t, done) => {
|
|
57
|
+
const nested = {
|
|
58
|
+
key: "nested value",
|
|
59
|
+
other: "other value",
|
|
60
|
+
child: {
|
|
61
|
+
has: "child value",
|
|
62
|
+
other: "other child value",
|
|
63
|
+
},
|
|
64
|
+
}
|
|
65
|
+
holster.get("nested").put(nested, err => {
|
|
66
|
+
assert.equal(err, null)
|
|
67
|
+
|
|
68
|
+
// Getting a nested object requires waiting for radisk to write to disk,
|
|
69
|
+
// as it will batch the writes. (Default wait is 1 millisecond.)
|
|
70
|
+
setTimeout(() => {
|
|
71
|
+
holster.get("nested", {".": {"*": "k"}}, data => {
|
|
72
|
+
assert.deepEqual(data, {key: "nested value"})
|
|
73
|
+
|
|
74
|
+
holster.get("nested").next("child", {".": {"*": "h"}}, data => {
|
|
75
|
+
assert.deepEqual(data, {has: "child value"})
|
|
76
|
+
|
|
77
|
+
holster.get("nested").next("child", {".": {"<": "has"}}, data => {
|
|
78
|
+
// Less than means less than or equal to in lex.
|
|
79
|
+
assert.deepEqual(data, {has: "child value"})
|
|
80
|
+
done()
|
|
81
|
+
})
|
|
82
|
+
})
|
|
83
|
+
})
|
|
84
|
+
}, 2)
|
|
85
|
+
})
|
|
86
|
+
})
|
|
87
|
+
|
|
88
|
+
test("chained get before put nested object", (t, done) => {
|
|
89
|
+
const nested = {
|
|
90
|
+
key: "hello nested value",
|
|
91
|
+
other: "other value",
|
|
92
|
+
child: {
|
|
93
|
+
has: "hello child value",
|
|
94
|
+
other: "other child value",
|
|
95
|
+
},
|
|
96
|
+
}
|
|
97
|
+
holster
|
|
98
|
+
.get("hello")
|
|
99
|
+
.next("nested")
|
|
100
|
+
.put(nested, err => {
|
|
101
|
+
assert.equal(err, null)
|
|
102
|
+
|
|
103
|
+
setTimeout(() => {
|
|
104
|
+
holster.get("hello").next("nested", {".": {"*": "k"}}, data => {
|
|
105
|
+
assert.deepEqual(data, {key: "hello nested value"})
|
|
106
|
+
|
|
107
|
+
holster
|
|
108
|
+
.get("hello")
|
|
109
|
+
.next("nested")
|
|
110
|
+
.next("child", {".": {"*": "h"}}, data => {
|
|
111
|
+
assert.deepEqual(data, {has: "hello child value"})
|
|
112
|
+
|
|
113
|
+
holster
|
|
114
|
+
.get("hello")
|
|
115
|
+
.next("nested")
|
|
116
|
+
.next("child", {".": {">": "other"}}, data => {
|
|
117
|
+
// Greater than means greater than or equal to in lex.
|
|
118
|
+
assert.deepEqual(data, {other: "other child value"})
|
|
119
|
+
done()
|
|
120
|
+
})
|
|
121
|
+
})
|
|
122
|
+
})
|
|
123
|
+
}, 2)
|
|
124
|
+
})
|
|
125
|
+
})
|
|
126
|
+
|
|
127
|
+
test("put and get two nested objects", (t, done) => {
|
|
128
|
+
const two = {
|
|
129
|
+
key: "two nested values",
|
|
130
|
+
child1: {
|
|
131
|
+
has: "child value 1",
|
|
132
|
+
},
|
|
133
|
+
child2: {
|
|
134
|
+
has: "child value 2",
|
|
135
|
+
},
|
|
136
|
+
}
|
|
137
|
+
holster.get("two").put(two, err => {
|
|
138
|
+
assert.equal(err, null)
|
|
139
|
+
|
|
140
|
+
setTimeout(() => {
|
|
141
|
+
holster.get("two", {".": {"*": "child"}}, data => {
|
|
142
|
+
assert.deepEqual(data, {
|
|
143
|
+
child1: two.child1,
|
|
144
|
+
child2: two.child2,
|
|
145
|
+
})
|
|
146
|
+
|
|
147
|
+
holster
|
|
148
|
+
.get("two")
|
|
149
|
+
.next("child1", {".": {">": "g", "<": "i"}}, data => {
|
|
150
|
+
assert.deepEqual(data, {has: "child value 1"})
|
|
151
|
+
done()
|
|
152
|
+
})
|
|
153
|
+
})
|
|
154
|
+
}, 2)
|
|
155
|
+
})
|
|
156
|
+
})
|
|
157
|
+
|
|
158
|
+
test("on and put nested object", (t, done) => {
|
|
159
|
+
const nested = {
|
|
160
|
+
key: "nested key",
|
|
161
|
+
child: {
|
|
162
|
+
has: "child key",
|
|
163
|
+
},
|
|
164
|
+
}
|
|
165
|
+
// The node needs to exist before it can be listend to for updates.
|
|
166
|
+
holster.get("on-nested").put(nested, err => {
|
|
167
|
+
assert.equal(err, null)
|
|
168
|
+
|
|
169
|
+
const update = {
|
|
170
|
+
child: {
|
|
171
|
+
has: "child update",
|
|
172
|
+
},
|
|
173
|
+
}
|
|
174
|
+
holster
|
|
175
|
+
.get("on-nested")
|
|
176
|
+
.next("child")
|
|
177
|
+
.on({".": {">": "h"}}, data => {
|
|
178
|
+
assert.deepEqual(data, update.child)
|
|
179
|
+
done()
|
|
180
|
+
})
|
|
181
|
+
|
|
182
|
+
setTimeout(() => {
|
|
183
|
+
holster.get("on-nested").put(update, err => {
|
|
184
|
+
assert.equal(err, null)
|
|
185
|
+
})
|
|
186
|
+
}, 10)
|
|
187
|
+
})
|
|
188
|
+
})
|
|
189
|
+
|
|
190
|
+
test("cleanup", (t, done) => {
|
|
191
|
+
fs.rm("test/holster.lex", {recursive: true, force: true}, err => {
|
|
192
|
+
assert.equal(err, null)
|
|
193
|
+
done()
|
|
194
|
+
})
|
|
195
|
+
})
|
|
196
|
+
})
|
|
@@ -0,0 +1,53 @@
|
|
|
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("holster.off", () => {
|
|
8
|
+
const wss = new Server("ws://localhost:1234")
|
|
9
|
+
const holster = Holster({file: "test/holster.off", wss: wss, maxAge: 100})
|
|
10
|
+
|
|
11
|
+
test("calling off without get callback null", (t, done) => {
|
|
12
|
+
holster.off(data => {
|
|
13
|
+
assert.equal(data, null)
|
|
14
|
+
done()
|
|
15
|
+
})
|
|
16
|
+
})
|
|
17
|
+
|
|
18
|
+
test("off with cb for property on root then update - no event", (t, done) => {
|
|
19
|
+
const cb = data => {
|
|
20
|
+
console.log("should not be called for key:", data)
|
|
21
|
+
done()
|
|
22
|
+
}
|
|
23
|
+
holster.get("key").on(cb)
|
|
24
|
+
holster.get("key").off(cb)
|
|
25
|
+
|
|
26
|
+
holster.get("key").put("value", err => {
|
|
27
|
+
assert.equal(err, null)
|
|
28
|
+
done()
|
|
29
|
+
})
|
|
30
|
+
})
|
|
31
|
+
|
|
32
|
+
test("off no cb for property on root then update - no event", (t, done) => {
|
|
33
|
+
holster.get("key1").on(data => {
|
|
34
|
+
console.log("should not be called for key1:", data)
|
|
35
|
+
done()
|
|
36
|
+
})
|
|
37
|
+
setTimeout(() => {
|
|
38
|
+
holster.get("key1").off()
|
|
39
|
+
|
|
40
|
+
holster.get("key1").put("value1", err => {
|
|
41
|
+
assert.equal(err, null)
|
|
42
|
+
done()
|
|
43
|
+
})
|
|
44
|
+
}, 10)
|
|
45
|
+
})
|
|
46
|
+
|
|
47
|
+
test("cleanup", (t, done) => {
|
|
48
|
+
fs.rm("test/holster.off", {recursive: true, force: true}, err => {
|
|
49
|
+
assert.equal(err, null)
|
|
50
|
+
done()
|
|
51
|
+
})
|
|
52
|
+
})
|
|
53
|
+
})
|