@mblaney/holster 1.0.35 → 2.0.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/build/holster.js +3 -3
- package/build/holster.js.map +3 -3
- package/dist/holster.js +4 -0
- package/dist/holster.js.map +7 -0
- package/package.json +22 -5
- package/src/dup.js +4 -5
- package/src/get.js +10 -19
- package/src/ham.js +22 -49
- package/src/holster.js +172 -58
- package/src/index.js +1 -1
- package/src/radisk.js +89 -86
- package/src/sea-utils.js +1 -1
- package/src/sea.js +0 -52
- package/src/store.js +8 -8
- package/src/user.js +0 -4
- package/src/wire.js +209 -79
- package/ts/src/array.ts +78 -0
- package/ts/src/buffer.ts +441 -0
- package/ts/src/dup.ts +38 -0
- package/ts/src/get.ts +83 -0
- package/ts/src/ham.ts +258 -0
- package/ts/src/holster.ts +968 -0
- package/ts/src/index.ts +35 -0
- package/ts/src/radisk.ts +591 -0
- package/ts/src/radix.ts +157 -0
- package/ts/src/schemas.ts +515 -0
- package/ts/src/sea-utils.ts +103 -0
- package/ts/src/sea.ts +388 -0
- package/ts/src/store.ts +321 -0
- package/ts/src/user.ts +377 -0
- package/ts/src/utils.ts +232 -0
- package/ts/src/wire.ts +961 -0
- package/.dockerignore +0 -9
- package/.github/workflows/node.js.yml +0 -31
- package/.github/workflows/npm-publish.yml +0 -33
- package/Dockerfile +0 -20
- package/examples/index.html +0 -33
- package/examples/index.js +0 -49
- package/examples/user.html +0 -31
- package/examples/user.js +0 -93
- package/examples/wire.html +0 -27
- package/examples/wire.js +0 -55
- package/test/holster.get.test.js +0 -94
- package/test/holster.lex.test.js +0 -196
- package/test/holster.off.test.js +0 -53
- package/test/holster.on.test.js +0 -264
- package/test/holster.put.test.js +0 -358
- package/test/holster.secure.test.js +0 -202
- package/test/holster.user.get.test.js +0 -114
- package/test/holster.user.lex.test.js +0 -149
- package/test/holster.user.off.test.js +0 -73
- package/test/holster.user.on.test.js +0 -300
- package/test/holster.user.put.test.js +0 -463
- package/test/kitty/! +0 -9
- package/test/names.js +0 -225
- package/test/radisk.cache.test.js +0 -213
- package/test/radisk.test.js +0 -331
- package/test/radix.test.js +0 -117
- package/test/sea.test.js +0 -284
- package/test/split.test.js +0 -317
- package/test/store.test.js +0 -316
- package/test/system/basic.test.js +0 -29
- package/test/system/chained-on.test.js +0 -41
- package/test/system/concurrent-listeners.test.js +0 -125
- package/test/system/immediate-read.test.js +0 -30
- package/test/system/listener-before-data.test.js +0 -51
- package/test/system/promise-wrapper.test.js +0 -47
- package/test/system/user-chained-on.test.js +0 -60
- package/test/system/user-concurrent-listeners.test.js +0 -142
- package/test/system/user-listener-before-data.test.js +0 -66
- package/test/user.test.js +0 -173
- package/test/wire.test.js +0 -158
package/src/radisk.js
CHANGED
|
@@ -34,7 +34,6 @@ 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 (!opt.readTimeout) opt.readTimeout = 1000 // Read timeout in milliseconds
|
|
38
37
|
if (typeof opt.cache === "undefined") opt.cache = true
|
|
39
38
|
|
|
40
39
|
if (!opt.store) {
|
|
@@ -124,37 +123,29 @@ const Radisk = opt => {
|
|
|
124
123
|
}
|
|
125
124
|
}
|
|
126
125
|
|
|
127
|
-
// Check if there's already a pending read for this key
|
|
128
126
|
if (pendingReads.has(key)) {
|
|
129
|
-
|
|
130
|
-
pendingReads.get(key).callbacks.push(cb)
|
|
127
|
+
pendingReads.get(key).push(cb)
|
|
131
128
|
return
|
|
132
129
|
}
|
|
133
130
|
|
|
134
|
-
|
|
135
|
-
const pending = {callbacks: [cb], fired: false, timeoutId: null}
|
|
136
|
-
pendingReads.set(key, pending)
|
|
137
|
-
|
|
138
|
-
// Set a timeout to fire callbacks if read takes too long
|
|
139
|
-
pending.timeoutId = setTimeout(() => {
|
|
140
|
-
if (!pending.fired) {
|
|
141
|
-
pending.fired = true
|
|
142
|
-
pendingReads.delete(key)
|
|
143
|
-
const err = new Error("radisk read timeout")
|
|
144
|
-
pending.callbacks.forEach(callback => callback(err, undefined))
|
|
145
|
-
}
|
|
146
|
-
}, opt.readTimeout)
|
|
147
|
-
|
|
131
|
+
pendingReads.set(key, [cb])
|
|
148
132
|
const readStart = Date.now()
|
|
133
|
+
// If radisk.read genuinely hangs (fd leak, stuck I/O), recover
|
|
134
|
+
// callbacks as a cache miss so callers can fall through to the wire path.
|
|
135
|
+
// 30s is long enough to distinguish a hang from a slow-but-healthy read.
|
|
136
|
+
const timeout = setTimeout(() => {
|
|
137
|
+
const callbacks = pendingReads.get(key)
|
|
138
|
+
if (!callbacks) return
|
|
139
|
+
opt.log(`radisk read hang detected for key: ${key}`)
|
|
140
|
+
pendingReads.delete(key)
|
|
141
|
+
callbacks.forEach(callback => callback(null, null))
|
|
142
|
+
}, 30000)
|
|
149
143
|
return radisk.read(key, (err, result) => {
|
|
150
|
-
clearTimeout(
|
|
144
|
+
clearTimeout(timeout)
|
|
151
145
|
perfLog("read", readStart, key)
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
pendingReads.delete(key)
|
|
156
|
-
pending.callbacks.forEach(callback => callback(err, result))
|
|
157
|
-
}
|
|
146
|
+
const callbacks = pendingReads.get(key) || []
|
|
147
|
+
pendingReads.delete(key)
|
|
148
|
+
callbacks.forEach(callback => callback(err, result))
|
|
158
149
|
})
|
|
159
150
|
}
|
|
160
151
|
|
|
@@ -288,6 +279,8 @@ const Radisk = opt => {
|
|
|
288
279
|
limit: "",
|
|
289
280
|
done: false,
|
|
290
281
|
count: 0,
|
|
282
|
+
halfKey: "", // soul ID at the ~opt.size/2 mark for a balanced split
|
|
283
|
+
halfOffset: 0, // write.text.length before that entry
|
|
291
284
|
each: (value, key, k, pre) => {
|
|
292
285
|
// each is called for all keys, but stop adding to write.text when
|
|
293
286
|
// write.slice is called, then the current contents of write.text
|
|
@@ -298,17 +291,46 @@ const Radisk = opt => {
|
|
|
298
291
|
value = typeof value === "undefined" ? "" : "=" + Radisk.encode(value)
|
|
299
292
|
const enc =
|
|
300
293
|
Radisk.encode(pre.length) + "#" + Radisk.encode(k) + value + "\n"
|
|
301
|
-
// Cannot split the file if only have one entry to write.
|
|
302
|
-
//
|
|
294
|
+
// Cannot split the file if only have one entry to write. Allow splitting
|
|
295
|
+
// at depth > 0 only when k itself contains enq, meaning k is a full
|
|
296
|
+
// "soulId\x05property" key, not merely a property suffix of a shared
|
|
297
|
+
// soul prefix. Use the full key (pre + k) to extract the soul boundary.
|
|
298
|
+
const validBoundary =
|
|
299
|
+
pre.length === 0 || (pre.length === 1 && k.includes(enq))
|
|
300
|
+
|
|
301
|
+
// Record the first valid split boundary past the halfway mark so a
|
|
302
|
+
// full overflow can split here instead of at the tail, keeping both
|
|
303
|
+
// halves near opt.size/2 rather than leaving the file always full.
|
|
304
|
+
if (
|
|
305
|
+
!write.halfKey &&
|
|
306
|
+
write.count > 1 &&
|
|
307
|
+
write.text.length > opt.size / 2 &&
|
|
308
|
+
validBoundary
|
|
309
|
+
) {
|
|
310
|
+
const fullKey = pre.join("") + k
|
|
311
|
+
const end = fullKey.indexOf(enq)
|
|
312
|
+
write.halfKey =
|
|
313
|
+
end === -1 ? fullKey : fullKey.substring(0, end)
|
|
314
|
+
write.halfOffset = write.text.length
|
|
315
|
+
}
|
|
316
|
+
|
|
303
317
|
if (
|
|
304
318
|
write.count > 1 &&
|
|
305
|
-
|
|
306
|
-
|
|
319
|
+
write.text.length + enc.length > opt.size &&
|
|
320
|
+
validBoundary
|
|
307
321
|
) {
|
|
308
|
-
const
|
|
309
|
-
|
|
310
|
-
|
|
322
|
+
const fullKey = pre.join("") + k
|
|
323
|
+
const end = fullKey.indexOf(enq)
|
|
324
|
+
write.limit =
|
|
325
|
+
write.halfKey ||
|
|
326
|
+
(end === -1 ? fullKey : fullKey.substring(0, end))
|
|
327
|
+
// Cannot split if the key is the same as the current file name.
|
|
311
328
|
if (write.limit !== file) {
|
|
329
|
+
// Truncate to the balanced split point — text after halfOffset is
|
|
330
|
+
// handled by the write.slice call below, which reads from rad.
|
|
331
|
+
if (write.halfKey) {
|
|
332
|
+
write.text = write.text.substring(0, write.halfOffset)
|
|
333
|
+
}
|
|
312
334
|
write.done = true
|
|
313
335
|
write.sub = Radix()
|
|
314
336
|
Radix.map(rad, write.slice)
|
|
@@ -330,6 +352,13 @@ const Radisk = opt => {
|
|
|
330
352
|
// finished.
|
|
331
353
|
const writeStart = Date.now()
|
|
332
354
|
opt.store.put(file, write.text, err => {
|
|
355
|
+
// If a split occurred the cached radix tree still contains entries that
|
|
356
|
+
// were moved to the sub-file. Invalidate so the next save.mix re-reads
|
|
357
|
+
// from disk rather than re-writing those entries back to this file.
|
|
358
|
+
if (write.done) {
|
|
359
|
+
cache.delete(file)
|
|
360
|
+
fileListCache = null
|
|
361
|
+
}
|
|
333
362
|
perfLog("file-write", writeStart, file, write.text.length)
|
|
334
363
|
cb(err)
|
|
335
364
|
})
|
|
@@ -340,71 +369,45 @@ const Radisk = opt => {
|
|
|
340
369
|
const end = key.indexOf(enq)
|
|
341
370
|
const soul = end === -1 ? key : key.substring(0, end)
|
|
342
371
|
|
|
343
|
-
|
|
344
|
-
|
|
345
|
-
|
|
346
|
-
|
|
347
|
-
|
|
348
|
-
|
|
349
|
-
|
|
350
|
-
|
|
351
|
-
|
|
352
|
-
|
|
353
|
-
// Check multi-file cache first
|
|
354
|
-
if (opt.cache && cache.has(read.file)) {
|
|
355
|
-
const cachedRadix = cache.get(read.file)
|
|
356
|
-
read.value = cachedRadix(key)
|
|
357
|
-
// Return cached result (defined or undefined) since in-memory
|
|
358
|
-
// radix tree is authoritative
|
|
359
|
-
return cb(u, read.value)
|
|
360
|
-
}
|
|
361
|
-
|
|
362
|
-
radisk.parse(read.file, read.it)
|
|
363
|
-
return
|
|
364
|
-
}
|
|
365
|
-
|
|
366
|
-
// Want the filename closest to soul.
|
|
367
|
-
if (file > soul || file < read.file) return
|
|
372
|
+
// Binary search for largest filename <= soul in a sorted array.
|
|
373
|
+
const findFile = files => {
|
|
374
|
+
let lo = 0, hi = files.length - 1, result = null
|
|
375
|
+
while (lo <= hi) {
|
|
376
|
+
const mid = (lo + hi) >> 1
|
|
377
|
+
if (files[mid] <= soul) { result = files[mid]; lo = mid + 1 }
|
|
378
|
+
else { hi = mid - 1 }
|
|
379
|
+
}
|
|
380
|
+
return result
|
|
381
|
+
}
|
|
368
382
|
|
|
369
|
-
|
|
370
|
-
}
|
|
371
|
-
|
|
383
|
+
const read = file => {
|
|
384
|
+
if (!file) { cb("no file found", u); return }
|
|
385
|
+
if (opt.cache && cache.has(file)) {
|
|
386
|
+
return cb(u, cache.get(file)(key))
|
|
387
|
+
}
|
|
388
|
+
radisk.parse(file, (err, disk) => {
|
|
372
389
|
if (err) opt.log(err)
|
|
373
|
-
if (disk) {
|
|
374
|
-
|
|
375
|
-
|
|
376
|
-
checkMemoryUsage() // Check memory usage after adding to cache
|
|
377
|
-
}
|
|
378
|
-
read.value = disk(key)
|
|
390
|
+
if (disk && opt.cache) {
|
|
391
|
+
cache.set(file, disk)
|
|
392
|
+
checkMemoryUsage()
|
|
379
393
|
}
|
|
380
|
-
cb(err,
|
|
381
|
-
}
|
|
394
|
+
cb(err, disk ? disk(key) : u)
|
|
395
|
+
})
|
|
382
396
|
}
|
|
397
|
+
|
|
383
398
|
const now = Date.now()
|
|
384
|
-
if (
|
|
385
|
-
|
|
386
|
-
fileListCache &&
|
|
387
|
-
now - fileListCacheTime < FILE_LIST_CACHE_TTL
|
|
388
|
-
) {
|
|
389
|
-
// Use cached file list
|
|
390
|
-
fileListCache.forEach(file => read.lex(file))
|
|
391
|
-
read.lex() // Signal end of list
|
|
399
|
+
if (opt.cache && fileListCache && now - fileListCacheTime < FILE_LIST_CACHE_TTL) {
|
|
400
|
+
read(findFile(fileListCache, soul))
|
|
392
401
|
} else {
|
|
393
|
-
// Refresh cache
|
|
394
402
|
const files = []
|
|
395
|
-
const originalLex = read.lex
|
|
396
|
-
read.lex = file => {
|
|
397
|
-
if (file) files.push(file)
|
|
398
|
-
return originalLex(file)
|
|
399
|
-
}
|
|
400
|
-
|
|
401
403
|
opt.store.list(file => {
|
|
402
|
-
|
|
403
|
-
|
|
404
|
-
|
|
404
|
+
if (file) { files.push(file); return }
|
|
405
|
+
files.sort()
|
|
406
|
+
if (opt.cache) {
|
|
405
407
|
fileListCache = files
|
|
406
408
|
fileListCacheTime = now
|
|
407
409
|
}
|
|
410
|
+
read(findFile(files, soul))
|
|
408
411
|
})
|
|
409
412
|
}
|
|
410
413
|
}
|
package/src/sea-utils.js
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import SafeBuffer from "./buffer.js"
|
|
2
2
|
|
|
3
|
-
const isNode = typeof
|
|
3
|
+
const isNode = typeof process !== "undefined" && process.versions?.node != null
|
|
4
4
|
const crypto = isNode
|
|
5
5
|
? (await import(/*webpackIgnore: true*/ "node:crypto")).webcrypto
|
|
6
6
|
: globalThis.crypto
|
package/src/sea.js
CHANGED
|
@@ -238,58 +238,6 @@ const SEA = {
|
|
|
238
238
|
return false
|
|
239
239
|
}
|
|
240
240
|
},
|
|
241
|
-
// Verify individual property signatures stored in node._["s"]
|
|
242
|
-
// Returns array of valid property names
|
|
243
|
-
verifyProperties: async (node, pub, cb) => {
|
|
244
|
-
if (!pub || !node) {
|
|
245
|
-
if (cb) cb([])
|
|
246
|
-
return []
|
|
247
|
-
}
|
|
248
|
-
|
|
249
|
-
const key = await utils.subtle.importKey(
|
|
250
|
-
"jwk",
|
|
251
|
-
utils.jwk(pub),
|
|
252
|
-
{name: "ECDSA", namedCurve: "P-256"},
|
|
253
|
-
false,
|
|
254
|
-
["verify"],
|
|
255
|
-
)
|
|
256
|
-
|
|
257
|
-
const alg = {name: "ECDSA", hash: {name: "SHA-256"}}
|
|
258
|
-
const valid = []
|
|
259
|
-
const propertySignatures = (node._ && node._["s"]) || {}
|
|
260
|
-
|
|
261
|
-
// Verify each property individually
|
|
262
|
-
for (const k of Object.keys(node).sort()) {
|
|
263
|
-
if (k === "_" || k == userPublicKey) continue
|
|
264
|
-
|
|
265
|
-
const propSig = propertySignatures[k]
|
|
266
|
-
if (!propSig) {
|
|
267
|
-
continue
|
|
268
|
-
}
|
|
269
|
-
|
|
270
|
-
try {
|
|
271
|
-
const hash = await utils.sha256(node[k])
|
|
272
|
-
const sig = new Uint8Array(SafeBuffer.from(propSig, "base64"))
|
|
273
|
-
|
|
274
|
-
const isValid = await utils.subtle.verify(
|
|
275
|
-
alg,
|
|
276
|
-
key,
|
|
277
|
-
sig,
|
|
278
|
-
new Uint8Array(hash),
|
|
279
|
-
)
|
|
280
|
-
if (isValid) {
|
|
281
|
-
valid.push(k)
|
|
282
|
-
} else {
|
|
283
|
-
console.log(`warning: property '${k}' signature verification failed`)
|
|
284
|
-
}
|
|
285
|
-
} catch (err) {
|
|
286
|
-
console.log(`warning: property '${k}' error verifying: ${err.message}`)
|
|
287
|
-
}
|
|
288
|
-
}
|
|
289
|
-
|
|
290
|
-
if (cb) cb(valid)
|
|
291
|
-
return valid
|
|
292
|
-
},
|
|
293
241
|
work: async (data, salt, cb) => {
|
|
294
242
|
if (typeof salt === "function") {
|
|
295
243
|
cb = salt
|
package/src/store.js
CHANGED
|
@@ -2,7 +2,7 @@ import Radisk from "./radisk.js"
|
|
|
2
2
|
import Radix from "./radix.js"
|
|
3
3
|
import * as utils from "./utils.js"
|
|
4
4
|
|
|
5
|
-
const isNode = typeof
|
|
5
|
+
const isNode = typeof process !== "undefined" && process.versions?.node != null
|
|
6
6
|
const fs = isNode ? await import(/*webpackIgnore: true*/ "node:fs") : undefined
|
|
7
7
|
|
|
8
8
|
// ASCII character for enquiry.
|
|
@@ -112,6 +112,7 @@ const fileSystem = opt => {
|
|
|
112
112
|
const req = tx.objectStore(dir).get(file)
|
|
113
113
|
req.onerror = () => {
|
|
114
114
|
console.log(`error getting ${dir}/${file}`)
|
|
115
|
+
cb(req.error)
|
|
115
116
|
}
|
|
116
117
|
req.onsuccess = () => {
|
|
117
118
|
cb(null, req.result)
|
|
@@ -132,6 +133,7 @@ const fileSystem = opt => {
|
|
|
132
133
|
const req = tx.objectStore(dir).put(data, file)
|
|
133
134
|
req.onerror = () => {
|
|
134
135
|
console.log(`error putting data on ${dir}/${file}`)
|
|
136
|
+
cb(req.error)
|
|
135
137
|
}
|
|
136
138
|
req.onsuccess = () => {
|
|
137
139
|
cb(null)
|
|
@@ -150,7 +152,10 @@ const fileSystem = opt => {
|
|
|
150
152
|
const _list = cb => {
|
|
151
153
|
const tx = db.transaction([dir], "readonly")
|
|
152
154
|
const req = tx.objectStore(dir).getAllKeys()
|
|
153
|
-
req.onerror = () =>
|
|
155
|
+
req.onerror = () => {
|
|
156
|
+
console.log("error getting keys for", dir)
|
|
157
|
+
cb()
|
|
158
|
+
}
|
|
154
159
|
req.onsuccess = () => {
|
|
155
160
|
req.result.forEach(cb)
|
|
156
161
|
cb()
|
|
@@ -217,12 +222,7 @@ const Store = opt => {
|
|
|
217
222
|
node._[">"][key] = value[1]
|
|
218
223
|
// If signature is present, store it in _["s"]
|
|
219
224
|
if (value.length === 3 && value[2]) {
|
|
220
|
-
|
|
221
|
-
// Store signature under both state and key for compatibility
|
|
222
|
-
signatures[state] = value[2]
|
|
223
|
-
// TODO: Can probably remove the following when old data is no
|
|
224
|
-
// longer a problem.
|
|
225
|
-
signatures[key] = value[2]
|
|
225
|
+
signatures[value[1]] = value[2]
|
|
226
226
|
}
|
|
227
227
|
}
|
|
228
228
|
|
package/src/user.js
CHANGED
|
@@ -5,7 +5,6 @@ import {userPublicKey, userSignature} from "./utils.js"
|
|
|
5
5
|
|
|
6
6
|
const User = (opt, wire) => {
|
|
7
7
|
if (!wire) wire = Wire(opt)
|
|
8
|
-
const wait = opt?.wait ?? 5000
|
|
9
8
|
let pubs = []
|
|
10
9
|
let creating = false
|
|
11
10
|
let authing = false
|
|
@@ -76,7 +75,6 @@ const User = (opt, wire) => {
|
|
|
76
75
|
|
|
77
76
|
done(null)
|
|
78
77
|
},
|
|
79
|
-
{wait: wait},
|
|
80
78
|
)
|
|
81
79
|
}
|
|
82
80
|
|
|
@@ -103,7 +101,6 @@ const User = (opt, wire) => {
|
|
|
103
101
|
pubs = Object.keys(data)
|
|
104
102
|
next()
|
|
105
103
|
},
|
|
106
|
-
{wait: wait},
|
|
107
104
|
)
|
|
108
105
|
}
|
|
109
106
|
|
|
@@ -183,7 +180,6 @@ const User = (opt, wire) => {
|
|
|
183
180
|
})
|
|
184
181
|
})
|
|
185
182
|
},
|
|
186
|
-
{wait: wait},
|
|
187
183
|
)
|
|
188
184
|
},
|
|
189
185
|
auth: (username, password, cb) => {
|