@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/radisk.js
ADDED
|
@@ -0,0 +1,438 @@
|
|
|
1
|
+
import Radix from "./radix.js"
|
|
2
|
+
import * as utils from "./utils.js"
|
|
3
|
+
|
|
4
|
+
// ASCII character for end of text.
|
|
5
|
+
const etx = String.fromCharCode(3)
|
|
6
|
+
// ASCII character for enquiry.
|
|
7
|
+
const enq = String.fromCharCode(5)
|
|
8
|
+
// ASCII character for unit separator.
|
|
9
|
+
const unit = String.fromCharCode(31)
|
|
10
|
+
|
|
11
|
+
// Radisk provides access to a radix tree that is stored in the provided
|
|
12
|
+
// opt.store interface.
|
|
13
|
+
const Radisk = opt => {
|
|
14
|
+
var u
|
|
15
|
+
var cache = null
|
|
16
|
+
|
|
17
|
+
if (!opt) opt = {}
|
|
18
|
+
if (!opt.log) opt.log = console.log
|
|
19
|
+
if (!opt.batch) opt.batch = 10 * 1000
|
|
20
|
+
if (!opt.write) opt.write = 1 // Wait time before write in milliseconds.
|
|
21
|
+
if (!opt.size) opt.size = 1024 * 1024 // File size on disk, default 1MB.
|
|
22
|
+
if (!opt.store) {
|
|
23
|
+
opt.log(
|
|
24
|
+
"Radisk needs `store` interface with `{get: fn, put: fn, list: fn}`",
|
|
25
|
+
)
|
|
26
|
+
return
|
|
27
|
+
}
|
|
28
|
+
if (!opt.store.get) {
|
|
29
|
+
opt.log("Radisk needs `store.get` interface with `(file, cb)`")
|
|
30
|
+
return
|
|
31
|
+
}
|
|
32
|
+
if (!opt.store.put) {
|
|
33
|
+
opt.log("Radisk needs `store.put` interface with `(file, data, cb)`")
|
|
34
|
+
return
|
|
35
|
+
}
|
|
36
|
+
if (!opt.store.list) {
|
|
37
|
+
opt.log("Radisk needs a streaming `store.list` interface with `(cb)`")
|
|
38
|
+
return
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
// Any and all storage adapters should:
|
|
42
|
+
// 1. Because writing to disk takes time, we should batch data to disk.
|
|
43
|
+
// This improves performance, and reduces potential disk corruption.
|
|
44
|
+
// 2. If a batch exceeds a certain number of writes, we should immediately
|
|
45
|
+
// write to disk when physically possible. This caps total performance,
|
|
46
|
+
// but reduces potential loss.
|
|
47
|
+
const radisk = (key, value, cb) => {
|
|
48
|
+
key = "" + key
|
|
49
|
+
|
|
50
|
+
// If no value is provided then the second parameter is the callback
|
|
51
|
+
// function. Read value from memory or disk and call callback with it.
|
|
52
|
+
if (typeof value === "function") {
|
|
53
|
+
cb = value
|
|
54
|
+
value = radisk.batch(key)
|
|
55
|
+
if (typeof value !== "undefined") {
|
|
56
|
+
return cb(u, value)
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
if (radisk.thrash.at) {
|
|
60
|
+
value = radisk.thrash.at(key)
|
|
61
|
+
if (typeof value !== "undefined") {
|
|
62
|
+
return cb(u, value)
|
|
63
|
+
}
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
return radisk.read(key, cb)
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
// Otherwise store the value provided.
|
|
70
|
+
radisk.batch(key, value)
|
|
71
|
+
if (cb) {
|
|
72
|
+
radisk.batch.acks.push(cb)
|
|
73
|
+
}
|
|
74
|
+
// Don't wait if we have batched too many.
|
|
75
|
+
if (++radisk.batch.ed >= opt.batch) {
|
|
76
|
+
return radisk.thrash()
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
// Otherwise wait for more updates before writing.
|
|
80
|
+
clearTimeout(radisk.batch.timeout)
|
|
81
|
+
radisk.batch.timeout = setTimeout(radisk.thrash, opt.write)
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
radisk.batch = Radix()
|
|
85
|
+
radisk.batch.acks = []
|
|
86
|
+
radisk.batch.ed = 0
|
|
87
|
+
|
|
88
|
+
radisk.thrash = () => {
|
|
89
|
+
if (radisk.thrash.ing) {
|
|
90
|
+
return (radisk.thrash.more = true)
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
clearTimeout(radisk.batch.timeout)
|
|
94
|
+
radisk.thrash.more = false
|
|
95
|
+
radisk.thrash.ing = true
|
|
96
|
+
var batch = (radisk.thrash.at = radisk.batch)
|
|
97
|
+
radisk.batch = null
|
|
98
|
+
radisk.batch = Radix()
|
|
99
|
+
radisk.batch.acks = []
|
|
100
|
+
radisk.batch.ed = 0
|
|
101
|
+
let i = 0
|
|
102
|
+
radisk.save(batch, err => {
|
|
103
|
+
// This is to ignore multiple callbacks from radisk.save calling
|
|
104
|
+
// radisk.write? It looks like multiple callbacks will be made if a
|
|
105
|
+
// file needs to be split.
|
|
106
|
+
if (++i > 1) return
|
|
107
|
+
|
|
108
|
+
if (err) opt.log(err)
|
|
109
|
+
batch.acks.forEach(cb => cb(err))
|
|
110
|
+
radisk.thrash.at = null
|
|
111
|
+
radisk.thrash.ing = false
|
|
112
|
+
if (radisk.thrash.more) radisk.thrash()
|
|
113
|
+
})
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
// 1. Find the first radix item in memory
|
|
117
|
+
// 2. Use that as the starting index in the directory of files
|
|
118
|
+
// 3. Find the first file that is lexically larger than it
|
|
119
|
+
// 4. Read the previous file into memory
|
|
120
|
+
// 5. Scan through in memory radix for all values lexically less than limit
|
|
121
|
+
// 6. Merge and write all of those to the in-memory file and back to disk
|
|
122
|
+
// 7. If file is to large then split. More details needed here
|
|
123
|
+
radisk.save = (rad, cb) => {
|
|
124
|
+
const save = {
|
|
125
|
+
find: (tree, key) => {
|
|
126
|
+
// This is false for any key until save.start is set to an initial key.
|
|
127
|
+
if (key < save.start) return
|
|
128
|
+
|
|
129
|
+
save.start = key
|
|
130
|
+
opt.store.list(save.lex)
|
|
131
|
+
return true
|
|
132
|
+
},
|
|
133
|
+
lex: file => {
|
|
134
|
+
if (!file || file > save.start) {
|
|
135
|
+
save.end = file
|
|
136
|
+
// ! is used as the first file name as it's the first printable
|
|
137
|
+
// character, so always matches as lexically less than any node.
|
|
138
|
+
save.mix(save.file || "!", save.start, save.end)
|
|
139
|
+
return true
|
|
140
|
+
}
|
|
141
|
+
|
|
142
|
+
save.file = file
|
|
143
|
+
},
|
|
144
|
+
mix: (file, start, end) => {
|
|
145
|
+
save.start = save.end = save.file = u
|
|
146
|
+
radisk.parse(file, (err, disk) => {
|
|
147
|
+
if (err) return cb(err)
|
|
148
|
+
|
|
149
|
+
Radix.map(rad, (value, key) => {
|
|
150
|
+
if (key < start) return
|
|
151
|
+
|
|
152
|
+
if (end && end < key) {
|
|
153
|
+
save.start = key
|
|
154
|
+
return save.start
|
|
155
|
+
}
|
|
156
|
+
|
|
157
|
+
disk(key, value)
|
|
158
|
+
})
|
|
159
|
+
radisk.write(file, disk, save.next)
|
|
160
|
+
})
|
|
161
|
+
},
|
|
162
|
+
next: err => {
|
|
163
|
+
if (err) return cb(err)
|
|
164
|
+
|
|
165
|
+
if (save.start) return Radix.map(rad, save.find)
|
|
166
|
+
|
|
167
|
+
cb(err)
|
|
168
|
+
},
|
|
169
|
+
}
|
|
170
|
+
Radix.map(rad, save.find)
|
|
171
|
+
}
|
|
172
|
+
|
|
173
|
+
radisk.write = (file, rad, cb) => {
|
|
174
|
+
// Invalidate cache on write.
|
|
175
|
+
cache = null
|
|
176
|
+
const write = {
|
|
177
|
+
text: "",
|
|
178
|
+
count: 0,
|
|
179
|
+
file: file,
|
|
180
|
+
each: (value, key, k, pre) => {
|
|
181
|
+
write.count++
|
|
182
|
+
var enc =
|
|
183
|
+
Radisk.encode(pre.length) +
|
|
184
|
+
"#" +
|
|
185
|
+
Radisk.encode(k) +
|
|
186
|
+
(typeof value === "undefined" ? "" : "=" + Radisk.encode(value)) +
|
|
187
|
+
"\n"
|
|
188
|
+
// Cannot split the file if only have one entry to write.
|
|
189
|
+
if (write.count > 1 && write.text.length + enc.length > opt.size) {
|
|
190
|
+
write.text = ""
|
|
191
|
+
// Otherwise split the entries in half.
|
|
192
|
+
write.limit = Math.ceil(write.count / 2)
|
|
193
|
+
write.count = 0
|
|
194
|
+
write.sub = Radix()
|
|
195
|
+
Radix.map(rad, write.slice)
|
|
196
|
+
return true
|
|
197
|
+
}
|
|
198
|
+
|
|
199
|
+
write.text += enc
|
|
200
|
+
},
|
|
201
|
+
put: () => {
|
|
202
|
+
opt.store.put(file, write.text, cb)
|
|
203
|
+
},
|
|
204
|
+
slice: (value, key) => {
|
|
205
|
+
if (key < write.file) return
|
|
206
|
+
|
|
207
|
+
if (++write.count > write.limit) {
|
|
208
|
+
var name = write.file
|
|
209
|
+
// Use only the soul of the key as the filename so that all
|
|
210
|
+
// properties of a soul are written to the same file.
|
|
211
|
+
let end = key.indexOf(enq)
|
|
212
|
+
if (end === -1) {
|
|
213
|
+
write.file = key
|
|
214
|
+
} else {
|
|
215
|
+
write.file = key.substring(0, end)
|
|
216
|
+
}
|
|
217
|
+
// write.limit can be reached after already writing properties of
|
|
218
|
+
// the current node, so remove it from write.sub before writing to
|
|
219
|
+
// disk so that it's not duplicated across files.
|
|
220
|
+
write.sub(write.file, null)
|
|
221
|
+
write.count = 0
|
|
222
|
+
radisk.write(name, write.sub, write.next)
|
|
223
|
+
return true
|
|
224
|
+
}
|
|
225
|
+
|
|
226
|
+
write.sub(key, value)
|
|
227
|
+
},
|
|
228
|
+
next: err => {
|
|
229
|
+
if (err) return cb(err)
|
|
230
|
+
|
|
231
|
+
write.sub = Radix()
|
|
232
|
+
if (!Radix.map(rad, write.slice)) {
|
|
233
|
+
radisk.write(write.file, write.sub, cb)
|
|
234
|
+
}
|
|
235
|
+
},
|
|
236
|
+
}
|
|
237
|
+
// If Radix.map doesn't return true when called with write.each as a
|
|
238
|
+
// callback then didn't need to split the data. The accumulated write.text
|
|
239
|
+
// can then be stored with write.put().
|
|
240
|
+
if (!Radix.map(rad, write.each, true)) write.put()
|
|
241
|
+
}
|
|
242
|
+
|
|
243
|
+
radisk.read = (key, cb) => {
|
|
244
|
+
if (cache) {
|
|
245
|
+
let value = cache(key)
|
|
246
|
+
if (typeof value !== "undefined") return cb(u, value)
|
|
247
|
+
}
|
|
248
|
+
// Only the soul of the key is compared to filenames (see radisk.write).
|
|
249
|
+
let soul = key
|
|
250
|
+
let end = key.indexOf(enq)
|
|
251
|
+
if (end !== -1) {
|
|
252
|
+
soul = key.substring(0, end)
|
|
253
|
+
}
|
|
254
|
+
|
|
255
|
+
const read = {
|
|
256
|
+
lex: file => {
|
|
257
|
+
// store.list should call lex without a file last, which means all file
|
|
258
|
+
// names were compared to soul, so the current read.file is ok to use.
|
|
259
|
+
if (!file) {
|
|
260
|
+
if (!read.file) {
|
|
261
|
+
cb("no file found", u)
|
|
262
|
+
return
|
|
263
|
+
}
|
|
264
|
+
|
|
265
|
+
radisk.parse(read.file, read.it)
|
|
266
|
+
return
|
|
267
|
+
}
|
|
268
|
+
|
|
269
|
+
// Want the filename closest to soul.
|
|
270
|
+
if (file > soul || file < read.file) return
|
|
271
|
+
|
|
272
|
+
read.file = file
|
|
273
|
+
},
|
|
274
|
+
it: (err, disk) => {
|
|
275
|
+
if (err) opt.log(err)
|
|
276
|
+
if (disk) {
|
|
277
|
+
cache = disk
|
|
278
|
+
read.value = disk(key)
|
|
279
|
+
}
|
|
280
|
+
cb(err, read.value)
|
|
281
|
+
},
|
|
282
|
+
}
|
|
283
|
+
opt.store.list(read.lex)
|
|
284
|
+
}
|
|
285
|
+
|
|
286
|
+
// Let us start by assuming we are the only process that is
|
|
287
|
+
// changing the directory or bucket. Not because we do not want
|
|
288
|
+
// to be multi-process/machine, but because we want to experiment
|
|
289
|
+
// with how much performance and scale we can get out of only one.
|
|
290
|
+
// Then we can work on the harder problem of being multi-process.
|
|
291
|
+
radisk.parse = (file, cb) => {
|
|
292
|
+
const parse = {
|
|
293
|
+
disk: Radix(),
|
|
294
|
+
read: (err, data) => {
|
|
295
|
+
if (err) return cb(err)
|
|
296
|
+
|
|
297
|
+
if (!data) return cb(u, parse.disk)
|
|
298
|
+
|
|
299
|
+
let pre = []
|
|
300
|
+
// Work though data by splitting into 3 values. The first value says
|
|
301
|
+
// if the second value is one of: the radix level for a key, the key
|
|
302
|
+
// iteself, or a value. The third is the rest of the data to work with.
|
|
303
|
+
let tmp = parse.split(data)
|
|
304
|
+
while (tmp) {
|
|
305
|
+
let key
|
|
306
|
+
let value
|
|
307
|
+
let i = tmp[1]
|
|
308
|
+
tmp = parse.split(tmp[2]) || ""
|
|
309
|
+
if (tmp[0] === "#") {
|
|
310
|
+
key = tmp[1]
|
|
311
|
+
pre = pre.slice(0, i)
|
|
312
|
+
if (i <= pre.length) pre.push(key)
|
|
313
|
+
}
|
|
314
|
+
tmp = parse.split(tmp[2]) || ""
|
|
315
|
+
if (tmp[0] === "\n") continue
|
|
316
|
+
|
|
317
|
+
if (tmp[0] === "=") value = tmp[1]
|
|
318
|
+
if (typeof key !== "undefined" && typeof value !== "undefined") {
|
|
319
|
+
parse.disk(pre.join(""), value)
|
|
320
|
+
}
|
|
321
|
+
tmp = parse.split(tmp[2])
|
|
322
|
+
}
|
|
323
|
+
cb(u, parse.disk)
|
|
324
|
+
},
|
|
325
|
+
split: data => {
|
|
326
|
+
if (!data) return
|
|
327
|
+
|
|
328
|
+
let i = -1
|
|
329
|
+
let a = ""
|
|
330
|
+
let c = null
|
|
331
|
+
while ((c = data[++i])) {
|
|
332
|
+
if (c === unit) break
|
|
333
|
+
|
|
334
|
+
a += c
|
|
335
|
+
}
|
|
336
|
+
let o = {}
|
|
337
|
+
if (c) {
|
|
338
|
+
return [a, Radisk.decode(data.slice(i), o), data.slice(i + o.i)]
|
|
339
|
+
}
|
|
340
|
+
},
|
|
341
|
+
}
|
|
342
|
+
opt.store.get(file, parse.read)
|
|
343
|
+
}
|
|
344
|
+
|
|
345
|
+
return radisk
|
|
346
|
+
}
|
|
347
|
+
|
|
348
|
+
Radisk.encode = data => {
|
|
349
|
+
// A key should be passed in as a string to encode, a value can optionally be
|
|
350
|
+
// an array of 2 items to include the value's state, as is done by store.js.
|
|
351
|
+
let state = ""
|
|
352
|
+
if (data instanceof Array && data.length === 2) {
|
|
353
|
+
state = etx + data[1]
|
|
354
|
+
data = data[0]
|
|
355
|
+
}
|
|
356
|
+
|
|
357
|
+
if (typeof data === "string") {
|
|
358
|
+
let i = 0
|
|
359
|
+
let current = null
|
|
360
|
+
let text = unit
|
|
361
|
+
while ((current = data[i++])) {
|
|
362
|
+
if (current === unit) text += unit
|
|
363
|
+
}
|
|
364
|
+
return text + '"' + data + state + unit
|
|
365
|
+
}
|
|
366
|
+
|
|
367
|
+
const rel = utils.rel.is(data)
|
|
368
|
+
if (rel) return unit + "#" + rel + state + unit
|
|
369
|
+
|
|
370
|
+
if (utils.num.is(data)) return unit + "+" + (data || 0) + state + unit
|
|
371
|
+
|
|
372
|
+
if (data === true) return unit + "+" + state + unit
|
|
373
|
+
|
|
374
|
+
if (data === false) return unit + "-" + state + unit
|
|
375
|
+
|
|
376
|
+
if (data === null) return unit + " " + state + unit
|
|
377
|
+
}
|
|
378
|
+
|
|
379
|
+
Radisk.decode = (data, obj) => {
|
|
380
|
+
var text = ""
|
|
381
|
+
var i = -1
|
|
382
|
+
var n = 0
|
|
383
|
+
var current = null
|
|
384
|
+
var previous = null
|
|
385
|
+
if (data[0] !== unit) return
|
|
386
|
+
|
|
387
|
+
// Find a control character previous to the text we want, skipping
|
|
388
|
+
// consecutive unit separator characters at the beginning of the data.
|
|
389
|
+
while ((current = data[++i])) {
|
|
390
|
+
if (previous) {
|
|
391
|
+
if (current === unit) {
|
|
392
|
+
if (--n <= 0) break
|
|
393
|
+
}
|
|
394
|
+
text += current
|
|
395
|
+
} else if (current === unit) {
|
|
396
|
+
n++
|
|
397
|
+
} else {
|
|
398
|
+
previous = current || true
|
|
399
|
+
}
|
|
400
|
+
}
|
|
401
|
+
|
|
402
|
+
if (obj) obj.i = i + 1
|
|
403
|
+
|
|
404
|
+
let [value, state] = text.split(etx)
|
|
405
|
+
if (!state) {
|
|
406
|
+
if (previous === '"') return text
|
|
407
|
+
|
|
408
|
+
if (previous === "#") return utils.rel.ify(text)
|
|
409
|
+
|
|
410
|
+
if (previous === "+") {
|
|
411
|
+
if (text.length === 0) return true
|
|
412
|
+
|
|
413
|
+
return parseFloat(text)
|
|
414
|
+
}
|
|
415
|
+
|
|
416
|
+
if (previous === "-") return false
|
|
417
|
+
|
|
418
|
+
if (previous === " ") return null
|
|
419
|
+
} else {
|
|
420
|
+
state = parseFloat(state)
|
|
421
|
+
// If state was found then return an array.
|
|
422
|
+
if (previous === '"') return [value, state]
|
|
423
|
+
|
|
424
|
+
if (previous === "#") return [utils.rel.ify(value), state]
|
|
425
|
+
|
|
426
|
+
if (previous === "+") {
|
|
427
|
+
if (value.length === 0) return [true, state]
|
|
428
|
+
|
|
429
|
+
return [parseFloat(value), state]
|
|
430
|
+
}
|
|
431
|
+
|
|
432
|
+
if (previous === "-") return [false, state]
|
|
433
|
+
|
|
434
|
+
if (previous === " ") return [null, state]
|
|
435
|
+
}
|
|
436
|
+
}
|
|
437
|
+
|
|
438
|
+
export default Radisk
|
package/src/radix.js
ADDED
|
@@ -0,0 +1,106 @@
|
|
|
1
|
+
import * as utils from "./utils.js"
|
|
2
|
+
|
|
3
|
+
// ASCII character for group separator.
|
|
4
|
+
const group = String.fromCharCode(29)
|
|
5
|
+
// ASCII character for record separator.
|
|
6
|
+
const record = String.fromCharCode(30)
|
|
7
|
+
|
|
8
|
+
const Radix = () => {
|
|
9
|
+
const radix = (keys, value, tree) => {
|
|
10
|
+
if (!tree) {
|
|
11
|
+
if (!radix[group]) radix[group] = {}
|
|
12
|
+
tree = radix[group]
|
|
13
|
+
}
|
|
14
|
+
if (!keys) return tree
|
|
15
|
+
|
|
16
|
+
let i = 0
|
|
17
|
+
let tmp = {}
|
|
18
|
+
let key = keys[i]
|
|
19
|
+
const max = keys.length - 1
|
|
20
|
+
const noValue = typeof value === "undefined"
|
|
21
|
+
// Find a matching value using the shortest string from keys.
|
|
22
|
+
let found = tree[key]
|
|
23
|
+
while (!found && i < max) {
|
|
24
|
+
key += keys[++i]
|
|
25
|
+
found = tree[key]
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
if (!found) {
|
|
29
|
+
// If not found from the provided keys try matching with an existing key.
|
|
30
|
+
const result = utils.obj.map(tree, (hasValue, hasKey) => {
|
|
31
|
+
let j = 0
|
|
32
|
+
let matchingKey = ""
|
|
33
|
+
while (hasKey[j] === keys[j]) {
|
|
34
|
+
matchingKey += hasKey[j++]
|
|
35
|
+
}
|
|
36
|
+
if (matchingKey) {
|
|
37
|
+
if (noValue) {
|
|
38
|
+
// matchingKey has to be as long as the original keys when reading.
|
|
39
|
+
if (j <= max) return
|
|
40
|
+
|
|
41
|
+
tmp[hasKey.slice(j)] = hasValue
|
|
42
|
+
return hasValue
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
let replace = {
|
|
46
|
+
[hasKey.slice(j)]: hasValue,
|
|
47
|
+
[keys.slice(j)]: {[record]: value},
|
|
48
|
+
}
|
|
49
|
+
tree[matchingKey] = {[group]: replace}
|
|
50
|
+
delete tree[hasKey]
|
|
51
|
+
return true
|
|
52
|
+
}
|
|
53
|
+
})
|
|
54
|
+
if (!result) {
|
|
55
|
+
if (noValue) return
|
|
56
|
+
|
|
57
|
+
if (!tree[key]) tree[key] = {}
|
|
58
|
+
tree[key][record] = value
|
|
59
|
+
} else if (noValue) {
|
|
60
|
+
return tmp
|
|
61
|
+
}
|
|
62
|
+
} else if (i === max) {
|
|
63
|
+
// If no value use the key provided to return a whole group or record.
|
|
64
|
+
if (noValue) {
|
|
65
|
+
// If an individual record isn't found then return the whole group.
|
|
66
|
+
return typeof found[record] === "undefined"
|
|
67
|
+
? found[group]
|
|
68
|
+
: found[record]
|
|
69
|
+
}
|
|
70
|
+
// Otherwise create a new record at the provided key for value.
|
|
71
|
+
found[record] = value
|
|
72
|
+
} else {
|
|
73
|
+
// Found at a shorter key, try again.
|
|
74
|
+
if (!found[group] && !noValue) found[group] = {}
|
|
75
|
+
return radix(keys.slice(++i), value, found[group])
|
|
76
|
+
}
|
|
77
|
+
}
|
|
78
|
+
return radix
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
Radix.map = function map(radix, cb, opt, pre) {
|
|
82
|
+
if (!pre) pre = []
|
|
83
|
+
var tree = radix[group] || radix
|
|
84
|
+
var keys = Object.keys(tree).sort()
|
|
85
|
+
var u
|
|
86
|
+
|
|
87
|
+
for (let i = 0; i < keys.length; i++) {
|
|
88
|
+
let key = keys[i]
|
|
89
|
+
let found = tree[key]
|
|
90
|
+
let tmp = found[record]
|
|
91
|
+
if (typeof tmp !== "undefined") {
|
|
92
|
+
tmp = cb(tmp, pre.join("") + key, key, pre)
|
|
93
|
+
if (typeof tmp !== "undefined") return tmp
|
|
94
|
+
} else if (opt) {
|
|
95
|
+
cb(u, pre.join(""), key, pre)
|
|
96
|
+
}
|
|
97
|
+
if (found[group]) {
|
|
98
|
+
pre.push(key)
|
|
99
|
+
tmp = map(found[group], cb, opt, pre)
|
|
100
|
+
if (typeof tmp !== "undefined") return tmp
|
|
101
|
+
pre.pop()
|
|
102
|
+
}
|
|
103
|
+
}
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
export default Radix
|
package/src/sea-utils.js
ADDED
|
@@ -0,0 +1,65 @@
|
|
|
1
|
+
import SafeBuffer from "./buffer.js"
|
|
2
|
+
|
|
3
|
+
const isNode = typeof document === "undefined"
|
|
4
|
+
const crypto = isNode
|
|
5
|
+
? (await import("node:crypto")).webcrypto
|
|
6
|
+
: globalThis.crypto
|
|
7
|
+
export const subtle = crypto.subtle
|
|
8
|
+
|
|
9
|
+
export const stringify = data => {
|
|
10
|
+
return typeof data === "string" ? data : JSON.stringify(data)
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
export const parse = text => {
|
|
14
|
+
try {
|
|
15
|
+
return JSON.parse(text)
|
|
16
|
+
} catch {
|
|
17
|
+
return text
|
|
18
|
+
}
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
export const random = length => {
|
|
22
|
+
const array = new Uint8Array(SafeBuffer.alloc(length))
|
|
23
|
+
return SafeBuffer.from(crypto.getRandomValues(array))
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
export const jwk = (pub, priv) => {
|
|
27
|
+
const [x, y] = pub.split(".")
|
|
28
|
+
const ops = priv ? ["sign"] : ["verify"]
|
|
29
|
+
return {
|
|
30
|
+
kty: "EC",
|
|
31
|
+
crv: "P-256",
|
|
32
|
+
x: x,
|
|
33
|
+
y: y,
|
|
34
|
+
d: priv,
|
|
35
|
+
ext: true,
|
|
36
|
+
key_ops: ops,
|
|
37
|
+
}
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
export const sha256 = async data => {
|
|
41
|
+
const hash = await subtle.digest(
|
|
42
|
+
{name: "SHA-256"},
|
|
43
|
+
new TextEncoder().encode(stringify(data)),
|
|
44
|
+
)
|
|
45
|
+
return SafeBuffer.from(hash)
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
export const aeskey = async (key, salt) => {
|
|
49
|
+
const combined = key + salt.toString("utf8")
|
|
50
|
+
const hash = SafeBuffer.from(await sha256(combined), "binary")
|
|
51
|
+
const jwk = keyToJwk(hash)
|
|
52
|
+
return await subtle.importKey("jwk", jwk, {name: "AES-GCM"}, false, [
|
|
53
|
+
"encrypt",
|
|
54
|
+
"decrypt",
|
|
55
|
+
])
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
const keyToJwk = key => {
|
|
59
|
+
const k = key
|
|
60
|
+
.toString("base64")
|
|
61
|
+
.replace(/\+/g, "-")
|
|
62
|
+
.replace(/\//g, "_")
|
|
63
|
+
.replace(/\=/g, "")
|
|
64
|
+
return {kty: "oct", k: k, ext: false, alg: "A256GCM"}
|
|
65
|
+
}
|