@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/radix.ts
ADDED
|
@@ -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
|
+
|