@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.
Files changed (46) hide show
  1. package/.github/workflows/node.js.yml +31 -0
  2. package/LICENSE.md +55 -0
  3. package/README.md +24 -0
  4. package/build/holster.js +4 -0
  5. package/build/holster.js.map +7 -0
  6. package/examples/index.html +33 -0
  7. package/examples/index.js +49 -0
  8. package/examples/user.html +31 -0
  9. package/examples/user.js +88 -0
  10. package/examples/wire.html +27 -0
  11. package/examples/wire.js +55 -0
  12. package/package.json +44 -0
  13. package/src/array.js +34 -0
  14. package/src/buffer.js +83 -0
  15. package/src/dup.js +23 -0
  16. package/src/get.js +19 -0
  17. package/src/ham.js +151 -0
  18. package/src/holster.js +502 -0
  19. package/src/index.js +21 -0
  20. package/src/radisk.js +438 -0
  21. package/src/radix.js +106 -0
  22. package/src/sea-utils.js +65 -0
  23. package/src/sea.js +224 -0
  24. package/src/store.js +225 -0
  25. package/src/user.js +330 -0
  26. package/src/utils.js +124 -0
  27. package/src/wire.js +290 -0
  28. package/test/holster.get.test.js +60 -0
  29. package/test/holster.lex.test.js +196 -0
  30. package/test/holster.off.test.js +53 -0
  31. package/test/holster.on.test.js +168 -0
  32. package/test/holster.put.test.js +278 -0
  33. package/test/holster.secure.test.js +210 -0
  34. package/test/holster.user.get.test.js +86 -0
  35. package/test/holster.user.lex.test.js +148 -0
  36. package/test/holster.user.off.test.js +72 -0
  37. package/test/holster.user.on.test.js +185 -0
  38. package/test/holster.user.put.test.js +381 -0
  39. package/test/kitty/! +9 -0
  40. package/test/names.js +225 -0
  41. package/test/radisk.test.js +191 -0
  42. package/test/radix.test.js +117 -0
  43. package/test/sea.test.js +284 -0
  44. package/test/store.test.js +316 -0
  45. package/test/user.test.js +173 -0
  46. package/test/wire.test.js +159 -0
package/src/holster.js ADDED
@@ -0,0 +1,502 @@
1
+ import * as utils from "./utils.js"
2
+ import Wire from "./wire.js"
3
+ import User from "./user.js"
4
+ import SEA from "./sea.js"
5
+
6
+ const Holster = opt => {
7
+ const wire = Wire(opt)
8
+ const user = User(null, wire)
9
+ // Map callbacks since the user's callback is not passed to wire.on.
10
+ const map = new Map()
11
+ // Allow concurrent calls to the api by storing each context.
12
+ const allctx = new Map()
13
+
14
+ const ok = data => {
15
+ return (
16
+ data === null ||
17
+ data === true ||
18
+ data === false ||
19
+ typeof data === "string" ||
20
+ utils.rel.is(data) ||
21
+ utils.num.is(data)
22
+ )
23
+ }
24
+
25
+ // check returns true if data is ok to add to a graph, an error string if
26
+ // the data can't be converted, and the keys on the data object otherwise.
27
+ const check = data => {
28
+ if (ok(data)) return true
29
+
30
+ if (utils.obj.is(data)) {
31
+ const keys = []
32
+ for (const [key, value] of Object.entries(data)) {
33
+ if (key === "_") {
34
+ return "error underscore cannot be used as an item name"
35
+ }
36
+ if (utils.obj.is(value) || ok(value)) {
37
+ keys.push(key)
38
+ continue
39
+ }
40
+ return `error {${key}:${value}} cannot be converted to graph`
41
+ }
42
+ if (keys.length !== 0) return keys
43
+ }
44
+ return `error ${data} cannot be converted to a graph`
45
+ }
46
+
47
+ const api = ctxid => {
48
+ const get = (lex, soul, ack) => {
49
+ wire.get(utils.obj.put(lex, "#", soul), async msg => {
50
+ if (msg.err) console.log(msg.err)
51
+ if (msg.put && msg.put[soul]) {
52
+ delete msg.put[soul]._
53
+ delete msg.put[soul][utils.userPublicKey]
54
+ // Resolve any rels on the node before returning to the user.
55
+ for (const key of Object.keys(msg.put[soul])) {
56
+ const id = utils.rel.is(msg.put[soul][key])
57
+ if (id) {
58
+ const data = await new Promise(res => {
59
+ const _ctxid = utils.text.random()
60
+ allctx.set(_ctxid, {chain: [{item: null, soul: id}]})
61
+ api(_ctxid).next(null, res)
62
+ })
63
+ msg.put[soul][key] = data
64
+ }
65
+ }
66
+ ack(msg.put[soul])
67
+ } else {
68
+ // No data callback.
69
+ ack(null)
70
+ }
71
+ })
72
+ }
73
+
74
+ const graph = async (soul, data, cb) => {
75
+ if (!cb) cb = console.log
76
+
77
+ if (!user.is) {
78
+ if (opt.secure) {
79
+ cb(`error putting data on ${soul}: user required in secure mode`)
80
+ return null
81
+ }
82
+
83
+ return utils.graph(soul, data)
84
+ }
85
+
86
+ if (!user.is) {
87
+ cb(`error putting data on ${soul}: user not authenticated`)
88
+ return null
89
+ }
90
+
91
+ const signed = await SEA.sign(data, user.is)
92
+ return utils.graph(soul, signed.m, signed.s, user.is.pub)
93
+ }
94
+
95
+ const done = data => {
96
+ const ctx = allctx.get(ctxid)
97
+ if (ctx && typeof ctx.cb !== "undefined") ctx.cb(data)
98
+ else if (data) console.log(data)
99
+ // A context updated by "on" should only be removed by "off".
100
+ if (ctx && !ctx.on) allctx.delete(ctxid)
101
+ }
102
+
103
+ const resolve = (request, cb) => {
104
+ const get = request && typeof request.get !== "undefined"
105
+ const put = request && typeof request.put !== "undefined"
106
+ const on = request && typeof request.on !== "undefined"
107
+ const off = request && typeof request.off !== "undefined"
108
+
109
+ let found = false
110
+ const ctx = allctx.get(ctxid)
111
+ for (var i = 1; i < ctx.chain.length; i++) {
112
+ if (ctx.chain[i].soul !== null) continue
113
+
114
+ found = true
115
+ break
116
+ }
117
+
118
+ if (found) {
119
+ // Found a soul that needs resolving, need the previous context
120
+ // (ie the parent node) to find a soul relation for it.
121
+ const {item, soul} = ctx.chain[i - 1]
122
+ wire.get({"#": soul, ".": item}, async msg => {
123
+ if (msg.err) {
124
+ console.log(`error getting ${item} on ${soul}: ${msg.err}`)
125
+ if (cb) cb(null)
126
+ return
127
+ }
128
+
129
+ const node = msg.put && msg.put[soul]
130
+ if (node && typeof node[item] !== "undefined") {
131
+ let id = utils.rel.is(node[item])
132
+ if (id) {
133
+ ctx.chain[i].soul = id
134
+ // Not sure why the map needs to be set rather than just ctx?
135
+ allctx.set(ctxid, {chain: ctx.chain, cb: ctx.cb})
136
+ // Call api again using the updated context.
137
+ if (get) api(ctxid).next(null, request.get, cb)
138
+ else if (put) api(ctxid).put(request.put, cb)
139
+ else if (on) api(ctxid).on(request.on, cb)
140
+ else if (off) api(ctxid).off(cb)
141
+ } else if (get) {
142
+ // Request was not for a node, return a property on the current
143
+ // soul.
144
+ cb(node[item])
145
+ } else if (put) {
146
+ // Request was chained before put, so rel doesn't exist yet.
147
+ id = utils.text.random()
148
+ const rel = {[item]: utils.rel.ify(id)}
149
+ const g = await graph(soul, rel, cb)
150
+ if (g === null) return
151
+
152
+ wire.put(g, err => {
153
+ if (err) {
154
+ cb(`error putting ${item} on ${soul}: ${err}`)
155
+ return
156
+ }
157
+
158
+ ctx.chain[i].soul = id
159
+ api(ctxid).put(request.put, cb)
160
+ })
161
+ } else if (on) {
162
+ console.log(`error resolving on for ${item} on ${soul}`)
163
+ cb(null)
164
+ } else if (off) {
165
+ console.log(`error resolving off for ${item} on ${soul}`)
166
+ if (cb) cb(null)
167
+ }
168
+ } else if (put) {
169
+ cb(`error ${item} not found on ${soul}`)
170
+ } else {
171
+ console.log(`error ${item} not found on ${soul}`)
172
+ if (cb) cb(null)
173
+ }
174
+ })
175
+ // Callback has been passed to next soul lookup or called above, so
176
+ // return false as the calling code should not continue.
177
+ return false
178
+ }
179
+
180
+ if (get && ctx.chain[ctx.chain.length - 1].item !== null) {
181
+ // The context has been resolved but it does not include the requested
182
+ // node, which requires one more lookup.
183
+ ctx.chain.push({item: null, soul: null})
184
+ api(ctxid).next(null, request.get, cb)
185
+ return false
186
+ }
187
+
188
+ // Return the last context, ie the soul required by the calling code.
189
+ return ctx.chain[ctx.chain.length - 1]
190
+ }
191
+
192
+ return {
193
+ get: (key, lex, cb) => {
194
+ if (typeof lex === "function") {
195
+ cb = lex
196
+ lex = null
197
+ }
198
+ if (key === null || key === "" || key === "_") {
199
+ if (cb) cb(null)
200
+ return
201
+ }
202
+
203
+ // lex requires a callback as it's not included in the chain below.
204
+ if (lex && !cb) {
205
+ console.log("lex requires a callback function")
206
+ return
207
+ }
208
+
209
+ // Top level keys are added to a root node so their values don't need
210
+ // to be objects, the user's public key is used as the root node when
211
+ // user.ctx is set.
212
+ const root = user.ctx ? user.ctx : "root"
213
+
214
+ ctxid = utils.text.random()
215
+ allctx.set(ctxid, {chain: [{item: key, soul: root}], cb: cb})
216
+ if (!cb) return api(ctxid)
217
+
218
+ // When there's a callback need to resolve the context first.
219
+ const {soul} = resolve({get: lex}, done)
220
+ if (soul) get(lex, soul, done)
221
+ },
222
+ next: (key, lex, cb) => {
223
+ const ack = data => {
224
+ cb ? cb(data) : done(data)
225
+ }
226
+
227
+ if (typeof lex === "function") {
228
+ cb = lex
229
+ lex = null
230
+ }
231
+ if (!ctxid) {
232
+ console.log("please provide a key using get(key)")
233
+ ack(null)
234
+ return
235
+ }
236
+
237
+ if (key === "" || key === "_") {
238
+ ack(null)
239
+ return
240
+ }
241
+
242
+ // lex requires a callback as it's not included in the chain below.
243
+ if (lex && !cb) {
244
+ console.log("lex requires a callback function")
245
+ return
246
+ }
247
+
248
+ const ctx = allctx.get(ctxid)
249
+ // ctx already removed by another chained callback is ok?
250
+ if (!ctx) return
251
+
252
+ if (cb && typeof ctx.cb === "undefined") {
253
+ // This (and ack) allows nested objects to set their own callbacks.
254
+ ctx.cb = cb
255
+ cb = null
256
+ }
257
+
258
+ // Push the key to the context as it needs a soul lookup.
259
+ // (null is used to call the api with updated context)
260
+ if (key !== null) ctx.chain.push({item: key, soul: null})
261
+ if (!ctx.cb) return api(ctxid)
262
+
263
+ // When there's a callback need to resolve the context first.
264
+ const {soul} = resolve({get: lex}, ack)
265
+ if (soul) get(lex, soul, ack)
266
+ },
267
+ put: (data, cb) => {
268
+ const ack = err => {
269
+ cb ? cb(err) : done(err)
270
+ }
271
+
272
+ if (!ctxid) {
273
+ ack("please provide a key using get(key)")
274
+ return
275
+ }
276
+
277
+ const ctx = allctx.get(ctxid)
278
+ // ctx already removed by another chained callback is ok?
279
+ if (!ctx) return
280
+
281
+ if (!ctx.cb) {
282
+ if (!cb) return
283
+
284
+ // This (and ack) allows nested objects to set their own callbacks.
285
+ ctx.cb = cb
286
+ cb = null
287
+ }
288
+
289
+ const result = check(data)
290
+ if (typeof result === "string") {
291
+ // All strings returned from check are errors, cannot continue.
292
+ ack(result)
293
+ return
294
+ }
295
+
296
+ // Resolve the current context before putting data.
297
+ const {item, soul} = resolve({put: data}, ack)
298
+ if (!soul) return
299
+
300
+ if (result === true) {
301
+ // When result is true data is a property to put on the current soul.
302
+ // Need to check if item is a rel and also set the node to null. (This
303
+ // applies for any update from a rel to a property, not just null.)
304
+ wire.get({"#": soul, ".": item}, async msg => {
305
+ if (msg.err) {
306
+ console.log(`error getting ${soul}: ${msg.err}`)
307
+ return
308
+ }
309
+
310
+ const current = msg.put && msg.put[soul] && msg.put[soul][item]
311
+ const id = utils.rel.is(current)
312
+ if (!id) {
313
+ // Not a rel, can just put the data.
314
+ const g = await graph(soul, {[item]: data}, ack)
315
+ if (g === null) return
316
+
317
+ wire.put(g, ack)
318
+ return
319
+ }
320
+
321
+ wire.get({"#": id}, async msg => {
322
+ if (msg.err) {
323
+ console.log(`error getting ${id}: ${msg.err}`)
324
+ return
325
+ }
326
+
327
+ if (!msg.put || !msg.put[id]) {
328
+ console.log(`error ${id} not found`)
329
+ return
330
+ }
331
+
332
+ delete msg.put[id]._
333
+ // null each of the properties on the node before putting data.
334
+ for (const key of Object.keys(msg.put[id])) {
335
+ if (key === utils.userPublicKey) continue
336
+
337
+ const err = await new Promise(res => {
338
+ const _ctxid = utils.text.random()
339
+ allctx.set(_ctxid, {chain: [{item: key, soul: id}]})
340
+ api(_ctxid).put(null, res)
341
+ })
342
+ if (err) {
343
+ ack(err)
344
+ return
345
+ }
346
+ }
347
+ const g = await graph(soul, {[item]: data}, ack)
348
+ if (g === null) return
349
+
350
+ wire.put(g, ack)
351
+ })
352
+ })
353
+ return
354
+ }
355
+
356
+ // Otherwise put the data using the keys returned in result.
357
+ // Need to check if a rel has already been added on the current node.
358
+ wire.get({"#": soul, ".": item}, async msg => {
359
+ if (msg.err) {
360
+ ack(`error getting ${soul}.${item}: ${msg.err}`)
361
+ return
362
+ }
363
+
364
+ const current = msg.put && msg.put[soul] && msg.put[soul][item]
365
+ const id = utils.rel.is(current)
366
+ if (!id) {
367
+ // The current rel doesn't exist, so add it first.
368
+ const rel = {[item]: utils.rel.ify(utils.text.random())}
369
+ const g = await graph(soul, rel, ack)
370
+ if (g === null) return
371
+
372
+ wire.put(g, err => {
373
+ if (err) {
374
+ ack(`error putting ${item} on ${soul}: ${err}`)
375
+ } else {
376
+ const _ctxid = utils.text.random()
377
+ const chain = [{item: item, soul: soul}]
378
+ // Pass the previous context's callback on here.
379
+ allctx.set(_ctxid, {chain: chain, cb: ctx.cb})
380
+ api(_ctxid).put(data)
381
+ }
382
+ })
383
+ return
384
+ }
385
+
386
+ let put = false
387
+ const update = {}
388
+ for (const key of result) {
389
+ const err = await new Promise(res => {
390
+ if (utils.obj.is(data[key])) {
391
+ // Use the current rel as the context for nested objects.
392
+ const _ctxid = utils.text.random()
393
+ allctx.set(_ctxid, {chain: [{item: key, soul: id}]})
394
+ api(_ctxid).put(data[key], res)
395
+ } else {
396
+ put = true
397
+ // Group other properties into one update.
398
+ update[key] = data[key]
399
+ res(null)
400
+ }
401
+ })
402
+ if (err) {
403
+ ack(err)
404
+ return
405
+ }
406
+ }
407
+ if (put) {
408
+ const g = await graph(id, update, ack)
409
+ if (g === null) return
410
+
411
+ wire.put(g, ack)
412
+ } else {
413
+ // Callback on behalf of nested objects.
414
+ ack(null)
415
+ }
416
+ })
417
+ },
418
+ on: (lex, cb) => {
419
+ if (typeof lex === "function") {
420
+ cb = lex
421
+ lex = null
422
+ }
423
+ if (!cb) return
424
+
425
+ if (!ctxid) {
426
+ console.log("please provide a key using get(key)")
427
+ cb(null)
428
+ return
429
+ }
430
+
431
+ // Resolve the current context before adding event listener.
432
+ const {item, soul} = resolve({on: lex}, cb)
433
+ if (!soul) return
434
+
435
+ // Flag that this context is set from on and shouldn't be removed.
436
+ allctx.set(ctxid, {chain: [{item: item, soul: soul}], on: true})
437
+ // Map the user's callback because it can also be passed to off,
438
+ // so need a reference to it to compare them.
439
+ map.set(cb, () => api(ctxid).next(null, cb))
440
+ // Check if item is a rel and add event listener for the node.
441
+ wire.get({"#": soul, ".": item}, msg => {
442
+ if (msg.err) {
443
+ console.log(`error getting ${soul}.${item}: ${msg.err}`)
444
+ return
445
+ }
446
+
447
+ const current = msg.put && msg.put[soul] && msg.put[soul][item]
448
+ const id = utils.rel.is(current)
449
+ if (id) {
450
+ if (lex) lex = utils.obj.put(lex, "#", id)
451
+ else lex = {"#": id, ".": null}
452
+ } else {
453
+ if (lex) lex = utils.obj.put(lex, "#", soul)
454
+ else lex = {"#": soul, ".": item}
455
+ }
456
+ wire.on(lex, map.get(cb))
457
+ })
458
+ },
459
+ off: cb => {
460
+ if (!ctxid) {
461
+ console.log("please provide a key using get(key)")
462
+ if (cb) cb(null)
463
+ return
464
+ }
465
+
466
+ // Resolve the current context before removing event listener.
467
+ const {item, soul} = resolve({off: true}, cb)
468
+ if (!soul) return
469
+
470
+ // Check if item is a rel and remove event listener for the node.
471
+ wire.get({"#": soul, ".": item}, msg => {
472
+ if (msg.err) {
473
+ console.log(`error getting ${soul}.${item}: ${msg.err}`)
474
+ return
475
+ }
476
+
477
+ const current = msg.put && msg.put[soul] && msg.put[soul][item]
478
+ const id = utils.rel.is(current)
479
+ if (id) wire.off({"#": id}, map.get(cb))
480
+ else wire.off({"#": soul}, map.get(cb))
481
+ map.delete(cb)
482
+ allctx.delete(ctxid)
483
+ })
484
+ },
485
+ user: pub => {
486
+ // Return the combined Holster and User APIs. Passing in a public key
487
+ // will set the user context to that key, otherwise user.ctx is set when
488
+ // a user logs in so that their public key will be used as the root node
489
+ // in get(), also put() will sign all graph updates for verification.
490
+ if (pub) user.ctx = "~" + pub
491
+ return Object.assign(user, api())
492
+ },
493
+ // Allow the wire spec to be used via holster.
494
+ wire: wire,
495
+ // Allow SEA functions to be used via holster.
496
+ SEA: SEA,
497
+ }
498
+ }
499
+ return api()
500
+ }
501
+
502
+ export default Holster
package/src/index.js ADDED
@@ -0,0 +1,21 @@
1
+ import express from "express"
2
+ import path from "path"
3
+ import {fileURLToPath} from "url"
4
+ import Holster from "./holster.js"
5
+
6
+ // TODO: pass in opt for websocket url, etc.
7
+ const holster = Holster()
8
+
9
+ // Note that express is only used here to serve the examples folder. It is not
10
+ // required to run Holster itself.
11
+ const dirname = path.dirname(fileURLToPath(import.meta.url))
12
+
13
+ const app = express()
14
+
15
+ app.use(express.static(path.join(dirname, "..")))
16
+
17
+ app.get("/", (req, res) => {
18
+ res.redirect("/examples/index.html")
19
+ })
20
+
21
+ app.listen(3000)