@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.
Files changed (67) 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 +23 -5
  6. package/src/get.js +6 -18
  7. package/src/ham.js +17 -47
  8. package/src/holster.js +106 -28
  9. package/src/sea.js +0 -52
  10. package/src/store.js +7 -7
  11. package/src/wire.js +7 -1
  12. package/ts/src/array.ts +78 -0
  13. package/ts/src/buffer.ts +441 -0
  14. package/ts/src/dup.ts +49 -0
  15. package/ts/src/get.ts +83 -0
  16. package/ts/src/ham.ts +254 -0
  17. package/ts/src/holster.ts +931 -0
  18. package/ts/src/index.ts +35 -0
  19. package/ts/src/radisk.ts +585 -0
  20. package/ts/src/radix.ts +157 -0
  21. package/ts/src/schemas.ts +519 -0
  22. package/ts/src/sea-utils.ts +103 -0
  23. package/ts/src/sea.ts +388 -0
  24. package/ts/src/store.ts +321 -0
  25. package/ts/src/user.ts +381 -0
  26. package/ts/src/utils.ts +232 -0
  27. package/ts/src/wire.ts +829 -0
  28. package/.dockerignore +0 -9
  29. package/.github/workflows/node.js.yml +0 -31
  30. package/.github/workflows/npm-publish.yml +0 -33
  31. package/Dockerfile +0 -20
  32. package/examples/index.html +0 -33
  33. package/examples/index.js +0 -49
  34. package/examples/user.html +0 -31
  35. package/examples/user.js +0 -93
  36. package/examples/wire.html +0 -27
  37. package/examples/wire.js +0 -55
  38. package/test/holster.get.test.js +0 -94
  39. package/test/holster.lex.test.js +0 -196
  40. package/test/holster.off.test.js +0 -53
  41. package/test/holster.on.test.js +0 -264
  42. package/test/holster.put.test.js +0 -358
  43. package/test/holster.secure.test.js +0 -202
  44. package/test/holster.user.get.test.js +0 -114
  45. package/test/holster.user.lex.test.js +0 -149
  46. package/test/holster.user.off.test.js +0 -73
  47. package/test/holster.user.on.test.js +0 -300
  48. package/test/holster.user.put.test.js +0 -463
  49. package/test/kitty/! +0 -9
  50. package/test/names.js +0 -225
  51. package/test/radisk.cache.test.js +0 -213
  52. package/test/radisk.test.js +0 -331
  53. package/test/radix.test.js +0 -117
  54. package/test/sea.test.js +0 -284
  55. package/test/split.test.js +0 -317
  56. package/test/store.test.js +0 -316
  57. package/test/system/basic.test.js +0 -29
  58. package/test/system/chained-on.test.js +0 -41
  59. package/test/system/concurrent-listeners.test.js +0 -125
  60. package/test/system/immediate-read.test.js +0 -30
  61. package/test/system/listener-before-data.test.js +0 -51
  62. package/test/system/promise-wrapper.test.js +0 -47
  63. package/test/system/user-chained-on.test.js +0 -60
  64. package/test/system/user-concurrent-listeners.test.js +0 -142
  65. package/test/system/user-listener-before-data.test.js +0 -66
  66. package/test/user.test.js +0 -173
  67. package/test/wire.test.js +0 -158
@@ -0,0 +1,157 @@
1
+ /**
2
+ * Radix - Radix tree implementation for efficient key-value storage
3
+ * Supports prefix-based key compression
4
+ */
5
+
6
+ import * as utils from "./utils.ts"
7
+ import type { EncodedValue, RadixFunction } from "./schemas.ts"
8
+
9
+ // ASCII character for group separator
10
+ const group = String.fromCharCode(29)
11
+ // ASCII character for record separator
12
+ const record = String.fromCharCode(30)
13
+
14
+ // Local type for internal radix tree structure (different from schemas.RadixValue)
15
+ // This represents the internal node structure with dynamic keys
16
+ interface RadixValue {
17
+ [key: string]: RadixTree | EncodedValue | undefined
18
+ }
19
+
20
+ // RadixTree is the internal tree structure (also known as RadixNode in schemas)
21
+ type RadixTree = Record<string, RadixValue>
22
+ // Alias for compatibility with schema types
23
+ type RadixNode = RadixTree
24
+
25
+ // Type guard to check if a value is a RadixTree (object)
26
+ const isRadixTree = (value: unknown): value is RadixTree => {
27
+ return typeof value === "object" && value !== null && !Array.isArray(value)
28
+ }
29
+
30
+ /**
31
+ * Create a new Radix tree
32
+ */
33
+ const Radix = (): RadixFunction => {
34
+ const radix = ((
35
+ keys?: string,
36
+ value?: EncodedValue,
37
+ tree?: RadixTree
38
+ ): RadixNode | EncodedValue | undefined | Record<string, EncodedValue> => {
39
+ if (!tree) {
40
+ if (!(radix as Record<string, unknown>)[group]) (radix as Record<string, unknown>)[group] = {}
41
+ tree = (radix as Record<string, RadixTree>)[group]!
42
+ }
43
+ if (!keys) return tree
44
+
45
+ let i = 0
46
+ let tmp: Record<string, RadixValue> = {}
47
+ let key = keys[i]!
48
+ const max = keys.length - 1
49
+ const noValue = typeof value === "undefined"
50
+
51
+ // Find a matching value using the shortest string from keys
52
+ let found: RadixValue | undefined = tree![key]
53
+ while (!found && i < max) {
54
+ key += keys[++i]
55
+ found = tree![key]
56
+ }
57
+
58
+ if (!found) {
59
+ // If not found from the provided keys try matching with an existing key
60
+ const result = utils.obj.map(tree!, (hasValue: RadixValue, hasKey: string) => {
61
+ let j = 0
62
+ let matchingKey = ""
63
+ while (hasKey[j] === keys[j]) {
64
+ matchingKey += hasKey[j]
65
+ j++
66
+ }
67
+ if (matchingKey) {
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
74
+ }
75
+
76
+ const replace: RadixTree = {
77
+ [hasKey.slice(j)]: hasValue,
78
+ [keys.slice(j)]: { [record]: value! },
79
+ }
80
+ tree![matchingKey] = { [group]: replace }
81
+ delete tree![hasKey]
82
+ return true
83
+ }
84
+ return undefined
85
+ })
86
+
87
+ if (!result) {
88
+ if (noValue) return undefined
89
+
90
+ if (!tree![key]) tree![key] = {}
91
+ tree![key]![record] = value!
92
+ } else if (noValue) {
93
+ return tmp
94
+ }
95
+ } else if (i === max) {
96
+ // If no value use the key provided to return a whole group or record
97
+ if (noValue) {
98
+ // If an individual record isn't found then return the whole group
99
+ return typeof found[record] === "undefined" ? found[group] : found[record]
100
+ }
101
+ // Otherwise create a new record at the provided key for value
102
+ found[record] = value!
103
+ } else {
104
+ // Found at a shorter key, try again
105
+ if (!found[group] && !noValue) found[group] = {}
106
+ const nextTree = found[group]
107
+ if (isRadixTree(nextTree)) {
108
+ return radix(keys.slice(++i), value, nextTree as RadixTree) as RadixNode
109
+ }
110
+ }
111
+
112
+ return undefined
113
+ }) as RadixFunction
114
+
115
+ return radix
116
+ }
117
+
118
+ /**
119
+ * Map over all values in a radix tree
120
+ */
121
+ Radix.map = function map(
122
+ radix: RadixFunction | RadixNode,
123
+ cb: (value: EncodedValue, fullKey: string, key: string, pre: string[]) => unknown,
124
+ opt?: boolean,
125
+ pre: string[] = []
126
+ ): unknown {
127
+ const tree = ((radix as Record<string, RadixTree>)[group] || radix) as RadixTree
128
+ const keys = Object.keys(tree).sort()
129
+
130
+ for (let i = 0; i < keys.length; i++) {
131
+ const key = keys[i]!
132
+ const found = tree[key]
133
+ if (!found) continue
134
+
135
+ const recordValue = found[record]
136
+
137
+ if (typeof recordValue !== "undefined") {
138
+ const result = cb(recordValue as EncodedValue, pre.join("") + key, key, pre)
139
+ if (typeof result !== "undefined") return result
140
+ } else if (opt) {
141
+ cb(undefined as never, pre.join(""), key, pre)
142
+ }
143
+
144
+ const groupValue = found[group]
145
+ if (isRadixTree(groupValue)) {
146
+ pre.push(key)
147
+ const result = map(groupValue, cb, opt, pre)
148
+ if (typeof result !== "undefined") return result
149
+ pre.pop()
150
+ }
151
+ }
152
+
153
+ return undefined
154
+ }
155
+
156
+ export default Radix
157
+
@@ -0,0 +1,519 @@
1
+ import { z } from "zod"
2
+
3
+ // ============================================================================
4
+ // Core Primitive Types
5
+ // ============================================================================
6
+
7
+ /**
8
+ * Soul identifier - unique identifier for graph nodes
9
+ * Examples: "root", "~pubkey", "~@username", random UUIDs
10
+ */
11
+ export const SoulSchema = z.string().min(1)
12
+ export type Soul = z.infer<typeof SoulSchema>
13
+
14
+ /**
15
+ * Timestamp for state vector (HAM conflict resolution)
16
+ */
17
+ export const TimestampSchema = z.number().int().nonnegative()
18
+ export type Timestamp = z.infer<typeof TimestampSchema>
19
+
20
+ /**
21
+ * Base64 encoded string (for signatures, encrypted data, etc)
22
+ */
23
+ export const Base64Schema = z.string()
24
+ export type Base64String = z.infer<typeof Base64Schema>
25
+
26
+ // ============================================================================
27
+ // Graph Data Structures
28
+ // ============================================================================
29
+
30
+ /**
31
+ * Relation - reference to another node in the graph
32
+ * Format: {"#": "soul-id"}
33
+ */
34
+ export const RelationSchema = z.object({
35
+ "#": SoulSchema,
36
+ })
37
+ export type Relation = z.infer<typeof RelationSchema>
38
+
39
+ /**
40
+ * Valid graph values - primitives, null, relations, or nested objects
41
+ * Note: GraphValue is recursive to allow nested objects (verified in src/holster.js check() function)
42
+ */
43
+ export const GraphValueSchema: z.ZodType<GraphValue> = z.lazy(() =>
44
+ z.union([
45
+ z.string(),
46
+ z.number(),
47
+ z.boolean(),
48
+ z.null(),
49
+ RelationSchema,
50
+ z.record(z.string(), GraphValueSchema),
51
+ ])
52
+ )
53
+
54
+ export type GraphValue =
55
+ | string
56
+ | number
57
+ | boolean
58
+ | null
59
+ | Relation
60
+ | { [key: string]: GraphValue }
61
+
62
+ /**
63
+ * State vector - tracks timestamps for each property
64
+ */
65
+ export const StateVectorSchema = z.record(z.string(), TimestampSchema)
66
+ export type StateVector = z.infer<typeof StateVectorSchema>
67
+
68
+ /**
69
+ * Signature map - maps timestamps or property names to signatures
70
+ */
71
+ export const SignatureMapSchema = z.record(z.string(), Base64Schema)
72
+ export type SignatureMap = z.infer<typeof SignatureMapSchema>
73
+
74
+ /**
75
+ * Graph metadata attached to each node
76
+ */
77
+ export const GraphMetadataSchema = z.object({
78
+ "#": SoulSchema,
79
+ ">": StateVectorSchema,
80
+ s: SignatureMapSchema.optional(),
81
+ })
82
+ export type GraphMetadata = z.infer<typeof GraphMetadataSchema>
83
+
84
+ /**
85
+ * User metadata keys
86
+ */
87
+ export const USER_PUBLIC_KEY = "_holster_user_public_key"
88
+ export const USER_SIGNATURE = "_holster_user_signature"
89
+
90
+ /**
91
+ * Graph node - contains metadata and properties
92
+ * Note: Using manual type to properly handle the index signature with special _ key
93
+ */
94
+ export const GraphNodeSchema = z.object({
95
+ _: GraphMetadataSchema,
96
+ }).and(z.record(z.string(), GraphValueSchema))
97
+
98
+ export type GraphNode = {
99
+ _: GraphMetadata
100
+ [key: string]: GraphValue | GraphMetadata
101
+ }
102
+
103
+ /**
104
+ * Graph - collection of nodes indexed by soul
105
+ */
106
+ export const GraphSchema = z.record(SoulSchema, GraphNodeSchema)
107
+ export type Graph = z.infer<typeof GraphSchema>
108
+
109
+ // ============================================================================
110
+ // Query/Lex Structures
111
+ // ============================================================================
112
+
113
+ /**
114
+ * Prefix filter for lex queries
115
+ */
116
+ export const PrefixFilterSchema = z.object({
117
+ "*": z.string(),
118
+ })
119
+ export type PrefixFilter = z.infer<typeof PrefixFilterSchema>
120
+
121
+ /**
122
+ * Range filter for lex queries
123
+ */
124
+ export const RangeFilterSchema = z.object({
125
+ ">": z.string().optional(),
126
+ "<": z.string().optional(),
127
+ })
128
+ export type RangeFilter = z.infer<typeof RangeFilterSchema>
129
+
130
+ /**
131
+ * Lex filter - supports various query patterns
132
+ */
133
+ export const LexFilterSchema = z.union([
134
+ z.string(),
135
+ z.array(z.string()),
136
+ PrefixFilterSchema,
137
+ RangeFilterSchema,
138
+ z.null(),
139
+ z.undefined(),
140
+ ])
141
+ export type LexFilter = z.infer<typeof LexFilterSchema>
142
+
143
+ // LexFilter with optional "." property for nested queries
144
+ export type LexWithDot =
145
+ | LexFilter
146
+ | { ".": LexFilter }
147
+
148
+ /**
149
+ * Lex query object
150
+ */
151
+ export const LexSchema = z.object({
152
+ "#": SoulSchema,
153
+ ".": LexFilterSchema.optional(),
154
+ })
155
+ export type Lex = z.infer<typeof LexSchema>
156
+
157
+ // ============================================================================
158
+ // Wire Protocol Messages
159
+ // ============================================================================
160
+
161
+ /**
162
+ * Wire message ID (for deduplication)
163
+ */
164
+ export const MessageIdSchema = z.string()
165
+ export type MessageId = z.infer<typeof MessageIdSchema>
166
+
167
+ /**
168
+ * Wire message - protocol for network communication
169
+ */
170
+ export const WireMessageSchema = z.object({
171
+ "#": MessageIdSchema.optional(),
172
+ "@": MessageIdSchema.optional(),
173
+ get: LexSchema.optional(),
174
+ put: GraphSchema.optional(),
175
+ err: z.string().optional(),
176
+ throttle: z.number().optional(),
177
+ })
178
+ export type WireMessage = z.infer<typeof WireMessageSchema>
179
+
180
+ /**
181
+ * Wire options for get/put operations
182
+ */
183
+ export const WireOptionsSchema = z
184
+ .object({
185
+ secure: z.boolean().optional(),
186
+ fast: z.boolean().optional(),
187
+ wait: z.number().optional(),
188
+ })
189
+ .optional()
190
+ export type WireOptions = z.infer<typeof WireOptionsSchema>
191
+
192
+ // ============================================================================
193
+ // Crypto/Security Structures
194
+ // ============================================================================
195
+
196
+ /**
197
+ * ECDSA + ECDH key pair for user
198
+ */
199
+ export const UserPairSchema = z.object({
200
+ pub: z.string(), // Public key for signing (ECDSA)
201
+ priv: z.string(), // Private key for signing (ECDSA)
202
+ epub: z.string(), // Public key for encryption (ECDH)
203
+ epriv: z.string(), // Private key for encryption (ECDH)
204
+ })
205
+ export type UserPair = z.infer<typeof UserPairSchema>
206
+
207
+ /**
208
+ * User identity (authenticated user state)
209
+ */
210
+ export const UserIdentitySchema = z.object({
211
+ username: z.string(),
212
+ pub: z.string(),
213
+ epub: z.string(),
214
+ priv: z.string(),
215
+ epriv: z.string(),
216
+ })
217
+ export type UserIdentity = z.infer<typeof UserIdentitySchema>
218
+
219
+ /**
220
+ * Encrypted data structure
221
+ */
222
+ export const EncryptedDataSchema = z.object({
223
+ ct: Base64Schema, // Ciphertext
224
+ iv: Base64Schema, // Initialization vector
225
+ s: Base64Schema, // Salt
226
+ })
227
+ export type EncryptedData = z.infer<typeof EncryptedDataSchema>
228
+
229
+ /**
230
+ * Signed data structure
231
+ */
232
+ export const SignedDataSchema = z.object({
233
+ m: z.union([z.string(), z.record(z.string(), z.unknown())]), // Message
234
+ s: Base64Schema, // Signature
235
+ })
236
+ export type SignedData = z.infer<typeof SignedDataSchema>
237
+
238
+ /**
239
+ * User authentication data
240
+ */
241
+ export const UserAuthSchema = z.object({
242
+ enc: EncryptedDataSchema,
243
+ salt: z.string(),
244
+ })
245
+ export type UserAuth = z.infer<typeof UserAuthSchema>
246
+
247
+ /**
248
+ * JWK (JSON Web Key) format
249
+ */
250
+ export const JWKSchema = z.object({
251
+ kty: z.string(),
252
+ crv: z.string().optional(),
253
+ x: z.string().optional(),
254
+ y: z.string().optional(),
255
+ d: z.string().optional(),
256
+ k: z.string().optional(),
257
+ ext: z.boolean(),
258
+ key_ops: z.array(z.string()).optional(),
259
+ alg: z.string().optional(),
260
+ })
261
+ export type JWK = z.infer<typeof JWKSchema>
262
+
263
+ // ============================================================================
264
+ // Storage Structures
265
+ // ============================================================================
266
+
267
+ /**
268
+ * Encoded value for storage: [value, timestamp] or [value, timestamp, signature]
269
+ */
270
+ export const EncodedValueSchema = z.union([
271
+ z.tuple([GraphValueSchema, TimestampSchema]),
272
+ z.tuple([GraphValueSchema, TimestampSchema, Base64Schema]),
273
+ ])
274
+
275
+ // Manual type definition for better tuple handling
276
+ export type EncodedValue =
277
+ | [GraphValue, Timestamp]
278
+ | [GraphValue, Timestamp, Base64String]
279
+
280
+ /**
281
+ * Store interface callbacks
282
+ */
283
+ export const StoreCallbackSchema = z.function()
284
+
285
+ /**
286
+ * File system interface for storage
287
+ */
288
+ export const FileSystemInterfaceSchema = z.object({
289
+ get: z.function(),
290
+ put: z.function(),
291
+ list: z.function(),
292
+ })
293
+
294
+ // FileSystemInterface: Flexible to support both string-based and EncodedValue storage
295
+ // In-memory stores use strings, disk stores use EncodedValue/RadixNode
296
+ export interface FileSystemInterface {
297
+ get: (file: string, cb: (err?: string | null, data?: string | EncodedValue | Record<string, unknown>) => void) => void
298
+ put: (file: string, data: string | EncodedValue | Record<string, unknown>, cb: (err?: string | null) => void) => void
299
+ list: (cb: (file?: string) => void) => void
300
+ }
301
+
302
+ /**
303
+ * Store options
304
+ */
305
+ export const StoreOptionsSchema = z.object({
306
+ file: z.string().optional(),
307
+ store: FileSystemInterfaceSchema.optional(),
308
+ indexedDB: z.boolean().optional(),
309
+ secure: z.boolean().optional(),
310
+ })
311
+ export type StoreOptions = z.infer<typeof StoreOptionsSchema>
312
+
313
+ /**
314
+ * Radisk options
315
+ */
316
+ export const RadiskOptionsSchema = z.object({
317
+ log: z.function().optional(),
318
+ batch: z.number().optional(),
319
+ write: z.number().optional(),
320
+ size: z.number().optional(),
321
+ memoryLimit: z.number().optional(),
322
+ readTimeout: z.number().optional(),
323
+ cache: z.boolean().optional(),
324
+ store: FileSystemInterfaceSchema.optional(),
325
+ })
326
+
327
+ export interface RadiskOptions {
328
+ log?: (...args: unknown[]) => void
329
+ batch?: number
330
+ write?: number
331
+ size?: number
332
+ memoryLimit?: number
333
+ readTimeout?: number
334
+ cache?: boolean
335
+ store?: FileSystemInterface
336
+ }
337
+
338
+ // ============================================================================
339
+ // HAM (Conflict Resolution) Structures
340
+ // ============================================================================
341
+
342
+ /**
343
+ * HAM comparison result
344
+ */
345
+ export const HamResultSchema = z.object({
346
+ incoming: z.boolean().optional(),
347
+ current: z.boolean().optional(),
348
+ historical: z.boolean().optional(),
349
+ state: z.boolean().optional(),
350
+ })
351
+ export type HamResult = z.infer<typeof HamResultSchema>
352
+
353
+ /**
354
+ * HAM mix result - result of merging changes into graph
355
+ */
356
+ export const HamMixResultSchema = z.object({
357
+ now: GraphSchema,
358
+ defer: GraphSchema,
359
+ wait: z.number(),
360
+ listeners: z.array(z.function()),
361
+ })
362
+ export type HamMixResult = z.infer<typeof HamMixResultSchema>
363
+
364
+ /**
365
+ * Listen callback map for event listeners
366
+ */
367
+ export const ListenerSchema = z.object({
368
+ ".": LexFilterSchema.optional(),
369
+ cb: z.function(),
370
+ })
371
+ export type Listener = z.infer<typeof ListenerSchema>
372
+
373
+ export const ListenMapSchema = z.record(SoulSchema, z.array(ListenerSchema))
374
+ export type ListenMap = z.infer<typeof ListenMapSchema>
375
+
376
+ // ============================================================================
377
+ // Holster API Structures
378
+ // ============================================================================
379
+
380
+ /**
381
+ * Context chain for chained API calls
382
+ */
383
+ export const ChainItemSchema = z.object({
384
+ item: z.string().nullable(),
385
+ soul: z.string().nullable(),
386
+ })
387
+ export type ChainItem = z.infer<typeof ChainItemSchema>
388
+
389
+ export const ApiContextSchema = z.object({
390
+ chain: z.array(ChainItemSchema),
391
+ cb: z.function().optional(),
392
+ on: z.boolean().optional(),
393
+ user: UserIdentitySchema.nullable().optional(),
394
+ })
395
+ export type ApiContext = z.infer<typeof ApiContextSchema>
396
+
397
+ /**
398
+ * Holster options
399
+ */
400
+ export const HolsterOptionsSchema = z
401
+ .object({
402
+ peers: z.union([z.string(), z.array(z.string())]).optional(),
403
+ secure: z.boolean().optional(),
404
+ port: z.number().optional(),
405
+ server: z.any().optional(),
406
+ wss: z.any().optional(),
407
+ maxAge: z.number().optional(),
408
+ maxConnections: z.number().optional(),
409
+ maxMessageSize: z.number().optional(),
410
+ maxQueueLength: z.number().optional(),
411
+ wait: z.number().optional(),
412
+ })
413
+ .passthrough()
414
+ export type HolsterOptions = z.infer<typeof HolsterOptionsSchema>
415
+
416
+ // ============================================================================
417
+ // Utility Type Guards
418
+ // ============================================================================
419
+
420
+ /**
421
+ * Check if value is a relation
422
+ */
423
+ export function isRelation(value: unknown): value is Relation {
424
+ return RelationSchema.safeParse(value).success
425
+ }
426
+
427
+ /**
428
+ * Check if value is a valid graph value
429
+ */
430
+ export function isGraphValue(value: unknown): value is GraphValue {
431
+ return GraphValueSchema.safeParse(value).success
432
+ }
433
+
434
+ /**
435
+ * Check if object is a valid graph node
436
+ */
437
+ export function isGraphNode(value: unknown): value is GraphNode {
438
+ return GraphNodeSchema.safeParse(value).success
439
+ }
440
+
441
+ /**
442
+ * Check if object is a valid lex query
443
+ */
444
+ export function isLex(value: unknown): value is Lex {
445
+ return LexSchema.safeParse(value).success
446
+ }
447
+
448
+ // ============================================================================
449
+ // Radix Tree Structures
450
+ // ============================================================================
451
+
452
+ /**
453
+ * Radix tree node (recursive structure)
454
+ */
455
+ export type RadixNode = {
456
+ [key: string]: RadixValue | undefined
457
+ }
458
+
459
+ // RadixValue can be GraphValue (raw), EncodedValue (tuple), or RadixNode (nested tree)
460
+ // This matches src/radix.js behavior - radix stores ANY value type
461
+ export type RadixValue = GraphValue | EncodedValue | RadixNode
462
+
463
+ /**
464
+ * Radix function type - Single Source of Truth for Radix tree operations
465
+ * Supports both synchronous get/put and async callback forms
466
+ */
467
+ export type RadixFunction = {
468
+ // Main call signature: synchronous get/put operations
469
+ (keys?: string, value?: GraphValue | EncodedValue, tree?: RadixNode):
470
+ RadixNode | EncodedValue | undefined | Record<string, EncodedValue>
471
+
472
+ // Callback form: async operations
473
+ (key?: string, value?: RadixValue, cb?: (err?: string) => void): void
474
+
475
+ // Static map method for iterating over radix tree values
476
+ map: (
477
+ radix: RadixFunction | RadixNode,
478
+ cb: (value: EncodedValue, fullKey: string, key: string, pre: string[]) => unknown,
479
+ opt?: boolean,
480
+ pre?: string[]
481
+ ) => unknown
482
+
483
+ // Dynamic properties for internal state (e.g., group separator key)
484
+ [key: string]: unknown
485
+ }
486
+
487
+ // ============================================================================
488
+ // Dup (Duplicate Tracking) Structures
489
+ // ============================================================================
490
+
491
+ /**
492
+ * Dup store for tracking message IDs
493
+ */
494
+ export const DupStoreSchema = z.record(z.string(), z.number())
495
+ export type DupStore = z.infer<typeof DupStoreSchema>
496
+
497
+ export const DupSchema = z.object({
498
+ store: DupStoreSchema,
499
+ check: z.function(),
500
+ track: z.function(),
501
+ expiry: z.any().optional(),
502
+ })
503
+ export type Dup = z.infer<typeof DupSchema> & {
504
+ check: (id: string) => string | false
505
+ track: (id: string) => string
506
+ }
507
+
508
+ // ============================================================================
509
+ // Re-exports from modules
510
+ // ============================================================================
511
+
512
+ // Export RadiskInterface from radisk module for test usage
513
+ export type { RadiskInterface } from "./radisk.ts"
514
+
515
+ // Export other interfaces for test usage
516
+ export type { WireAPI as WireInterface } from "./wire.ts"
517
+ export type { UserInterface } from "./user.ts"
518
+ export type { StoreInterface } from "./store.ts"
519
+