@mblaney/holster 2.0.2 → 2.1.1

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/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@mblaney/holster",
3
- "version": "2.0.2",
3
+ "version": "2.1.1",
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",
@@ -24,7 +24,6 @@
24
24
  "author": "Malcolm Blaney",
25
25
  "license": "Zlib, MIT, Apache-2.0",
26
26
  "dependencies": {
27
- "express": "^5.1.0",
28
27
  "fs": "^0.0.1-security",
29
28
  "ws": "^8.18.0",
30
29
  "zod": "^4.1.13"
@@ -33,6 +32,7 @@
33
32
  "@types/node": "^24.10.1",
34
33
  "@types/ws": "^8.18.1",
35
34
  "esbuild": "0.25.3",
35
+ "express": "^5.1.0",
36
36
  "mock-socket": "^9.3.1",
37
37
  "prettier": "^3.4.2",
38
38
  "tsx": "^4.0.0",
package/src/ham.js CHANGED
@@ -35,7 +35,14 @@ const Ham = (state, currentState, value, currentValue, signed = false) => {
35
35
  return {incoming: true}
36
36
  }
37
37
 
38
- Ham.mix = async (change, graph, hasNode, secure, listen) => {
38
+ Ham.mix = async (
39
+ change,
40
+ graph,
41
+ hasNode,
42
+ secure,
43
+ listen,
44
+ maxGraphSize = MAX_GRAPH_SIZE,
45
+ ) => {
39
46
  if (!change || typeof change !== "object") {
40
47
  throw new TypeError("change must be an object")
41
48
  }
@@ -216,7 +223,7 @@ Ham.mix = async (change, graph, hasNode, secure, listen) => {
216
223
  }
217
224
 
218
225
  const souls = Object.keys(graph)
219
- if (souls.length > MAX_GRAPH_SIZE) {
226
+ if (souls.length > maxGraphSize) {
220
227
  // Sort by oldest state timestamp and remove oldest entries
221
228
  const soulsByAge = souls
222
229
  .map(soul => {
@@ -225,7 +232,7 @@ Ham.mix = async (change, graph, hasNode, secure, listen) => {
225
232
  return {soul, maxState}
226
233
  })
227
234
  .sort((a, b) => a.maxState - b.maxState)
228
- const remove = soulsByAge.slice(0, souls.length - MAX_GRAPH_SIZE)
235
+ const remove = soulsByAge.slice(0, souls.length - maxGraphSize)
229
236
  remove.forEach(({soul}) => {
230
237
  delete graph[soul]
231
238
  if (hasNode) hasNode.delete(soul)
package/src/holster.js CHANGED
@@ -64,28 +64,30 @@ const Holster = opt => {
64
64
  delete msg.put[soul][utils.userPublicKey]
65
65
  delete msg.put[soul][utils.userSignature]
66
66
  // Resolve any rels on the node before returning to the user.
67
- await Promise.all(Object.keys(msg.put[soul]).map(async key => {
68
- const id = utils.rel.is(msg.put[soul][key])
69
- if (!id) return
70
- const attemptRead = async (retries = 0) => {
71
- const data = await new Promise(res => {
72
- const _ctxid = utils.text.random()
73
- const ctx = allctx.get(ctxid)
74
- allctx.set(_ctxid, {
75
- chain: [{item: null, soul: id}],
76
- user: ctx ? ctx.user : null,
67
+ await Promise.all(
68
+ Object.keys(msg.put[soul]).map(async key => {
69
+ const id = utils.rel.is(msg.put[soul][key])
70
+ if (!id) return
71
+ const attemptRead = async (retries = 0) => {
72
+ const data = await new Promise(res => {
73
+ const _ctxid = utils.text.random()
74
+ const ctx = allctx.get(ctxid)
75
+ allctx.set(_ctxid, {
76
+ chain: [{item: null, soul: id}],
77
+ user: ctx ? ctx.user : null,
78
+ })
79
+ api(_ctxid).next(null, res, _opt)
77
80
  })
78
- api(_ctxid).next(null, res, _opt)
79
- })
80
- if (data !== null || retries >= 5) {
81
- return data
81
+ if (data !== null || retries >= 5) {
82
+ return data
83
+ }
84
+ // Data not ready, retry after delay
85
+ await new Promise(resolve => setTimeout(resolve, 50))
86
+ return attemptRead(retries + 1)
82
87
  }
83
- // Data not ready, retry after delay
84
- await new Promise(resolve => setTimeout(resolve, 50))
85
- return attemptRead(retries + 1)
86
- }
87
- msg.put[soul][key] = await attemptRead()
88
- }))
88
+ msg.put[soul][key] = await attemptRead()
89
+ }),
90
+ )
89
91
  ack(msg.put[soul])
90
92
  } else {
91
93
  // No data callback.
@@ -281,7 +283,11 @@ const Holster = opt => {
281
283
  if (request._get) cb(null)
282
284
  } else if (get && node) {
283
285
  const sv = node._ && node._[">"]
284
- if (sv && Object.keys(sv).length > 0 && typeof sv[item] === "undefined") {
286
+ if (
287
+ sv &&
288
+ Object.keys(sv).length > 0 &&
289
+ typeof sv[item] === "undefined"
290
+ ) {
285
291
  // State vector has other properties but not this one — never written.
286
292
  if (cb) cb(null)
287
293
  } else {
@@ -666,8 +672,11 @@ const Holster = opt => {
666
672
  chain: [{item: null, soul: id}],
667
673
  user: ctx ? ctx.user : null,
668
674
  })
669
- api(_ctxid).next(null, res, retries === 0 ?
670
- utils.obj.put(_opt, "fast", true) : _opt)
675
+ api(_ctxid).next(
676
+ null,
677
+ res,
678
+ retries === 0 ? utils.obj.put(_opt, "fast", true) : _opt,
679
+ )
671
680
  })
672
681
  if (data !== null || retries >= 5) {
673
682
  retryCount = 0 // Reset retry counter on success
@@ -693,8 +702,10 @@ const Holster = opt => {
693
702
  // miss and never fire again if the data doesn't change while
694
703
  // the listener is attached, so we must poll for initial load.
695
704
  const retry = () => {
696
- const delay =
697
- Math.min(retryDelay * Math.pow(2, retryCount), 30000)
705
+ const delay = Math.min(
706
+ retryDelay * Math.pow(2, retryCount),
707
+ 30000,
708
+ )
698
709
  retryCount++
699
710
  retryTimer = setTimeout(() => {
700
711
  wire.get(
package/src/radisk.js CHANGED
@@ -34,7 +34,7 @@ const Radisk = opt => {
34
34
  if (!opt.write) opt.write = 1 // Wait time before write in milliseconds
35
35
  if (!opt.size) opt.size = 1024 * 1024 // File size on disk, default 1MB
36
36
  if (!opt.memoryLimit) opt.memoryLimit = 500 // Memory limit in MB
37
- if (typeof opt.cache === "undefined") opt.cache = true
37
+ if (typeof opt.cache === "undefined") opt.cache = true // Used in tests
38
38
 
39
39
  if (!opt.store) {
40
40
  opt.log(
@@ -265,7 +265,7 @@ const Radisk = opt => {
265
265
  limit: "",
266
266
  done: false,
267
267
  count: 0,
268
- halfKey: "", // soul ID at the ~opt.size/2 mark for a balanced split
268
+ halfKey: "", // soul ID at the ~opt.size/2 mark for a balanced split
269
269
  halfOffset: 0, // write.text.length before that entry
270
270
  each: (value, key, k, pre) => {
271
271
  // each is called for all keys, but stop adding to write.text when
@@ -295,8 +295,7 @@ const Radisk = opt => {
295
295
  ) {
296
296
  const fullKey = pre.join("") + k
297
297
  const end = fullKey.indexOf(enq)
298
- write.halfKey =
299
- end === -1 ? fullKey : fullKey.substring(0, end)
298
+ write.halfKey = end === -1 ? fullKey : fullKey.substring(0, end)
300
299
  write.halfOffset = write.text.length
301
300
  }
302
301
 
@@ -308,8 +307,7 @@ const Radisk = opt => {
308
307
  const fullKey = pre.join("") + k
309
308
  const end = fullKey.indexOf(enq)
310
309
  write.limit =
311
- write.halfKey ||
312
- (end === -1 ? fullKey : fullKey.substring(0, end))
310
+ write.halfKey || (end === -1 ? fullKey : fullKey.substring(0, end))
313
311
  // Cannot split if the key is the same as the current file name.
314
312
  if (write.limit !== file) {
315
313
  // Truncate to the balanced split point — text after halfOffset is
@@ -355,17 +353,26 @@ const Radisk = opt => {
355
353
 
356
354
  // Binary search for largest filename <= soul in a sorted array.
357
355
  const findFile = files => {
358
- let lo = 0, hi = files.length - 1, result = null
356
+ let lo = 0,
357
+ hi = files.length - 1,
358
+ result = null
359
359
  while (lo <= hi) {
360
360
  const mid = (lo + hi) >> 1
361
- if (files[mid] <= soul) { result = files[mid]; lo = mid + 1 }
362
- else { hi = mid - 1 }
361
+ if (files[mid] <= soul) {
362
+ result = files[mid]
363
+ lo = mid + 1
364
+ } else {
365
+ hi = mid - 1
366
+ }
363
367
  }
364
368
  return result
365
369
  }
366
370
 
367
371
  const read = file => {
368
- if (!file) { cb("no file found", u); return }
372
+ if (!file) {
373
+ cb("no file found", u)
374
+ return
375
+ }
369
376
  if (opt.cache && cache.has(file)) {
370
377
  return cb(u, cache.get(file)(key))
371
378
  }
@@ -380,12 +387,19 @@ const Radisk = opt => {
380
387
  }
381
388
 
382
389
  const now = Date.now()
383
- if (opt.cache && fileListCache && now - fileListCacheTime < FILE_LIST_CACHE_TTL) {
390
+ if (
391
+ opt.cache &&
392
+ fileListCache &&
393
+ now - fileListCacheTime < FILE_LIST_CACHE_TTL
394
+ ) {
384
395
  read(findFile(fileListCache, soul))
385
396
  } else {
386
397
  const files = []
387
398
  opt.store.list(file => {
388
- if (file) { files.push(file); return }
399
+ if (file) {
400
+ files.push(file)
401
+ return
402
+ }
389
403
  files.sort()
390
404
  if (opt.cache) {
391
405
  fileListCache = files
package/src/user.js CHANGED
@@ -28,54 +28,51 @@ const User = (opt, wire) => {
28
28
  }
29
29
 
30
30
  const pub = pubs.shift()
31
- wire.get(
32
- {"#": pub, ".": ["auth", "pub", "epub"]},
33
- async msg => {
34
- if (msg.err) {
35
- done(`error getting ${pub}: ${msg.err}`)
36
- return
37
- }
31
+ wire.get({"#": pub, ".": ["auth", "pub", "epub"]}, async msg => {
32
+ if (msg.err) {
33
+ done(`error getting ${pub}: ${msg.err}`)
34
+ return
35
+ }
38
36
 
39
- const data = msg.put && msg.put[pub]
40
- if (!data || !data.auth) return next()
37
+ const data = msg.put && msg.put[pub]
38
+ if (!data || !data.auth) return next()
41
39
 
42
- const auth = JSON.parse(data.auth)
43
- const work = await SEA.work(password, auth.salt)
44
- const dec = await SEA.decrypt(auth.enc, work)
45
- if (!dec) return next()
40
+ const auth = JSON.parse(data.auth)
41
+ const work = await SEA.work(password, auth.salt)
42
+ const dec = await SEA.decrypt(auth.enc, work)
43
+ if (!dec) return next()
46
44
 
47
- user.is = {
48
- username: username,
49
- pub: data.pub,
50
- epub: data.epub,
51
- priv: dec.priv,
52
- epriv: dec.epriv,
45
+ user.is = {
46
+ username: username,
47
+ pub: data.pub,
48
+ epub: data.epub,
49
+ priv: dec.priv,
50
+ epriv: dec.epriv,
51
+ }
52
+ if (newPassword !== "") {
53
+ // Encrypt private key using new password and a new salt.
54
+ const salt = utils.text.random(64)
55
+ const work = await SEA.work(newPassword, salt)
56
+ const enc = await SEA.encrypt(dec, work)
57
+ // Only update the auth property with the new password encryption.
58
+ const update = {
59
+ auth: JSON.stringify({enc: enc, salt: salt}),
53
60
  }
54
- if (newPassword !== "") {
55
- // Encrypt private key using new password and a new salt.
56
- const salt = utils.text.random(64)
57
- const work = await SEA.work(newPassword, salt)
58
- const enc = await SEA.encrypt(dec, work)
59
- // Only update the auth property with the new password encryption.
60
- const update = {
61
- auth: JSON.stringify({enc: enc, salt: salt}),
61
+ const timestamp = Date.now()
62
+ const sig = await SEA.signTimestamp(timestamp, user.is)
63
+ const graph = utils.graph(pub, update, sig, data.pub, timestamp)
64
+ wire.put(graph, err => {
65
+ if (err) {
66
+ done(`error putting ${update} on ${pub}: ${err}`)
67
+ } else {
68
+ done(null)
62
69
  }
63
- const timestamp = Date.now()
64
- const sig = await SEA.signTimestamp(timestamp, user.is)
65
- const graph = utils.graph(pub, update, sig, data.pub, timestamp)
66
- wire.put(graph, err => {
67
- if (err) {
68
- done(`error putting ${update} on ${pub}: ${err}`)
69
- } else {
70
- done(null)
71
- }
72
- })
73
- return
74
- }
70
+ })
71
+ return
72
+ }
75
73
 
76
- done(null)
77
- },
78
- )
74
+ done(null)
75
+ })
79
76
  }
80
77
 
81
78
  if (retries > 9) {
@@ -84,24 +81,21 @@ const User = (opt, wire) => {
84
81
  }
85
82
 
86
83
  const soul = "~@" + username
87
- wire.get(
88
- {"#": soul},
89
- async msg => {
90
- if (msg.err) {
91
- done(`error getting ${soul}: ${msg.err}`)
92
- return
93
- }
84
+ wire.get({"#": soul}, async msg => {
85
+ if (msg.err) {
86
+ done(`error getting ${soul}: ${msg.err}`)
87
+ return
88
+ }
94
89
 
95
- const data = msg.put && msg.put[soul]
96
- if (!data) return retry(username)
90
+ const data = msg.put && msg.put[soul]
91
+ if (!data) return retry(username)
97
92
 
98
- delete msg.put[soul]._
99
- // Usernames aren't guaranteed to be unique, so store the list and then
100
- // try each of them until one is successful with the given password.
101
- pubs = Object.keys(data)
102
- next()
103
- },
104
- )
93
+ delete msg.put[soul]._
94
+ // Usernames aren't guaranteed to be unique, so store the list and then
95
+ // try each of them until one is successful with the given password.
96
+ pubs = Object.keys(data)
97
+ next()
98
+ })
105
99
  }
106
100
 
107
101
  const user = {
@@ -129,58 +123,55 @@ const User = (opt, wire) => {
129
123
  creating = true
130
124
 
131
125
  const soul = "~@" + username
132
- wire.get(
133
- {"#": soul},
134
- async msg => {
135
- if (msg.err) {
136
- creating = false
137
- ack(`error getting ${soul}: ${msg.err}`)
138
- return
139
- }
126
+ wire.get({"#": soul}, async msg => {
127
+ if (msg.err) {
128
+ creating = false
129
+ ack(`error getting ${soul}: ${msg.err}`)
130
+ return
131
+ }
140
132
 
141
- if (msg.put && msg.put[soul]) {
142
- creating = false
143
- ack("Username already exists")
144
- return
145
- }
133
+ if (msg.put && msg.put[soul]) {
134
+ creating = false
135
+ ack("Username already exists")
136
+ return
137
+ }
146
138
 
147
- const salt = utils.text.random(64)
148
- const work = await SEA.work(password, salt)
149
- const pair = await SEA.pair()
150
- const priv = {priv: pair.priv, epriv: pair.epriv}
151
- const enc = await SEA.encrypt(priv, work)
152
- const data = {
153
- username: username,
154
- pub: pair.pub,
155
- epub: pair.epub,
156
- auth: JSON.stringify({enc: enc, salt: salt}),
139
+ const salt = utils.text.random(64)
140
+ const work = await SEA.work(password, salt)
141
+ const pair = await SEA.pair()
142
+ const priv = {priv: pair.priv, epriv: pair.epriv}
143
+ const enc = await SEA.encrypt(priv, work)
144
+ const data = {
145
+ username: username,
146
+ pub: pair.pub,
147
+ epub: pair.epub,
148
+ auth: JSON.stringify({enc: enc, salt: salt}),
149
+ }
150
+
151
+ const pub = "~" + pair.pub
152
+ // Sign the timestamp for new account
153
+ const timestamp = Date.now()
154
+ const sig = await SEA.signTimestamp(timestamp, pair)
155
+ const graph = utils.graph(pub, data, sig, pair.pub, timestamp)
156
+ wire.put(graph, err => {
157
+ creating = false
158
+ if (err) {
159
+ ack(`error putting ${data} on ${pub}: ${err}`)
160
+ return
157
161
  }
158
162
 
159
- const pub = "~" + pair.pub
160
- // Sign the timestamp for new account
161
- const timestamp = Date.now()
162
- const sig = await SEA.signTimestamp(timestamp, pair)
163
- const graph = utils.graph(pub, data, sig, pair.pub, timestamp)
164
- wire.put(graph, err => {
165
- creating = false
163
+ const rel = {[pub]: {"#": pub}}
164
+ wire.put(utils.graph(soul, rel), err => {
166
165
  if (err) {
167
- ack(`error putting ${data} on ${pub}: ${err}`)
166
+ ack(`error putting ${rel} on ${soul}: ${err}`)
168
167
  return
169
168
  }
170
169
 
171
- const rel = {[pub]: {"#": pub}}
172
- wire.put(utils.graph(soul, rel), err => {
173
- if (err) {
174
- ack(`error putting ${rel} on ${soul}: ${err}`)
175
- return
176
- }
177
-
178
- // Return null on success.
179
- if (cb) cb(null)
180
- })
170
+ // Return null on success.
171
+ if (cb) cb(null)
181
172
  })
182
- },
183
- )
173
+ })
174
+ })
184
175
  },
185
176
  auth: (username, password, cb) => {
186
177
  const ack = err => {