@mblaney/holster 2.0.1 → 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 -28
- package/src/store.js +30 -17
- package/src/user.js +92 -101
- package/src/wire.js +325 -125
- 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 -51
- 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 +76 -58
- package/ts/src/user.ts +136 -113
- package/ts/src/utils.ts +36 -11
- package/ts/src/wire.ts +477 -248
package/ts/src/index.ts
CHANGED
|
@@ -1,35 +1,34 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* Holster TypeScript Entry Point
|
|
3
|
-
*
|
|
3
|
+
*
|
|
4
4
|
* This is the main entry point for the TypeScript version of Holster.
|
|
5
5
|
* It exports all the main modules and types.
|
|
6
6
|
*/
|
|
7
7
|
|
|
8
8
|
// Main API
|
|
9
|
-
export {
|
|
9
|
+
export {default as Holster, type HolsterAPI} from "./holster.ts"
|
|
10
10
|
|
|
11
11
|
// Core modules
|
|
12
|
-
export {
|
|
13
|
-
export {
|
|
14
|
-
export {
|
|
15
|
-
export {
|
|
12
|
+
export {default as Wire, type WireAPI} from "./wire.ts"
|
|
13
|
+
export {default as User, type UserInterface} from "./user.ts"
|
|
14
|
+
export {default as SEA} from "./sea.ts"
|
|
15
|
+
export {default as Store, type StoreInterface} from "./store.ts"
|
|
16
16
|
|
|
17
17
|
// Data structure modules
|
|
18
|
-
export {
|
|
19
|
-
export {
|
|
20
|
-
export {
|
|
21
|
-
export {
|
|
22
|
-
export {
|
|
18
|
+
export {default as Ham} from "./ham.ts"
|
|
19
|
+
export {default as Get} from "./get.ts"
|
|
20
|
+
export {default as Radisk} from "./radisk.ts"
|
|
21
|
+
export {default as Radix} from "./radix.ts"
|
|
22
|
+
export {default as Dup, type DupInterface} from "./dup.ts"
|
|
23
23
|
|
|
24
24
|
// Utilities
|
|
25
25
|
export * as utils from "./utils.ts"
|
|
26
26
|
export * as seaUtils from "./sea-utils.ts"
|
|
27
|
-
export {
|
|
28
|
-
export {
|
|
27
|
+
export {default as SafeBuffer} from "./buffer.ts"
|
|
28
|
+
export {default as SeaArray} from "./array.ts"
|
|
29
29
|
|
|
30
30
|
// Types and Schemas
|
|
31
31
|
export * from "./schemas.ts"
|
|
32
32
|
|
|
33
33
|
// Default export
|
|
34
|
-
export {
|
|
35
|
-
|
|
34
|
+
export {default} from "./holster.ts"
|
package/ts/src/radisk.ts
CHANGED
|
@@ -4,9 +4,14 @@
|
|
|
4
4
|
*/
|
|
5
5
|
|
|
6
6
|
import Radix from "./radix.ts"
|
|
7
|
-
import type {
|
|
7
|
+
import type {RadixFunction} from "./schemas.ts"
|
|
8
8
|
import * as utils from "./utils.ts"
|
|
9
|
-
import type {
|
|
9
|
+
import type {
|
|
10
|
+
EncodedValue,
|
|
11
|
+
RadiskOptions,
|
|
12
|
+
GraphValue,
|
|
13
|
+
Relation,
|
|
14
|
+
} from "./schemas.ts"
|
|
10
15
|
|
|
11
16
|
// ASCII character for end of text
|
|
12
17
|
const etx = String.fromCharCode(3)
|
|
@@ -18,8 +23,18 @@ const unit = String.fromCharCode(31)
|
|
|
18
23
|
export interface RadiskInterface extends RadixFunction {
|
|
19
24
|
// Override call signature to support both get (with callback) and put (with value and optional callback)
|
|
20
25
|
// Accepts GraphValue (raw values) OR EncodedValue (tuples) - see src/radisk.js and tests
|
|
21
|
-
(
|
|
22
|
-
|
|
26
|
+
(
|
|
27
|
+
key?: string,
|
|
28
|
+
value?:
|
|
29
|
+
| GraphValue
|
|
30
|
+
| EncodedValue
|
|
31
|
+
| ((
|
|
32
|
+
err?: string | null,
|
|
33
|
+
value?: EncodedValue | Record<string, EncodedValue>,
|
|
34
|
+
) => void),
|
|
35
|
+
cb?: (err?: string | null) => void,
|
|
36
|
+
): void
|
|
37
|
+
|
|
23
38
|
batch: RadixFunction & {
|
|
24
39
|
acks: Array<(err?: string | null) => void>
|
|
25
40
|
ed: number
|
|
@@ -32,9 +47,22 @@ export interface RadiskInterface extends RadixFunction {
|
|
|
32
47
|
more?: boolean
|
|
33
48
|
}
|
|
34
49
|
save: (rad: RadixFunction, cb: (err?: string | null) => void) => void
|
|
35
|
-
write: (
|
|
36
|
-
|
|
37
|
-
|
|
50
|
+
write: (
|
|
51
|
+
file: string,
|
|
52
|
+
rad: RadixFunction,
|
|
53
|
+
cb: (err?: string | null) => void,
|
|
54
|
+
) => void
|
|
55
|
+
read: (
|
|
56
|
+
key: string,
|
|
57
|
+
cb: (
|
|
58
|
+
err?: string | null,
|
|
59
|
+
value?: EncodedValue | Record<string, EncodedValue>,
|
|
60
|
+
) => void,
|
|
61
|
+
) => void
|
|
62
|
+
parse: (
|
|
63
|
+
file: string,
|
|
64
|
+
cb: (err?: string | null, disk?: RadixFunction) => void,
|
|
65
|
+
) => void
|
|
38
66
|
}
|
|
39
67
|
|
|
40
68
|
/**
|
|
@@ -48,7 +76,12 @@ const Radisk = (opt: RadiskOptions) => {
|
|
|
48
76
|
|
|
49
77
|
const pendingReads = new Map<
|
|
50
78
|
string,
|
|
51
|
-
Array<
|
|
79
|
+
Array<
|
|
80
|
+
(
|
|
81
|
+
err?: string | null,
|
|
82
|
+
result?: EncodedValue | Record<string, EncodedValue>,
|
|
83
|
+
) => void
|
|
84
|
+
>
|
|
52
85
|
>()
|
|
53
86
|
|
|
54
87
|
let lastMemoryCheck = 0
|
|
@@ -67,30 +100,27 @@ const Radisk = (opt: RadiskOptions) => {
|
|
|
67
100
|
}
|
|
68
101
|
|
|
69
102
|
if (!options.store) {
|
|
70
|
-
throw new Error(
|
|
103
|
+
throw new Error(
|
|
104
|
+
"Radisk needs `store` interface with `{get: fn, put: fn, list: fn}`",
|
|
105
|
+
)
|
|
71
106
|
}
|
|
72
107
|
if (!options.store.get) {
|
|
73
108
|
throw new Error("Radisk needs `store.get` interface with `(file, cb)`")
|
|
74
109
|
}
|
|
75
110
|
if (!options.store.put) {
|
|
76
|
-
throw new Error(
|
|
111
|
+
throw new Error(
|
|
112
|
+
"Radisk needs `store.put` interface with `(file, data, cb)`",
|
|
113
|
+
)
|
|
77
114
|
}
|
|
78
115
|
if (!options.store.list) {
|
|
79
|
-
throw new Error(
|
|
116
|
+
throw new Error(
|
|
117
|
+
"Radisk needs a streaming `store.list` interface with `(cb)`",
|
|
118
|
+
)
|
|
80
119
|
}
|
|
81
120
|
|
|
82
121
|
// After validation, store is guaranteed to be defined
|
|
83
122
|
const store = options.store
|
|
84
123
|
|
|
85
|
-
const perfLog = (operation: string, startTime: number, key: string, size?: number): void => {
|
|
86
|
-
const duration = Date.now() - startTime
|
|
87
|
-
if (duration > 3000) {
|
|
88
|
-
console.log(
|
|
89
|
-
`[RADISK-SLOW] ${operation}: ${duration}ms for ${key}${size ? ` (${size} bytes)` : ""}`
|
|
90
|
-
)
|
|
91
|
-
}
|
|
92
|
-
}
|
|
93
|
-
|
|
94
124
|
const checkMemoryUsage = (): void => {
|
|
95
125
|
if (typeof process === "undefined" || !process.memoryUsage) return
|
|
96
126
|
|
|
@@ -106,21 +136,31 @@ const Radisk = (opt: RadiskOptions) => {
|
|
|
106
136
|
if (heapUsageRatio > HEAP_CLEANUP_THRESHOLD) {
|
|
107
137
|
const cacheSize = cache.size
|
|
108
138
|
console.log(
|
|
109
|
-
`[RADISK-MEMORY] High memory usage: ${Math.round(heapUsageRatio * 100)}% of heap limit. Clearing ${cacheSize} cached files
|
|
139
|
+
`[RADISK-MEMORY] High memory usage: ${Math.round(heapUsageRatio * 100)}% of heap limit. Clearing ${cacheSize} cached files.`,
|
|
110
140
|
)
|
|
111
141
|
cache.clear()
|
|
112
|
-
if ((global as {
|
|
113
|
-
(global as {
|
|
142
|
+
if ((global as {gc?: () => void}).gc) {
|
|
143
|
+
;(global as {gc: () => void}).gc()
|
|
114
144
|
}
|
|
115
145
|
lastMemoryCheck = now + MEMORY_CHECK_INTERVAL
|
|
116
146
|
} else if (heapUsageRatio > HEAP_WARNING_THRESHOLD) {
|
|
117
147
|
console.log(
|
|
118
|
-
`[RADISK-MEMORY] Memory warning: ${Math.round(heapUsageRatio * 100)}% of heap limit used. Cache size: ${cache.size} files
|
|
148
|
+
`[RADISK-MEMORY] Memory warning: ${Math.round(heapUsageRatio * 100)}% of heap limit used. Cache size: ${cache.size} files.`,
|
|
119
149
|
)
|
|
120
150
|
}
|
|
121
151
|
}
|
|
122
152
|
|
|
123
|
-
const radisk = ((
|
|
153
|
+
const radisk = ((
|
|
154
|
+
key?: string,
|
|
155
|
+
value?:
|
|
156
|
+
| GraphValue
|
|
157
|
+
| EncodedValue
|
|
158
|
+
| ((
|
|
159
|
+
err?: string | null,
|
|
160
|
+
value?: EncodedValue | Record<string, EncodedValue>,
|
|
161
|
+
) => void),
|
|
162
|
+
cb?: (err?: string | null) => void,
|
|
163
|
+
) => {
|
|
124
164
|
if (typeof key !== "string") return undefined
|
|
125
165
|
|
|
126
166
|
if (typeof value === "function") {
|
|
@@ -143,7 +183,6 @@ const Radisk = (opt: RadiskOptions) => {
|
|
|
143
183
|
}
|
|
144
184
|
|
|
145
185
|
pendingReads.set(key, [callback])
|
|
146
|
-
const readStart = Date.now()
|
|
147
186
|
const timeout = setTimeout(() => {
|
|
148
187
|
const callbacks = pendingReads.get(key)
|
|
149
188
|
if (!callbacks) return
|
|
@@ -153,7 +192,6 @@ const Radisk = (opt: RadiskOptions) => {
|
|
|
153
192
|
}, 30000)
|
|
154
193
|
return radisk.read(key, (err, result) => {
|
|
155
194
|
clearTimeout(timeout)
|
|
156
|
-
perfLog("read", readStart, key)
|
|
157
195
|
const callbacks = pendingReads.get(key) || []
|
|
158
196
|
pendingReads.delete(key)
|
|
159
197
|
callbacks.forEach(callback => callback(err, result))
|
|
@@ -184,7 +222,6 @@ const Radisk = (opt: RadiskOptions) => {
|
|
|
184
222
|
return
|
|
185
223
|
}
|
|
186
224
|
|
|
187
|
-
const thrashStart = Date.now()
|
|
188
225
|
clearTimeout(radisk.batch.timeout)
|
|
189
226
|
radisk.thrash.more = false
|
|
190
227
|
radisk.thrash.ing = true
|
|
@@ -196,7 +233,6 @@ const Radisk = (opt: RadiskOptions) => {
|
|
|
196
233
|
radisk.save(batch, err => {
|
|
197
234
|
if (++i > 1) return
|
|
198
235
|
|
|
199
|
-
perfLog("thrash", thrashStart, `batch-${batch.ed}`)
|
|
200
236
|
if (err) options.log(err)
|
|
201
237
|
batch.acks.forEach(cb => cb(err))
|
|
202
238
|
radisk.thrash.at = undefined
|
|
@@ -273,7 +309,7 @@ const Radisk = (opt: RadiskOptions) => {
|
|
|
273
309
|
done: false,
|
|
274
310
|
count: 0,
|
|
275
311
|
sub: undefined as RadixFunction | undefined,
|
|
276
|
-
halfKey: "",
|
|
312
|
+
halfKey: "", // soul ID at the ~options.size/2 mark for a balanced split
|
|
277
313
|
halfOffset: 0, // write.text.length before that entry
|
|
278
314
|
each: (
|
|
279
315
|
value: EncodedValue,
|
|
@@ -301,8 +337,7 @@ const Radisk = (opt: RadiskOptions) => {
|
|
|
301
337
|
) {
|
|
302
338
|
const fullKey = pre.join("") + k
|
|
303
339
|
const endIdx = fullKey.indexOf(enq)
|
|
304
|
-
write.halfKey =
|
|
305
|
-
endIdx === -1 ? fullKey : fullKey.substring(0, endIdx)
|
|
340
|
+
write.halfKey = endIdx === -1 ? fullKey : fullKey.substring(0, endIdx)
|
|
306
341
|
write.halfOffset = write.text.length
|
|
307
342
|
}
|
|
308
343
|
|
|
@@ -337,7 +372,6 @@ const Radisk = (opt: RadiskOptions) => {
|
|
|
337
372
|
},
|
|
338
373
|
}
|
|
339
374
|
Radix.map(rad, write.each, true)
|
|
340
|
-
const writeStart = Date.now()
|
|
341
375
|
store.put(file, write.text, err => {
|
|
342
376
|
// If a split occurred the cached radix tree still contains entries that
|
|
343
377
|
// were moved to the sub-file. Invalidate so the next save.mix re-reads
|
|
@@ -346,7 +380,6 @@ const Radisk = (opt: RadiskOptions) => {
|
|
|
346
380
|
cache.delete(file)
|
|
347
381
|
fileListCache = null
|
|
348
382
|
}
|
|
349
|
-
perfLog("file-write", writeStart, file, write.text.length)
|
|
350
383
|
cb(err)
|
|
351
384
|
})
|
|
352
385
|
}
|
|
@@ -356,20 +389,35 @@ const Radisk = (opt: RadiskOptions) => {
|
|
|
356
389
|
const soul = endIdx === -1 ? key : key.substring(0, endIdx)
|
|
357
390
|
|
|
358
391
|
const findFile = (files: string[]): string | null => {
|
|
359
|
-
let lo = 0,
|
|
392
|
+
let lo = 0,
|
|
393
|
+
hi = files.length - 1,
|
|
394
|
+
result: string | null = null
|
|
360
395
|
while (lo <= hi) {
|
|
361
396
|
const mid = (lo + hi) >> 1
|
|
362
397
|
const f = files[mid]!
|
|
363
|
-
if (f <= soul) {
|
|
364
|
-
|
|
398
|
+
if (f <= soul) {
|
|
399
|
+
result = f
|
|
400
|
+
lo = mid + 1
|
|
401
|
+
} else {
|
|
402
|
+
hi = mid - 1
|
|
403
|
+
}
|
|
365
404
|
}
|
|
366
405
|
return result
|
|
367
406
|
}
|
|
368
407
|
|
|
369
408
|
const read = (file: string | null): void => {
|
|
370
|
-
if (!file) {
|
|
409
|
+
if (!file) {
|
|
410
|
+
cb("no file found", undefined)
|
|
411
|
+
return
|
|
412
|
+
}
|
|
371
413
|
if (options.cache && cache.has(file)) {
|
|
372
|
-
return cb(
|
|
414
|
+
return cb(
|
|
415
|
+
undefined,
|
|
416
|
+
cache.get(file)!(key) as
|
|
417
|
+
| EncodedValue
|
|
418
|
+
| Record<string, EncodedValue>
|
|
419
|
+
| undefined,
|
|
420
|
+
)
|
|
373
421
|
}
|
|
374
422
|
radisk.parse(file, (err, disk) => {
|
|
375
423
|
if (err) options.log(err)
|
|
@@ -377,17 +425,32 @@ const Radisk = (opt: RadiskOptions) => {
|
|
|
377
425
|
cache.set(file, disk)
|
|
378
426
|
checkMemoryUsage()
|
|
379
427
|
}
|
|
380
|
-
cb(
|
|
428
|
+
cb(
|
|
429
|
+
err,
|
|
430
|
+
disk
|
|
431
|
+
? (disk(key) as
|
|
432
|
+
| EncodedValue
|
|
433
|
+
| Record<string, EncodedValue>
|
|
434
|
+
| undefined)
|
|
435
|
+
: undefined,
|
|
436
|
+
)
|
|
381
437
|
})
|
|
382
438
|
}
|
|
383
439
|
|
|
384
440
|
const now = Date.now()
|
|
385
|
-
if (
|
|
441
|
+
if (
|
|
442
|
+
options.cache &&
|
|
443
|
+
fileListCache &&
|
|
444
|
+
now - fileListCacheTime < FILE_LIST_CACHE_TTL
|
|
445
|
+
) {
|
|
386
446
|
read(findFile(fileListCache))
|
|
387
447
|
} else {
|
|
388
448
|
const files: string[] = []
|
|
389
449
|
store.list((file?: string) => {
|
|
390
|
-
if (file) {
|
|
450
|
+
if (file) {
|
|
451
|
+
files.push(file)
|
|
452
|
+
return
|
|
453
|
+
}
|
|
391
454
|
files.sort()
|
|
392
455
|
if (options.cache) {
|
|
393
456
|
fileListCache = files
|
|
@@ -401,7 +464,10 @@ const Radisk = (opt: RadiskOptions) => {
|
|
|
401
464
|
radisk.parse = (file, cb) => {
|
|
402
465
|
const parse = {
|
|
403
466
|
disk: Radix(),
|
|
404
|
-
read: (
|
|
467
|
+
read: (
|
|
468
|
+
err?: string | null,
|
|
469
|
+
data?: string | EncodedValue | Record<string, unknown>,
|
|
470
|
+
): void => {
|
|
405
471
|
if (err) return cb(err)
|
|
406
472
|
if (!data) return cb(null, parse.disk)
|
|
407
473
|
|
|
@@ -412,7 +478,12 @@ const Radisk = (opt: RadiskOptions) => {
|
|
|
412
478
|
let key: string | undefined
|
|
413
479
|
let value: EncodedValue | undefined
|
|
414
480
|
const tmpVal = tmp[1]
|
|
415
|
-
const i =
|
|
481
|
+
const i =
|
|
482
|
+
typeof tmpVal === "number"
|
|
483
|
+
? tmpVal
|
|
484
|
+
: typeof tmpVal === "string"
|
|
485
|
+
? parseInt(tmpVal, 10)
|
|
486
|
+
: 0
|
|
416
487
|
tmp = parse.split(tmp[2]) || ("" as never)
|
|
417
488
|
if (tmp[0] === "#") {
|
|
418
489
|
const tmpKey = tmp[1]
|
|
@@ -439,16 +510,14 @@ const Radisk = (opt: RadiskOptions) => {
|
|
|
439
510
|
}
|
|
440
511
|
cb(null, parse.disk)
|
|
441
512
|
},
|
|
442
|
-
split: (
|
|
443
|
-
data: string
|
|
444
|
-
): [string, unknown, string] | undefined => {
|
|
513
|
+
split: (data: string): [string, unknown, string] | undefined => {
|
|
445
514
|
if (!data) return undefined
|
|
446
515
|
|
|
447
516
|
const i = data.indexOf(unit)
|
|
448
517
|
if (i === -1) return undefined
|
|
449
518
|
|
|
450
519
|
const a = data.slice(0, i)
|
|
451
|
-
const o: {
|
|
520
|
+
const o: {i?: number} = {}
|
|
452
521
|
return [a, Radisk.decode(data.slice(i), o), data.slice(i + o.i!)]
|
|
453
522
|
},
|
|
454
523
|
}
|
|
@@ -466,7 +535,7 @@ Radisk.encode = (input: EncodedValue | GraphValue): string => {
|
|
|
466
535
|
let state = ""
|
|
467
536
|
let sig = ""
|
|
468
537
|
let data: GraphValue
|
|
469
|
-
|
|
538
|
+
|
|
470
539
|
if (Array.isArray(input) && (input.length === 2 || input.length === 3)) {
|
|
471
540
|
// input is EncodedValue - extract the GraphValue and metadata
|
|
472
541
|
state = etx + input[1]
|
|
@@ -506,7 +575,10 @@ Radisk.encode = (input: EncodedValue | GraphValue): string => {
|
|
|
506
575
|
/**
|
|
507
576
|
* Decode value from storage
|
|
508
577
|
*/
|
|
509
|
-
Radisk.decode = (
|
|
578
|
+
Radisk.decode = (
|
|
579
|
+
data: string,
|
|
580
|
+
obj?: {i?: number},
|
|
581
|
+
): EncodedValue | GraphValue | Relation | null | undefined => {
|
|
510
582
|
let i = -1
|
|
511
583
|
let n = 0
|
|
512
584
|
let current: string | undefined = undefined
|
|
@@ -588,4 +660,3 @@ Radisk.decode = (data: string, obj?: { i?: number }): EncodedValue | GraphValue
|
|
|
588
660
|
}
|
|
589
661
|
|
|
590
662
|
export default Radisk
|
|
591
|
-
|
package/ts/src/radix.ts
CHANGED
|
@@ -4,7 +4,7 @@
|
|
|
4
4
|
*/
|
|
5
5
|
|
|
6
6
|
import * as utils from "./utils.ts"
|
|
7
|
-
import type {
|
|
7
|
+
import type {EncodedValue, RadixFunction} from "./schemas.ts"
|
|
8
8
|
|
|
9
9
|
// ASCII character for group separator
|
|
10
10
|
const group = String.fromCharCode(29)
|
|
@@ -34,10 +34,11 @@ const Radix = (): RadixFunction => {
|
|
|
34
34
|
const radix = ((
|
|
35
35
|
keys?: string,
|
|
36
36
|
value?: EncodedValue,
|
|
37
|
-
tree?: RadixTree
|
|
37
|
+
tree?: RadixTree,
|
|
38
38
|
): RadixNode | EncodedValue | undefined | Record<string, EncodedValue> => {
|
|
39
39
|
if (!tree) {
|
|
40
|
-
if (!(radix as Record<string, unknown>)[group])
|
|
40
|
+
if (!(radix as Record<string, unknown>)[group])
|
|
41
|
+
(radix as Record<string, unknown>)[group] = {}
|
|
41
42
|
tree = (radix as Record<string, RadixTree>)[group]!
|
|
42
43
|
}
|
|
43
44
|
if (!keys) return tree
|
|
@@ -57,32 +58,35 @@ const Radix = (): RadixFunction => {
|
|
|
57
58
|
|
|
58
59
|
if (!found) {
|
|
59
60
|
// If not found from the provided keys try matching with an existing key
|
|
60
|
-
const result = utils.obj.map(
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
matchingKey
|
|
65
|
-
j
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
if (noValue) {
|
|
69
|
-
// matchingKey has to be as long as the original keys when reading
|
|
70
|
-
if (j <= max) return undefined
|
|
71
|
-
|
|
72
|
-
tmp[hasKey.slice(j)] = hasValue
|
|
73
|
-
return hasValue
|
|
61
|
+
const result = utils.obj.map(
|
|
62
|
+
tree!,
|
|
63
|
+
(hasValue: RadixValue, hasKey: string) => {
|
|
64
|
+
let j = 0
|
|
65
|
+
let matchingKey = ""
|
|
66
|
+
while (hasKey[j] === keys[j]) {
|
|
67
|
+
matchingKey += hasKey[j]
|
|
68
|
+
j++
|
|
74
69
|
}
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
70
|
+
if (matchingKey) {
|
|
71
|
+
if (noValue) {
|
|
72
|
+
// matchingKey has to be as long as the original keys when reading
|
|
73
|
+
if (j <= max) return undefined
|
|
74
|
+
|
|
75
|
+
tmp[hasKey.slice(j)] = hasValue
|
|
76
|
+
return hasValue
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
const replace: RadixTree = {
|
|
80
|
+
[hasKey.slice(j)]: hasValue,
|
|
81
|
+
[keys.slice(j)]: {[record]: value!},
|
|
82
|
+
}
|
|
83
|
+
tree![matchingKey] = {[group]: replace}
|
|
84
|
+
delete tree![hasKey]
|
|
85
|
+
return true
|
|
79
86
|
}
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
}
|
|
84
|
-
return undefined
|
|
85
|
-
})
|
|
87
|
+
return undefined
|
|
88
|
+
},
|
|
89
|
+
)
|
|
86
90
|
|
|
87
91
|
if (!result) {
|
|
88
92
|
if (noValue) return undefined
|
|
@@ -96,7 +100,9 @@ const Radix = (): RadixFunction => {
|
|
|
96
100
|
// If no value use the key provided to return a whole group or record
|
|
97
101
|
if (noValue) {
|
|
98
102
|
// If an individual record isn't found then return the whole group
|
|
99
|
-
return typeof found[record] === "undefined"
|
|
103
|
+
return typeof found[record] === "undefined"
|
|
104
|
+
? found[group]
|
|
105
|
+
: found[record]
|
|
100
106
|
}
|
|
101
107
|
// Otherwise create a new record at the provided key for value
|
|
102
108
|
found[record] = value!
|
|
@@ -120,22 +126,33 @@ const Radix = (): RadixFunction => {
|
|
|
120
126
|
*/
|
|
121
127
|
Radix.map = function map(
|
|
122
128
|
radix: RadixFunction | RadixNode,
|
|
123
|
-
cb: (
|
|
129
|
+
cb: (
|
|
130
|
+
value: EncodedValue,
|
|
131
|
+
fullKey: string,
|
|
132
|
+
key: string,
|
|
133
|
+
pre: string[],
|
|
134
|
+
) => unknown,
|
|
124
135
|
opt?: boolean,
|
|
125
|
-
pre: string[] = []
|
|
136
|
+
pre: string[] = [],
|
|
126
137
|
): unknown {
|
|
127
|
-
const tree = ((radix as Record<string, RadixTree>)[group] ||
|
|
138
|
+
const tree = ((radix as Record<string, RadixTree>)[group] ||
|
|
139
|
+
radix) as RadixTree
|
|
128
140
|
const keys = Object.keys(tree).sort()
|
|
129
141
|
|
|
130
142
|
for (let i = 0; i < keys.length; i++) {
|
|
131
143
|
const key = keys[i]!
|
|
132
144
|
const found = tree[key]
|
|
133
145
|
if (!found) continue
|
|
134
|
-
|
|
146
|
+
|
|
135
147
|
const recordValue = found[record]
|
|
136
148
|
|
|
137
149
|
if (typeof recordValue !== "undefined") {
|
|
138
|
-
const result = cb(
|
|
150
|
+
const result = cb(
|
|
151
|
+
recordValue as EncodedValue,
|
|
152
|
+
pre.join("") + key,
|
|
153
|
+
key,
|
|
154
|
+
pre,
|
|
155
|
+
)
|
|
139
156
|
if (typeof result !== "undefined") return result
|
|
140
157
|
} else if (opt) {
|
|
141
158
|
cb(undefined as never, pre.join(""), key, pre)
|
|
@@ -154,4 +171,3 @@ Radix.map = function map(
|
|
|
154
171
|
}
|
|
155
172
|
|
|
156
173
|
export default Radix
|
|
157
|
-
|
package/ts/src/schemas.ts
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import {
|
|
1
|
+
import {z} from "zod"
|
|
2
2
|
|
|
3
3
|
// ============================================================================
|
|
4
4
|
// Core Primitive Types
|
|
@@ -48,7 +48,7 @@ export const GraphValueSchema: z.ZodType<GraphValue> = z.lazy(() =>
|
|
|
48
48
|
z.null(),
|
|
49
49
|
RelationSchema,
|
|
50
50
|
z.record(z.string(), GraphValueSchema),
|
|
51
|
-
])
|
|
51
|
+
]),
|
|
52
52
|
)
|
|
53
53
|
|
|
54
54
|
export type GraphValue =
|
|
@@ -57,7 +57,7 @@ export type GraphValue =
|
|
|
57
57
|
| boolean
|
|
58
58
|
| null
|
|
59
59
|
| Relation
|
|
60
|
-
| {
|
|
60
|
+
| {[key: string]: GraphValue}
|
|
61
61
|
|
|
62
62
|
/**
|
|
63
63
|
* State vector - tracks timestamps for each property
|
|
@@ -91,9 +91,11 @@ export const USER_SIGNATURE = "_holster_user_signature"
|
|
|
91
91
|
* Graph node - contains metadata and properties
|
|
92
92
|
* Note: Using manual type to properly handle the index signature with special _ key
|
|
93
93
|
*/
|
|
94
|
-
export const GraphNodeSchema = z
|
|
95
|
-
|
|
96
|
-
|
|
94
|
+
export const GraphNodeSchema = z
|
|
95
|
+
.object({
|
|
96
|
+
_: GraphMetadataSchema,
|
|
97
|
+
})
|
|
98
|
+
.and(z.record(z.string(), GraphValueSchema))
|
|
97
99
|
|
|
98
100
|
export type GraphNode = {
|
|
99
101
|
_: GraphMetadata
|
|
@@ -141,9 +143,7 @@ export const LexFilterSchema = z.union([
|
|
|
141
143
|
export type LexFilter = z.infer<typeof LexFilterSchema>
|
|
142
144
|
|
|
143
145
|
// LexFilter with optional "." property for nested queries
|
|
144
|
-
export type LexWithDot =
|
|
145
|
-
| LexFilter
|
|
146
|
-
| { ".": LexFilter }
|
|
146
|
+
export type LexWithDot = LexFilter | {".": LexFilter}
|
|
147
147
|
|
|
148
148
|
/**
|
|
149
149
|
* Lex query object
|
|
@@ -294,8 +294,18 @@ export const FileSystemInterfaceSchema = z.object({
|
|
|
294
294
|
// FileSystemInterface: Flexible to support both string-based and EncodedValue storage
|
|
295
295
|
// In-memory stores use strings, disk stores use EncodedValue/RadixNode
|
|
296
296
|
export interface FileSystemInterface {
|
|
297
|
-
get: (
|
|
298
|
-
|
|
297
|
+
get: (
|
|
298
|
+
file: string,
|
|
299
|
+
cb: (
|
|
300
|
+
err?: string | null,
|
|
301
|
+
data?: string | EncodedValue | Record<string, unknown>,
|
|
302
|
+
) => void,
|
|
303
|
+
) => void
|
|
304
|
+
put: (
|
|
305
|
+
file: string,
|
|
306
|
+
data: string | EncodedValue | Record<string, unknown>,
|
|
307
|
+
cb: (err?: string | null) => void,
|
|
308
|
+
) => void
|
|
299
309
|
list: (cb: (file?: string) => void) => void
|
|
300
310
|
}
|
|
301
311
|
|
|
@@ -405,6 +415,9 @@ export const HolsterOptionsSchema = z
|
|
|
405
415
|
maxConnections: z.number().optional(),
|
|
406
416
|
maxMessageSize: z.number().optional(),
|
|
407
417
|
maxQueueLength: z.number().optional(),
|
|
418
|
+
userLimit: z.boolean().optional(),
|
|
419
|
+
defaultLimit: z.number().optional(),
|
|
420
|
+
maxGraphSize: z.number().optional(),
|
|
408
421
|
})
|
|
409
422
|
.passthrough()
|
|
410
423
|
export type HolsterOptions = z.infer<typeof HolsterOptionsSchema>
|
|
@@ -462,20 +475,28 @@ export type RadixValue = GraphValue | EncodedValue | RadixNode
|
|
|
462
475
|
*/
|
|
463
476
|
export type RadixFunction = {
|
|
464
477
|
// Main call signature: synchronous get/put operations
|
|
465
|
-
(
|
|
466
|
-
|
|
467
|
-
|
|
478
|
+
(
|
|
479
|
+
keys?: string,
|
|
480
|
+
value?: GraphValue | EncodedValue,
|
|
481
|
+
tree?: RadixNode,
|
|
482
|
+
): RadixNode | EncodedValue | undefined | Record<string, EncodedValue>
|
|
483
|
+
|
|
468
484
|
// Callback form: async operations
|
|
469
485
|
(key?: string, value?: RadixValue, cb?: (err?: string) => void): void
|
|
470
|
-
|
|
486
|
+
|
|
471
487
|
// Static map method for iterating over radix tree values
|
|
472
488
|
map: (
|
|
473
|
-
radix: RadixFunction | RadixNode,
|
|
474
|
-
cb: (
|
|
489
|
+
radix: RadixFunction | RadixNode,
|
|
490
|
+
cb: (
|
|
491
|
+
value: EncodedValue,
|
|
492
|
+
fullKey: string,
|
|
493
|
+
key: string,
|
|
494
|
+
pre: string[],
|
|
495
|
+
) => unknown,
|
|
475
496
|
opt?: boolean,
|
|
476
|
-
pre?: string[]
|
|
497
|
+
pre?: string[],
|
|
477
498
|
) => unknown
|
|
478
|
-
|
|
499
|
+
|
|
479
500
|
// Dynamic properties for internal state (e.g., group separator key)
|
|
480
501
|
[key: string]: unknown
|
|
481
502
|
}
|
|
@@ -506,10 +527,9 @@ export type Dup = z.infer<typeof DupSchema> & {
|
|
|
506
527
|
// ============================================================================
|
|
507
528
|
|
|
508
529
|
// Export RadiskInterface from radisk module for test usage
|
|
509
|
-
export type {
|
|
530
|
+
export type {RadiskInterface} from "./radisk.ts"
|
|
510
531
|
|
|
511
532
|
// Export other interfaces for test usage
|
|
512
|
-
export type {
|
|
513
|
-
export type {
|
|
514
|
-
export type {
|
|
515
|
-
|
|
533
|
+
export type {WireAPI as WireInterface} from "./wire.ts"
|
|
534
|
+
export type {UserInterface} from "./user.ts"
|
|
535
|
+
export type {StoreInterface} from "./store.ts"
|