@mblaney/holster 1.0.35 → 1.0.36
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 +23 -5
- package/src/get.js +6 -18
- package/src/ham.js +17 -47
- package/src/holster.js +106 -28
- package/src/sea.js +0 -52
- package/src/store.js +7 -7
- package/src/wire.js +7 -1
- package/ts/src/array.ts +78 -0
- package/ts/src/buffer.ts +441 -0
- package/ts/src/dup.ts +49 -0
- package/ts/src/get.ts +83 -0
- package/ts/src/ham.ts +254 -0
- package/ts/src/holster.ts +931 -0
- package/ts/src/index.ts +35 -0
- package/ts/src/radisk.ts +585 -0
- package/ts/src/radix.ts +157 -0
- package/ts/src/schemas.ts +519 -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 +381 -0
- package/ts/src/utils.ts +232 -0
- package/ts/src/wire.ts +829 -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/ts/src/index.ts
ADDED
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Holster TypeScript Entry Point
|
|
3
|
+
*
|
|
4
|
+
* This is the main entry point for the TypeScript version of Holster.
|
|
5
|
+
* It exports all the main modules and types.
|
|
6
|
+
*/
|
|
7
|
+
|
|
8
|
+
// Main API
|
|
9
|
+
export { default as Holster, type HolsterAPI } from "./holster.ts"
|
|
10
|
+
|
|
11
|
+
// Core modules
|
|
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
|
+
|
|
17
|
+
// Data structure modules
|
|
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
|
+
|
|
24
|
+
// Utilities
|
|
25
|
+
export * as utils from "./utils.ts"
|
|
26
|
+
export * as seaUtils from "./sea-utils.ts"
|
|
27
|
+
export { default as SafeBuffer } from "./buffer.ts"
|
|
28
|
+
export { default as SeaArray } from "./array.ts"
|
|
29
|
+
|
|
30
|
+
// Types and Schemas
|
|
31
|
+
export * from "./schemas.ts"
|
|
32
|
+
|
|
33
|
+
// Default export
|
|
34
|
+
export { default } from "./holster.ts"
|
|
35
|
+
|
package/ts/src/radisk.ts
ADDED
|
@@ -0,0 +1,585 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Radisk - Persistent radix tree storage
|
|
3
|
+
* Provides disk-backed radix tree with caching and batching
|
|
4
|
+
*/
|
|
5
|
+
|
|
6
|
+
import Radix from "./radix.ts"
|
|
7
|
+
import type { RadixFunction } from "./schemas.ts"
|
|
8
|
+
import * as utils from "./utils.ts"
|
|
9
|
+
import type { EncodedValue, RadiskOptions, GraphValue, Relation } from "./schemas.ts"
|
|
10
|
+
|
|
11
|
+
// ASCII character for end of text
|
|
12
|
+
const etx = String.fromCharCode(3)
|
|
13
|
+
// ASCII character for enquiry
|
|
14
|
+
const enq = String.fromCharCode(5)
|
|
15
|
+
// ASCII character for unit separator
|
|
16
|
+
const unit = String.fromCharCode(31)
|
|
17
|
+
|
|
18
|
+
export interface RadiskInterface extends RadixFunction {
|
|
19
|
+
// Override call signature to support both get (with callback) and put (with value and optional callback)
|
|
20
|
+
// Accepts GraphValue (raw values) OR EncodedValue (tuples) - see src/radisk.js and tests
|
|
21
|
+
(key?: string, value?: GraphValue | EncodedValue | ((err?: string | null, value?: EncodedValue | Record<string, EncodedValue>) => void), cb?: (err?: string | null) => void): void
|
|
22
|
+
|
|
23
|
+
batch: RadixFunction & {
|
|
24
|
+
acks: Array<(err?: string | null) => void>
|
|
25
|
+
ed: number
|
|
26
|
+
timeout?: NodeJS.Timeout
|
|
27
|
+
}
|
|
28
|
+
thrash: {
|
|
29
|
+
(): void
|
|
30
|
+
at?: RadixFunction
|
|
31
|
+
ing?: boolean
|
|
32
|
+
more?: boolean
|
|
33
|
+
}
|
|
34
|
+
save: (rad: RadixFunction, cb: (err?: string | null) => void) => void
|
|
35
|
+
write: (file: string, rad: RadixFunction, cb: (err?: string | null) => void) => void
|
|
36
|
+
read: (key: string, cb: (err?: string | null, value?: EncodedValue | Record<string, EncodedValue>) => void) => void
|
|
37
|
+
parse: (file: string, cb: (err?: string | null, disk?: RadixFunction) => void) => void
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
/**
|
|
41
|
+
* Create a Radisk instance with persistent storage
|
|
42
|
+
*/
|
|
43
|
+
const Radisk = (opt: RadiskOptions) => {
|
|
44
|
+
const cache = new Map<string, RadixFunction>()
|
|
45
|
+
let fileListCache: string[] | null = null
|
|
46
|
+
let fileListCacheTime = 0
|
|
47
|
+
const FILE_LIST_CACHE_TTL = 10000
|
|
48
|
+
|
|
49
|
+
const pendingReads = new Map<
|
|
50
|
+
string,
|
|
51
|
+
{
|
|
52
|
+
callbacks: Array<(err?: string, result?: EncodedValue | Record<string, EncodedValue>) => void>
|
|
53
|
+
fired: boolean
|
|
54
|
+
timeoutId: NodeJS.Timeout | null
|
|
55
|
+
}
|
|
56
|
+
>()
|
|
57
|
+
|
|
58
|
+
let lastMemoryCheck = 0
|
|
59
|
+
const MEMORY_CHECK_INTERVAL = 30000
|
|
60
|
+
const HEAP_WARNING_THRESHOLD = 0.8
|
|
61
|
+
const HEAP_CLEANUP_THRESHOLD = 0.9
|
|
62
|
+
|
|
63
|
+
const options = {
|
|
64
|
+
log: opt.log || console.log,
|
|
65
|
+
batch: opt.batch || 100,
|
|
66
|
+
write: opt.write || 1,
|
|
67
|
+
size: opt.size || 1024 * 1024,
|
|
68
|
+
memoryLimit: opt.memoryLimit || 500,
|
|
69
|
+
readTimeout: opt.readTimeout || 1000,
|
|
70
|
+
cache: opt.cache !== undefined ? opt.cache : true,
|
|
71
|
+
store: opt.store,
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
if (!options.store) {
|
|
75
|
+
throw new Error("Radisk needs `store` interface with `{get: fn, put: fn, list: fn}`")
|
|
76
|
+
}
|
|
77
|
+
if (!options.store.get) {
|
|
78
|
+
throw new Error("Radisk needs `store.get` interface with `(file, cb)`")
|
|
79
|
+
}
|
|
80
|
+
if (!options.store.put) {
|
|
81
|
+
throw new Error("Radisk needs `store.put` interface with `(file, data, cb)`")
|
|
82
|
+
}
|
|
83
|
+
if (!options.store.list) {
|
|
84
|
+
throw new Error("Radisk needs a streaming `store.list` interface with `(cb)`")
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
// After validation, store is guaranteed to be defined
|
|
88
|
+
const store = options.store
|
|
89
|
+
|
|
90
|
+
const perfLog = (operation: string, startTime: number, key: string, size?: number): void => {
|
|
91
|
+
const duration = Date.now() - startTime
|
|
92
|
+
if (duration > 2000) {
|
|
93
|
+
console.log(
|
|
94
|
+
`[RADISK-SLOW] ${operation}: ${duration}ms for ${key}${size ? ` (${size} bytes)` : ""}`
|
|
95
|
+
)
|
|
96
|
+
}
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
const checkMemoryUsage = (): void => {
|
|
100
|
+
if (typeof process === "undefined" || !process.memoryUsage) return
|
|
101
|
+
|
|
102
|
+
const now = Date.now()
|
|
103
|
+
if (now - lastMemoryCheck < MEMORY_CHECK_INTERVAL) return
|
|
104
|
+
lastMemoryCheck = now
|
|
105
|
+
|
|
106
|
+
const memUsage = process.memoryUsage()
|
|
107
|
+
const heapUsed = memUsage.heapUsed
|
|
108
|
+
const maxHeapSize = options.memoryLimit * 1024 * 1024
|
|
109
|
+
const heapUsageRatio = heapUsed / maxHeapSize
|
|
110
|
+
|
|
111
|
+
if (heapUsageRatio > HEAP_CLEANUP_THRESHOLD) {
|
|
112
|
+
const cacheSize = cache.size
|
|
113
|
+
console.log(
|
|
114
|
+
`[RADISK-MEMORY] High memory usage: ${Math.round(heapUsageRatio * 100)}% of heap limit. Clearing ${cacheSize} cached files.`
|
|
115
|
+
)
|
|
116
|
+
cache.clear()
|
|
117
|
+
if ((global as { gc?: () => void }).gc) {
|
|
118
|
+
(global as { gc: () => void }).gc()
|
|
119
|
+
}
|
|
120
|
+
lastMemoryCheck = now + MEMORY_CHECK_INTERVAL
|
|
121
|
+
} else if (heapUsageRatio > HEAP_WARNING_THRESHOLD) {
|
|
122
|
+
console.log(
|
|
123
|
+
`[RADISK-MEMORY] Memory warning: ${Math.round(heapUsageRatio * 100)}% of heap limit used. Cache size: ${cache.size} files.`
|
|
124
|
+
)
|
|
125
|
+
}
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
const radisk = ((key?: string, value?: GraphValue | EncodedValue | ((err?: string | null, value?: EncodedValue | Record<string, EncodedValue>) => void), cb?: (err?: string | null) => void) => {
|
|
129
|
+
if (typeof key !== "string") return undefined
|
|
130
|
+
|
|
131
|
+
if (typeof value === "function") {
|
|
132
|
+
const callback = value
|
|
133
|
+
const batchValue = radisk.batch(key)
|
|
134
|
+
if (typeof batchValue !== "undefined") {
|
|
135
|
+
return callback(undefined, batchValue as EncodedValue)
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
if (radisk.thrash.at) {
|
|
139
|
+
const thrashValue = radisk.thrash.at(key)
|
|
140
|
+
if (typeof thrashValue !== "undefined") {
|
|
141
|
+
return callback(undefined, thrashValue as EncodedValue)
|
|
142
|
+
}
|
|
143
|
+
}
|
|
144
|
+
|
|
145
|
+
if (pendingReads.has(key)) {
|
|
146
|
+
pendingReads.get(key)!.callbacks.push(callback)
|
|
147
|
+
return
|
|
148
|
+
}
|
|
149
|
+
|
|
150
|
+
const pending: {
|
|
151
|
+
callbacks: Array<(err?: string | null, value?: EncodedValue | Record<string, EncodedValue>) => void>
|
|
152
|
+
fired: boolean
|
|
153
|
+
timeoutId: NodeJS.Timeout | null
|
|
154
|
+
} = { callbacks: [callback], fired: false, timeoutId: null }
|
|
155
|
+
pendingReads.set(key, pending)
|
|
156
|
+
|
|
157
|
+
pending.timeoutId = setTimeout(() => {
|
|
158
|
+
if (!pending.fired) {
|
|
159
|
+
pending.fired = true
|
|
160
|
+
pendingReads.delete(key)
|
|
161
|
+
const err = "radisk read timeout"
|
|
162
|
+
pending.callbacks.forEach(cb => cb(err, undefined))
|
|
163
|
+
}
|
|
164
|
+
}, options.readTimeout)
|
|
165
|
+
|
|
166
|
+
const readStart = Date.now()
|
|
167
|
+
return radisk.read(key, (err, result) => {
|
|
168
|
+
if (pending.timeoutId) clearTimeout(pending.timeoutId)
|
|
169
|
+
perfLog("read", readStart, key)
|
|
170
|
+
if (!pending.fired) {
|
|
171
|
+
pending.fired = true
|
|
172
|
+
pendingReads.delete(key)
|
|
173
|
+
pending.callbacks.forEach(callback => callback(err, result))
|
|
174
|
+
}
|
|
175
|
+
})
|
|
176
|
+
}
|
|
177
|
+
|
|
178
|
+
radisk.batch(key, value as EncodedValue)
|
|
179
|
+
checkMemoryUsage()
|
|
180
|
+
if (cb) {
|
|
181
|
+
radisk.batch.acks.push(cb)
|
|
182
|
+
}
|
|
183
|
+
if (++radisk.batch.ed >= options.batch) {
|
|
184
|
+
return radisk.thrash()
|
|
185
|
+
}
|
|
186
|
+
|
|
187
|
+
clearTimeout(radisk.batch.timeout)
|
|
188
|
+
radisk.batch.timeout = setTimeout(radisk.thrash, options.write)
|
|
189
|
+
return undefined
|
|
190
|
+
}) as unknown as RadiskInterface
|
|
191
|
+
|
|
192
|
+
radisk.batch = Radix() as RadiskInterface["batch"]
|
|
193
|
+
radisk.batch.acks = []
|
|
194
|
+
radisk.batch.ed = 0
|
|
195
|
+
|
|
196
|
+
radisk.thrash = (() => {
|
|
197
|
+
if (radisk.thrash.ing) {
|
|
198
|
+
radisk.thrash.more = true
|
|
199
|
+
return
|
|
200
|
+
}
|
|
201
|
+
|
|
202
|
+
const thrashStart = Date.now()
|
|
203
|
+
clearTimeout(radisk.batch.timeout)
|
|
204
|
+
radisk.thrash.more = false
|
|
205
|
+
radisk.thrash.ing = true
|
|
206
|
+
const batch = (radisk.thrash.at = radisk.batch)
|
|
207
|
+
radisk.batch = Radix() as RadiskInterface["batch"]
|
|
208
|
+
radisk.batch.acks = []
|
|
209
|
+
radisk.batch.ed = 0
|
|
210
|
+
let i = 0
|
|
211
|
+
radisk.save(batch, err => {
|
|
212
|
+
if (++i > 1) return
|
|
213
|
+
|
|
214
|
+
perfLog("thrash", thrashStart, `batch-${batch.ed}`)
|
|
215
|
+
if (err) options.log(err)
|
|
216
|
+
batch.acks.forEach(cb => cb(err))
|
|
217
|
+
radisk.thrash.at = undefined
|
|
218
|
+
radisk.thrash.ing = false
|
|
219
|
+
if (radisk.thrash.more) radisk.thrash()
|
|
220
|
+
})
|
|
221
|
+
}) as RadiskInterface["thrash"]
|
|
222
|
+
|
|
223
|
+
radisk.save = (rad, cb) => {
|
|
224
|
+
const save = {
|
|
225
|
+
find: (_tree: unknown, key: string): boolean | undefined => {
|
|
226
|
+
if (key < (save.start || "")) return undefined
|
|
227
|
+
save.start = key
|
|
228
|
+
store.list(save.lex)
|
|
229
|
+
return true
|
|
230
|
+
},
|
|
231
|
+
lex: (file?: string): void => {
|
|
232
|
+
if (!file || file > (save.start || "")) {
|
|
233
|
+
save.end = file
|
|
234
|
+
if (save.start) save.mix(save.file || "!", save.start, save.end)
|
|
235
|
+
} else {
|
|
236
|
+
save.file = file
|
|
237
|
+
}
|
|
238
|
+
},
|
|
239
|
+
mix: (file: string, start: string, end?: string): void => {
|
|
240
|
+
save.start = save.end = save.file = undefined
|
|
241
|
+
if (cache.has(file)) {
|
|
242
|
+
const disk = cache.get(file)!
|
|
243
|
+
Radix.map(rad, (value, key) => {
|
|
244
|
+
if (key < start) return undefined
|
|
245
|
+
if (end && end < key) {
|
|
246
|
+
save.start = key
|
|
247
|
+
return undefined
|
|
248
|
+
}
|
|
249
|
+
disk(key, value)
|
|
250
|
+
return undefined
|
|
251
|
+
})
|
|
252
|
+
radisk.write(file, disk, save.next)
|
|
253
|
+
} else {
|
|
254
|
+
radisk.parse(file, (err, disk) => {
|
|
255
|
+
if (err) return cb(err)
|
|
256
|
+
Radix.map(rad, (value, key) => {
|
|
257
|
+
if (key < start) return undefined
|
|
258
|
+
if (end && end < key) {
|
|
259
|
+
save.start = key
|
|
260
|
+
return undefined
|
|
261
|
+
}
|
|
262
|
+
disk!(key, value)
|
|
263
|
+
return undefined
|
|
264
|
+
})
|
|
265
|
+
radisk.write(file, disk!, save.next)
|
|
266
|
+
})
|
|
267
|
+
}
|
|
268
|
+
},
|
|
269
|
+
next: (err?: string | null): void => {
|
|
270
|
+
if (err) return cb(err)
|
|
271
|
+
if (save.start) {
|
|
272
|
+
Radix.map(rad, save.find)
|
|
273
|
+
return
|
|
274
|
+
}
|
|
275
|
+
cb(err)
|
|
276
|
+
},
|
|
277
|
+
start: undefined as string | undefined,
|
|
278
|
+
end: undefined as string | undefined,
|
|
279
|
+
file: undefined as string | undefined,
|
|
280
|
+
}
|
|
281
|
+
Radix.map(rad, save.find)
|
|
282
|
+
}
|
|
283
|
+
|
|
284
|
+
radisk.write = (file, rad, cb) => {
|
|
285
|
+
const write = {
|
|
286
|
+
text: "",
|
|
287
|
+
limit: "",
|
|
288
|
+
done: false,
|
|
289
|
+
count: 0,
|
|
290
|
+
sub: undefined as RadixFunction | undefined,
|
|
291
|
+
each: (value: EncodedValue, _key: string, k: string, pre: string[]): void | boolean => {
|
|
292
|
+
if (write.done) return
|
|
293
|
+
write.count++
|
|
294
|
+
const valueStr =
|
|
295
|
+
typeof value === "undefined" ? "" : "=" + Radisk.encode(value)
|
|
296
|
+
const enc =
|
|
297
|
+
Radisk.encode(pre.length) + "#" + Radisk.encode(k) + valueStr + "\n"
|
|
298
|
+
|
|
299
|
+
if (
|
|
300
|
+
write.count > 1 &&
|
|
301
|
+
pre.length === 0 &&
|
|
302
|
+
write.text.length + enc.length > options.size
|
|
303
|
+
) {
|
|
304
|
+
const endIdx = k.indexOf(enq)
|
|
305
|
+
write.limit = endIdx === -1 ? k : k.substring(0, endIdx)
|
|
306
|
+
if (write.limit !== file) {
|
|
307
|
+
write.done = true
|
|
308
|
+
write.sub = Radix()
|
|
309
|
+
Radix.map(rad, write.slice)
|
|
310
|
+
radisk.write(write.limit, write.sub, cb)
|
|
311
|
+
return undefined
|
|
312
|
+
}
|
|
313
|
+
}
|
|
314
|
+
write.text += enc
|
|
315
|
+
},
|
|
316
|
+
slice: (value: EncodedValue, key: string): void => {
|
|
317
|
+
if (key < write.limit) return
|
|
318
|
+
write.sub!(key, value)
|
|
319
|
+
},
|
|
320
|
+
}
|
|
321
|
+
Radix.map(rad, write.each, true)
|
|
322
|
+
const writeStart = Date.now()
|
|
323
|
+
store.put(file, write.text, err => {
|
|
324
|
+
perfLog("file-write", writeStart, file, write.text.length)
|
|
325
|
+
cb(err)
|
|
326
|
+
})
|
|
327
|
+
}
|
|
328
|
+
|
|
329
|
+
radisk.read = (key, cb) => {
|
|
330
|
+
const endIdx = key.indexOf(enq)
|
|
331
|
+
const soul = endIdx === -1 ? key : key.substring(0, endIdx)
|
|
332
|
+
|
|
333
|
+
const read = {
|
|
334
|
+
lex: (file?: string): void => {
|
|
335
|
+
if (!file) {
|
|
336
|
+
if (!read.file) {
|
|
337
|
+
cb("no file found", undefined)
|
|
338
|
+
return
|
|
339
|
+
}
|
|
340
|
+
|
|
341
|
+
if (options.cache && cache.has(read.file)) {
|
|
342
|
+
const cachedRadix = cache.get(read.file)!
|
|
343
|
+
read.value = cachedRadix(key) as EncodedValue | Record<string, EncodedValue> | undefined
|
|
344
|
+
return cb(undefined, read.value)
|
|
345
|
+
}
|
|
346
|
+
|
|
347
|
+
radisk.parse(read.file, read.it)
|
|
348
|
+
return
|
|
349
|
+
}
|
|
350
|
+
|
|
351
|
+
if (file > soul || file < (read.file || "")) return
|
|
352
|
+
read.file = file
|
|
353
|
+
},
|
|
354
|
+
it: (err?: string | null, disk?: RadixFunction): void => {
|
|
355
|
+
if (err) options.log(err)
|
|
356
|
+
if (disk) {
|
|
357
|
+
if (options.cache) {
|
|
358
|
+
cache.set(read.file!, disk)
|
|
359
|
+
checkMemoryUsage()
|
|
360
|
+
}
|
|
361
|
+
read.value = disk(key) as EncodedValue | Record<string, EncodedValue> | undefined
|
|
362
|
+
}
|
|
363
|
+
cb(err, read.value)
|
|
364
|
+
},
|
|
365
|
+
file: undefined as string | undefined,
|
|
366
|
+
value: undefined as EncodedValue | Record<string, EncodedValue> | undefined,
|
|
367
|
+
}
|
|
368
|
+
|
|
369
|
+
const now = Date.now()
|
|
370
|
+
if (
|
|
371
|
+
options.cache &&
|
|
372
|
+
fileListCache &&
|
|
373
|
+
now - fileListCacheTime < FILE_LIST_CACHE_TTL
|
|
374
|
+
) {
|
|
375
|
+
fileListCache.forEach(file => read.lex(file))
|
|
376
|
+
read.lex()
|
|
377
|
+
} else {
|
|
378
|
+
const files: string[] = []
|
|
379
|
+
const originalLex = read.lex
|
|
380
|
+
read.lex = (file?: string): void => {
|
|
381
|
+
if (file) files.push(file)
|
|
382
|
+
return originalLex(file)
|
|
383
|
+
}
|
|
384
|
+
|
|
385
|
+
store.list((file?: string) => {
|
|
386
|
+
read.lex(file)
|
|
387
|
+
if (!file && options.cache) {
|
|
388
|
+
fileListCache = files
|
|
389
|
+
fileListCacheTime = now
|
|
390
|
+
}
|
|
391
|
+
})
|
|
392
|
+
}
|
|
393
|
+
}
|
|
394
|
+
|
|
395
|
+
radisk.parse = (file, cb) => {
|
|
396
|
+
const parse = {
|
|
397
|
+
disk: Radix(),
|
|
398
|
+
read: (err?: string | null, data?: string | EncodedValue | Record<string, unknown>): void => {
|
|
399
|
+
if (err) return cb(err)
|
|
400
|
+
if (!data) return cb(null, parse.disk)
|
|
401
|
+
|
|
402
|
+
let pre: string[] = []
|
|
403
|
+
let preString = ""
|
|
404
|
+
let tmp = parse.split(data as string)
|
|
405
|
+
while (tmp) {
|
|
406
|
+
let key: string | undefined
|
|
407
|
+
let value: EncodedValue | undefined
|
|
408
|
+
const tmpVal = tmp[1]
|
|
409
|
+
const i = typeof tmpVal === "number" ? tmpVal : (typeof tmpVal === "string" ? parseInt(tmpVal, 10) : 0)
|
|
410
|
+
tmp = parse.split(tmp[2]) || ("" as never)
|
|
411
|
+
if (tmp[0] === "#") {
|
|
412
|
+
const tmpKey = tmp[1]
|
|
413
|
+
key = typeof tmpKey === "string" ? tmpKey : String(tmpKey)
|
|
414
|
+
if (i < pre.length) {
|
|
415
|
+
pre.length = i
|
|
416
|
+
}
|
|
417
|
+
if (i <= pre.length) {
|
|
418
|
+
pre[i] = key
|
|
419
|
+
pre.length = i + 1
|
|
420
|
+
}
|
|
421
|
+
preString = pre.join("")
|
|
422
|
+
}
|
|
423
|
+
tmp = parse.split(tmp[2]) || ("" as never)
|
|
424
|
+
if (tmp[0] === "\n") continue
|
|
425
|
+
|
|
426
|
+
if (tmp[0] === "=") {
|
|
427
|
+
value = tmp[1] as EncodedValue
|
|
428
|
+
}
|
|
429
|
+
if (typeof key !== "undefined" && typeof value !== "undefined") {
|
|
430
|
+
parse.disk(preString, value)
|
|
431
|
+
}
|
|
432
|
+
tmp = parse.split(tmp[2])
|
|
433
|
+
}
|
|
434
|
+
cb(null, parse.disk)
|
|
435
|
+
},
|
|
436
|
+
split: (
|
|
437
|
+
data: string
|
|
438
|
+
): [string, unknown, string] | undefined => {
|
|
439
|
+
if (!data) return undefined
|
|
440
|
+
|
|
441
|
+
const i = data.indexOf(unit)
|
|
442
|
+
if (i === -1) return undefined
|
|
443
|
+
|
|
444
|
+
const a = data.slice(0, i)
|
|
445
|
+
const o: { i?: number } = {}
|
|
446
|
+
return [a, Radisk.decode(data.slice(i), o), data.slice(i + o.i!)]
|
|
447
|
+
},
|
|
448
|
+
}
|
|
449
|
+
store.get(file, parse.read)
|
|
450
|
+
}
|
|
451
|
+
|
|
452
|
+
// TypeScript can't verify all properties are added dynamically, but they are at runtime
|
|
453
|
+
return radisk as unknown as RadiskInterface
|
|
454
|
+
}
|
|
455
|
+
|
|
456
|
+
/**
|
|
457
|
+
* Encode value for storage
|
|
458
|
+
*/
|
|
459
|
+
Radisk.encode = (input: EncodedValue | GraphValue): string => {
|
|
460
|
+
let state = ""
|
|
461
|
+
let sig = ""
|
|
462
|
+
let data: GraphValue
|
|
463
|
+
|
|
464
|
+
if (Array.isArray(input) && (input.length === 2 || input.length === 3)) {
|
|
465
|
+
// input is EncodedValue - extract the GraphValue and metadata
|
|
466
|
+
state = etx + input[1]
|
|
467
|
+
if (input.length === 3 && input[2]) {
|
|
468
|
+
sig = etx + input[2]
|
|
469
|
+
}
|
|
470
|
+
data = input[0]!
|
|
471
|
+
} else {
|
|
472
|
+
// input is already a GraphValue
|
|
473
|
+
data = input as GraphValue
|
|
474
|
+
}
|
|
475
|
+
|
|
476
|
+
if (typeof data === "string") {
|
|
477
|
+
let i = 0
|
|
478
|
+
let current: string | undefined = undefined
|
|
479
|
+
let text = unit
|
|
480
|
+
while ((current = data[i++])) {
|
|
481
|
+
if (current === unit) text += unit
|
|
482
|
+
}
|
|
483
|
+
return text + '"' + data + state + sig + unit
|
|
484
|
+
}
|
|
485
|
+
|
|
486
|
+
const relId = utils.rel.is(data)
|
|
487
|
+
if (relId) return unit + "#" + relId + state + sig + unit
|
|
488
|
+
|
|
489
|
+
if (utils.num.is(data)) return unit + "+" + (data || 0) + state + sig + unit
|
|
490
|
+
|
|
491
|
+
if (data === true) return unit + "+" + state + sig + unit
|
|
492
|
+
|
|
493
|
+
if (data === false) return unit + "-" + state + sig + unit
|
|
494
|
+
|
|
495
|
+
if (data === null) return unit + " " + state + sig + unit
|
|
496
|
+
|
|
497
|
+
return undefined as never
|
|
498
|
+
}
|
|
499
|
+
|
|
500
|
+
/**
|
|
501
|
+
* Decode value from storage
|
|
502
|
+
*/
|
|
503
|
+
Radisk.decode = (data: string, obj?: { i?: number }): EncodedValue | GraphValue | Relation | null | undefined => {
|
|
504
|
+
let i = -1
|
|
505
|
+
let n = 0
|
|
506
|
+
let current: string | undefined = undefined
|
|
507
|
+
let previous: string | undefined = undefined
|
|
508
|
+
let textStart = -1
|
|
509
|
+
let textEnd = -1
|
|
510
|
+
if (data[0] !== unit) return undefined
|
|
511
|
+
|
|
512
|
+
while ((current = data[++i])) {
|
|
513
|
+
if (previous) {
|
|
514
|
+
if (textStart === -1) textStart = i
|
|
515
|
+
if (current === unit) {
|
|
516
|
+
if (--n <= 0) {
|
|
517
|
+
textEnd = i
|
|
518
|
+
break
|
|
519
|
+
}
|
|
520
|
+
}
|
|
521
|
+
} else if (current === unit) {
|
|
522
|
+
n++
|
|
523
|
+
} else {
|
|
524
|
+
previous = current || "true"
|
|
525
|
+
}
|
|
526
|
+
}
|
|
527
|
+
|
|
528
|
+
const text =
|
|
529
|
+
textStart !== -1 ? data.slice(textStart, textEnd !== -1 ? textEnd : i) : ""
|
|
530
|
+
|
|
531
|
+
if (obj) obj.i = i + 1
|
|
532
|
+
|
|
533
|
+
const parts = text.split(etx)
|
|
534
|
+
const value = parts[0]!
|
|
535
|
+
const state = parts[1]
|
|
536
|
+
const sig = parts[2]
|
|
537
|
+
|
|
538
|
+
if (!state) {
|
|
539
|
+
if (previous === '"') return text
|
|
540
|
+
|
|
541
|
+
if (previous === "#") return utils.rel.ify(text)
|
|
542
|
+
|
|
543
|
+
if (previous === "+") {
|
|
544
|
+
if (text.length === 0) return true
|
|
545
|
+
return parseFloat(text)
|
|
546
|
+
}
|
|
547
|
+
|
|
548
|
+
if (previous === "-") return false
|
|
549
|
+
|
|
550
|
+
if (previous === " ") return null
|
|
551
|
+
} else {
|
|
552
|
+
const stateNum = parseFloat(state)
|
|
553
|
+
if (sig) {
|
|
554
|
+
if (previous === '"') return [value, stateNum, sig]
|
|
555
|
+
|
|
556
|
+
if (previous === "#") return [utils.rel.ify(value), stateNum, sig]
|
|
557
|
+
|
|
558
|
+
if (previous === "+") {
|
|
559
|
+
if (value.length === 0) return [true, stateNum, sig]
|
|
560
|
+
return [parseFloat(value), stateNum, sig]
|
|
561
|
+
}
|
|
562
|
+
|
|
563
|
+
if (previous === "-") return [false, stateNum, sig]
|
|
564
|
+
|
|
565
|
+
if (previous === " ") return [null, stateNum, sig]
|
|
566
|
+
} else {
|
|
567
|
+
if (previous === '"') return [value, stateNum]
|
|
568
|
+
|
|
569
|
+
if (previous === "#") return [utils.rel.ify(value), stateNum]
|
|
570
|
+
|
|
571
|
+
if (previous === "+") {
|
|
572
|
+
if (value.length === 0) return [true, stateNum]
|
|
573
|
+
return [parseFloat(value), stateNum]
|
|
574
|
+
}
|
|
575
|
+
|
|
576
|
+
if (previous === "-") return [false, stateNum]
|
|
577
|
+
|
|
578
|
+
if (previous === " ") return [null, stateNum]
|
|
579
|
+
}
|
|
580
|
+
}
|
|
581
|
+
return undefined
|
|
582
|
+
}
|
|
583
|
+
|
|
584
|
+
export default Radisk
|
|
585
|
+
|