@mblaney/holster 0.1.0 → 0.3.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/examples/user.js CHANGED
@@ -71,6 +71,11 @@ setTimeout(() => {
71
71
  }, 2000)
72
72
 
73
73
  setTimeout(() => {
74
+ if (!bob) {
75
+ console.log("Something went wrong, bob didn't log in.")
76
+ return
77
+ }
78
+
74
79
  // Use Bob's public key to fetch shared data under his account.
75
80
  holster.user(bob.pub).get("shared", enc => {
76
81
  user.auth("alice", "password", async err => {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@mblaney/holster",
3
- "version": "0.1.0",
3
+ "version": "0.3.0",
4
4
  "description": "Holster is a service that allows synchronising data between devices using Node, Deno, Bun or the browser.",
5
5
  "type": "module",
6
6
  "main": "index.js",
@@ -10,7 +10,7 @@
10
10
  },
11
11
  "keywords": [],
12
12
  "author": "Malcolm Blaney",
13
- "license": "Zlib OR MIT OR Apache-2.0",
13
+ "license": "Zlib, MIT, Apache-2.0",
14
14
  "dependencies": {
15
15
  "express": "^5.1.0",
16
16
  "fs": "^0.0.1-security",
package/src/holster.js CHANGED
@@ -4,12 +4,18 @@ import User from "./user.js"
4
4
  import SEA from "./sea.js"
5
5
 
6
6
  const Holster = opt => {
7
+ if (typeof opt === "string") opt = {peers: [opt]}
8
+ else if (opt instanceof Array) opt = {peers: opt}
9
+ else if (!utils.obj.is(opt)) opt = {}
10
+
7
11
  const wire = Wire(opt)
8
12
  const user = User(null, wire)
9
13
  // Map callbacks since the user's callback is not passed to wire.on.
10
14
  const map = new Map()
11
15
  // Allow concurrent calls to the api by storing each context.
12
16
  const allctx = new Map()
17
+ // Allow user queries by public key
18
+ let publicKey
13
19
 
14
20
  const ok = data => {
15
21
  return (
@@ -45,51 +51,50 @@ const Holster = opt => {
45
51
  }
46
52
 
47
53
  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
54
+ const get = (lex, soul, ack, opt) => {
55
+ wire.get(
56
+ utils.obj.put(lex, "#", soul),
57
+ async msg => {
58
+ if (msg.err) console.log(msg.err)
59
+ if (msg.put && msg.put[soul]) {
60
+ delete msg.put[soul]._
61
+ delete msg.put[soul][utils.userPublicKey]
62
+ // Resolve any rels on the node before returning to the user.
63
+ for (const key of Object.keys(msg.put[soul])) {
64
+ const id = utils.rel.is(msg.put[soul][key])
65
+ if (id) {
66
+ const data = await new Promise(res => {
67
+ const _ctxid = utils.text.random()
68
+ allctx.set(_ctxid, {chain: [{item: null, soul: id}]})
69
+ api(_ctxid).next(null, res)
70
+ })
71
+ msg.put[soul][key] = data
72
+ }
64
73
  }
74
+ ack(msg.put[soul])
75
+ } else {
76
+ // No data callback.
77
+ ack(null)
65
78
  }
66
- ack(msg.put[soul])
67
- } else {
68
- // No data callback.
69
- ack(null)
70
- }
71
- })
79
+ },
80
+ opt,
81
+ )
72
82
  }
73
83
 
74
84
  const graph = async (soul, data, cb) => {
75
85
  if (!cb) cb = console.log
76
86
 
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)
87
+ if (user.is) {
88
+ const signed = await SEA.sign(data, user.is)
89
+ return utils.graph(soul, signed.m, signed.s, user.is.pub)
84
90
  }
85
91
 
86
- if (!user.is) {
87
- cb(`error putting data on ${soul}: user not authenticated`)
92
+ if (opt.secure) {
93
+ cb(`error putting data on ${soul}: user required in secure mode`)
88
94
  return null
89
95
  }
90
96
 
91
- const signed = await SEA.sign(data, user.is)
92
- return utils.graph(soul, signed.m, signed.s, user.is.pub)
97
+ return utils.graph(soul, data)
93
98
  }
94
99
 
95
100
  const done = data => {
@@ -190,7 +195,7 @@ const Holster = opt => {
190
195
  }
191
196
 
192
197
  return {
193
- get: (key, lex, cb) => {
198
+ get: (key, lex, cb, opt) => {
194
199
  if (typeof lex === "function") {
195
200
  cb = lex
196
201
  lex = null
@@ -202,14 +207,14 @@ const Holster = opt => {
202
207
 
203
208
  // lex requires a callback as it's not included in the chain below.
204
209
  if (lex && !cb) {
205
- console.log("lex requires a callback function")
210
+ console.log("error lex requires a callback function")
206
211
  return
207
212
  }
208
213
 
209
214
  // Top level keys are added to a root node so their values don't need
210
215
  // 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"
216
+ // publicKey or user.ctx is set.
217
+ const root = publicKey ? publicKey : user.ctx ? user.ctx : "root"
213
218
 
214
219
  ctxid = utils.text.random()
215
220
  allctx.set(ctxid, {chain: [{item: key, soul: root}], cb: cb})
@@ -217,9 +222,9 @@ const Holster = opt => {
217
222
 
218
223
  // When there's a callback need to resolve the context first.
219
224
  const {soul} = resolve({get: lex}, done)
220
- if (soul) get(lex, soul, done)
225
+ if (soul) get(lex, soul, done, opt)
221
226
  },
222
- next: (key, lex, cb) => {
227
+ next: (key, lex, cb, opt) => {
223
228
  const ack = data => {
224
229
  cb ? cb(data) : done(data)
225
230
  }
@@ -229,7 +234,7 @@ const Holster = opt => {
229
234
  lex = null
230
235
  }
231
236
  if (!ctxid) {
232
- console.log("please provide a key using get(key)")
237
+ console.log("error please provide a key using get(key)")
233
238
  ack(null)
234
239
  return
235
240
  }
@@ -241,7 +246,7 @@ const Holster = opt => {
241
246
 
242
247
  // lex requires a callback as it's not included in the chain below.
243
248
  if (lex && !cb) {
244
- console.log("lex requires a callback function")
249
+ console.log("error lex requires a callback function")
245
250
  return
246
251
  }
247
252
 
@@ -262,9 +267,13 @@ const Holster = opt => {
262
267
 
263
268
  // When there's a callback need to resolve the context first.
264
269
  const {soul} = resolve({get: lex}, ack)
265
- if (soul) get(lex, soul, ack)
270
+ if (soul) get(lex, soul, ack, opt)
266
271
  },
267
- put: (data, cb) => {
272
+ put: (data, set, cb) => {
273
+ if (typeof set === "function") {
274
+ cb = set
275
+ set = false
276
+ }
268
277
  const ack = err => {
269
278
  cb ? cb(err) : done(err)
270
279
  }
@@ -285,6 +294,7 @@ const Holster = opt => {
285
294
  ctx.cb = cb
286
295
  cb = null
287
296
  }
297
+ if (set) data = {[utils.text.random()]: data}
288
298
 
289
299
  const result = check(data)
290
300
  if (typeof result === "string") {
@@ -423,7 +433,7 @@ const Holster = opt => {
423
433
  if (!cb) return
424
434
 
425
435
  if (!ctxid) {
426
- console.log("please provide a key using get(key)")
436
+ console.log("error please provide a key using get(key)")
427
437
  cb(null)
428
438
  return
429
439
  }
@@ -458,7 +468,7 @@ const Holster = opt => {
458
468
  },
459
469
  off: cb => {
460
470
  if (!ctxid) {
461
- console.log("please provide a key using get(key)")
471
+ console.log("error please provide a key using get(key)")
462
472
  if (cb) cb(null)
463
473
  return
464
474
  }
@@ -487,7 +497,8 @@ const Holster = opt => {
487
497
  // will set the user context to that key, otherwise user.ctx is set when
488
498
  // a user logs in so that their public key will be used as the root node
489
499
  // in get(), also put() will sign all graph updates for verification.
490
- if (pub) user.ctx = "~" + pub
500
+ if (pub) publicKey = "~" + pub
501
+ else publicKey = null
491
502
  return Object.assign(user, api())
492
503
  },
493
504
  // Allow the wire spec to be used via holster.
package/src/index.js CHANGED
@@ -3,7 +3,6 @@ import path from "path"
3
3
  import {fileURLToPath} from "url"
4
4
  import Holster from "./holster.js"
5
5
 
6
- // TODO: pass in opt for websocket url, etc.
7
6
  const holster = Holster()
8
7
 
9
8
  // Note that express is only used here to serve the examples folder. It is not
package/src/sea-utils.js CHANGED
@@ -2,7 +2,7 @@ import SafeBuffer from "./buffer.js"
2
2
 
3
3
  const isNode = typeof document === "undefined"
4
4
  const crypto = isNode
5
- ? (await import("node:crypto")).webcrypto
5
+ ? (await import(/*webpackIgnore: true*/ "node:crypto")).webcrypto
6
6
  : globalThis.crypto
7
7
  export const subtle = crypto.subtle
8
8
 
package/src/store.js CHANGED
@@ -3,7 +3,7 @@ import Radix from "./radix.js"
3
3
  import * as utils from "./utils.js"
4
4
 
5
5
  const isNode = typeof document === "undefined"
6
- const fs = isNode ? await import("node:fs") : undefined
6
+ const fs = isNode ? await import(/*webpackIgnore: true*/ "node:fs") : undefined
7
7
 
8
8
  // ASCII character for enquiry.
9
9
  const enq = String.fromCharCode(5)
package/src/wire.js CHANGED
@@ -15,8 +15,6 @@ if (typeof globalThis.WebSocket === "undefined") {
15
15
  // Wire starts a websocket client or server and returns get and put methods
16
16
  // for access to the wire spec and storage.
17
17
  const Wire = opt => {
18
- if (!utils.obj.is(opt)) opt = {}
19
-
20
18
  const dup = Dup(opt.maxAge)
21
19
  const store = Store(opt)
22
20
  const graph = {}
@@ -207,7 +205,10 @@ const Wire = opt => {
207
205
  // mock-sockets provides clients as a function that returns an array.
208
206
  let clients = () => wss.clients()
209
207
  if (!wss) {
210
- wss = new wsModule.WebSocketServer({port: 8080})
208
+ const config = opt.server
209
+ ? {server: opt.server}
210
+ : {port: opt.port || 8765}
211
+ wss = new wsModule.WebSocketServer(config)
211
212
  clients = () => wss.clients
212
213
  }
213
214
 
@@ -244,46 +245,57 @@ const Wire = opt => {
244
245
  return api(send)
245
246
  }
246
247
 
247
- let ws = new WebSocket("ws://localhost:8080")
248
+ const peers = []
248
249
  const send = data => {
249
- if (!ws || ws.readyState !== WebSocket.OPEN) {
250
- console.log("websocket not available")
251
- return
252
- }
253
-
254
- ws.send(data)
250
+ peers.forEach(peer => {
251
+ if (peer && peer.readyState === WebSocket.OPEN) {
252
+ peer.send(data)
253
+ } else {
254
+ console.log("websocket not available")
255
+ }
256
+ })
255
257
  }
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
258
+ if (!(opt.peers instanceof Array)) {
259
+ opt.peers = [`ws://localhost:${opt.port || 8765}`]
260
+ }
261
+ opt.peers.forEach(peer => {
262
+ let ws = new WebSocket(peer)
263
+ peers.push(ws)
264
+ const start = () => {
265
+ if (!ws) ws = new WebSocket(peer)
266
+ ws.onclose = c => {
267
+ if (peers.indexOf(ws) !== -1) {
268
+ peers.splice(peers.indexOf(ws), 1)
269
+ }
270
+ ws = null
271
+ setTimeout(start, Math.floor(Math.random() * 5000))
272
+ }
273
+ ws.onerror = e => {
274
+ console.error(e)
275
+ }
276
+ ws.onmessage = m => {
277
+ const msg = JSON.parse(m.data)
278
+ if (dup.check(msg["#"])) return
268
279
 
269
- dup.track(msg["#"])
270
- if (msg.get) get(msg, send)
271
- if (msg.put) put(msg, send)
272
- send(m.data)
280
+ dup.track(msg["#"])
281
+ if (msg.get) get(msg, send)
282
+ if (msg.put) put(msg, send)
283
+ send(m.data)
273
284
 
274
- const id = msg["@"]
275
- const cb = queue[id]
276
- if (cb) {
277
- delete msg["#"]
278
- delete msg["@"]
279
- cb(msg)
285
+ const id = msg["@"]
286
+ const cb = queue[id]
287
+ if (cb) {
288
+ delete msg["#"]
289
+ delete msg["@"]
290
+ cb(msg)
280
291
 
281
- delete queue[id]
292
+ delete queue[id]
293
+ }
282
294
  }
283
295
  }
284
- }
296
+ start()
297
+ })
285
298
 
286
- start()
287
299
  return api(send)
288
300
  }
289
301
 
@@ -269,6 +269,65 @@ describe("holster.put", () => {
269
269
  })
270
270
  })
271
271
 
272
+ test("holster put string with set", (t, done) => {
273
+ holster.get("set").put("first", true, err => {
274
+ assert.equal(err, null)
275
+
276
+ holster.get("set", data => {
277
+ assert.equal(Object.values(data)[0], "first")
278
+
279
+ setTimeout(() => {
280
+ holster.get("set").put("second", true, err => {
281
+ assert.equal(err, null)
282
+
283
+ holster.get("set", data => {
284
+ for (const value of Object.values(data)) {
285
+ assert.ok(value === "first" || value === "second")
286
+ }
287
+ done()
288
+ })
289
+ })
290
+ }, 2)
291
+ })
292
+ })
293
+ })
294
+
295
+ test("holster put object with set", (t, done) => {
296
+ const set1 = {
297
+ key: "value 1",
298
+ child: "child value 1",
299
+ }
300
+ const set2 = {
301
+ key: "value 2",
302
+ child: "child value 2",
303
+ }
304
+ holster.get("set2").put(set1, true, err => {
305
+ assert.equal(err, null)
306
+
307
+ holster.get("set2", data => {
308
+ assert.deepEqual(Object.values(data)[0], set1)
309
+
310
+ setTimeout(() => {
311
+ holster.get("set2").put(set2, true, err => {
312
+ assert.equal(err, null)
313
+
314
+ holster.get("set2", data => {
315
+ for (const value of Object.values(data)) {
316
+ if (value.key === "value 1") {
317
+ assert.deepEqual(value, set1)
318
+ }
319
+ if (value.key === "value 2") {
320
+ assert.deepEqual(value, set2)
321
+ }
322
+ }
323
+ done()
324
+ })
325
+ })
326
+ }, 2)
327
+ })
328
+ })
329
+ })
330
+
272
331
  test("cleanup", (t, done) => {
273
332
  fs.rm("test/holster.put", {recursive: true, force: true}, err => {
274
333
  assert.equal(err, null)
@@ -291,7 +291,25 @@ describe("holster.user.put", () => {
291
291
  })
292
292
  })
293
293
 
294
- test("get number using public key", (t, done) => {
294
+ test("get number using public key - logged in", (t, done) => {
295
+ let ok = false
296
+
297
+ holster.user(alice).get("pi", data => {
298
+ assert.equal(data, 3.14159)
299
+ if (ok) done()
300
+ else ok = true
301
+ })
302
+
303
+ // Not reusing the existing user here resets the stored public key used
304
+ // above, but that is ok because they use separate contexts.
305
+ holster.user().get("pi", data => {
306
+ assert.equal(data, 3.14159)
307
+ if (ok) done()
308
+ else ok = true
309
+ })
310
+ })
311
+
312
+ test("get number using public key - logged out", (t, done) => {
295
313
  user.leave()
296
314
 
297
315
  holster.user(alice).get("pi", data => {