@mblaney/holster 2.0.2 → 2.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/README.md +37 -6
- package/build/holster.js +3 -3
- package/build/holster.js.map +3 -3
- package/dist/holster.js +3 -3
- package/dist/holster.js.map +3 -3
- package/package.json +1 -1
- package/src/ham.js +10 -3
- package/src/holster.js +36 -25
- package/src/radisk.js +26 -12
- package/src/user.js +92 -101
- package/src/wire.js +298 -104
- package/ts/src/array.ts +10 -13
- package/ts/src/buffer.ts +40 -20
- package/ts/src/get.ts +5 -6
- package/ts/src/ham.ts +47 -28
- package/ts/src/holster.ts +277 -132
- package/ts/src/index.ts +14 -15
- package/ts/src/radisk.ts +122 -37
- package/ts/src/radix.ts +50 -34
- package/ts/src/schemas.ts +44 -24
- package/ts/src/sea-utils.ts +7 -11
- package/ts/src/sea.ts +51 -54
- package/ts/src/store.ts +48 -42
- package/ts/src/user.ts +136 -113
- package/ts/src/utils.ts +36 -11
- package/ts/src/wire.ts +453 -232
package/package.json
CHANGED
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 (
|
|
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 >
|
|
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 -
|
|
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(
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
const
|
|
72
|
-
const
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
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
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
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
|
-
|
|
84
|
-
|
|
85
|
-
|
|
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 (
|
|
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(
|
|
670
|
-
|
|
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
|
-
|
|
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: "",
|
|
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,
|
|
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) {
|
|
362
|
-
|
|
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) {
|
|
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 (
|
|
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) {
|
|
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
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
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
|
-
|
|
40
|
-
|
|
37
|
+
const data = msg.put && msg.put[pub]
|
|
38
|
+
if (!data || !data.auth) return next()
|
|
41
39
|
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
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
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
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
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
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
|
-
|
|
64
|
-
|
|
65
|
-
|
|
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
|
-
|
|
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
|
-
{
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
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
|
-
|
|
96
|
-
|
|
90
|
+
const data = msg.put && msg.put[soul]
|
|
91
|
+
if (!data) return retry(username)
|
|
97
92
|
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
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
|
-
{
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
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
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
133
|
+
if (msg.put && msg.put[soul]) {
|
|
134
|
+
creating = false
|
|
135
|
+
ack("Username already exists")
|
|
136
|
+
return
|
|
137
|
+
}
|
|
146
138
|
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
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
|
|
160
|
-
|
|
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 ${
|
|
166
|
+
ack(`error putting ${rel} on ${soul}: ${err}`)
|
|
168
167
|
return
|
|
169
168
|
}
|
|
170
169
|
|
|
171
|
-
|
|
172
|
-
|
|
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 => {
|