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