@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/utils.ts
ADDED
|
@@ -0,0 +1,232 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Utility functions for Holster
|
|
3
|
+
* Provides helpers for type checking, graph manipulation, and pattern matching
|
|
4
|
+
*/
|
|
5
|
+
|
|
6
|
+
import type { Soul, Relation, Graph, GraphNode, LexFilter, GraphValue } from "./schemas.ts"
|
|
7
|
+
|
|
8
|
+
// ============================================================================
|
|
9
|
+
// Number Utilities
|
|
10
|
+
// ============================================================================
|
|
11
|
+
|
|
12
|
+
export const num = {
|
|
13
|
+
/**
|
|
14
|
+
* Check if value is a valid number
|
|
15
|
+
*/
|
|
16
|
+
is: (n: unknown): n is number => {
|
|
17
|
+
if (n instanceof Array) return false
|
|
18
|
+
if (typeof n === "number") return !isNaN(n)
|
|
19
|
+
if (typeof n === "string") {
|
|
20
|
+
const parsed = parseFloat(n)
|
|
21
|
+
return !isNaN(parsed) && isFinite(parsed)
|
|
22
|
+
}
|
|
23
|
+
return false
|
|
24
|
+
},
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
// ============================================================================
|
|
28
|
+
// Object Utilities
|
|
29
|
+
// ============================================================================
|
|
30
|
+
|
|
31
|
+
type PlainObject = Record<string, unknown>
|
|
32
|
+
|
|
33
|
+
export const obj = {
|
|
34
|
+
/**
|
|
35
|
+
* Check if value is a plain object
|
|
36
|
+
*/
|
|
37
|
+
is: (o: unknown): o is PlainObject => {
|
|
38
|
+
if (!o || typeof o !== "object") return false
|
|
39
|
+
const toString = Object.prototype.toString.call(o)
|
|
40
|
+
const match = toString.match(/^\[object (\w+)\]$/)
|
|
41
|
+
return (
|
|
42
|
+
(o as Record<string, unknown>).constructor === Object ||
|
|
43
|
+
(match !== null && match[1] === "Object")
|
|
44
|
+
)
|
|
45
|
+
},
|
|
46
|
+
|
|
47
|
+
/**
|
|
48
|
+
* Map over object keys with early return support
|
|
49
|
+
*/
|
|
50
|
+
map: <T, R>(
|
|
51
|
+
list: Record<string, T>,
|
|
52
|
+
cb: (value: T, key: string, o?: Record<string, unknown>) => R | undefined,
|
|
53
|
+
o?: Record<string, unknown>
|
|
54
|
+
): R | undefined => {
|
|
55
|
+
const keys = Object.keys(list)
|
|
56
|
+
for (let i = 0; i < keys.length; i++) {
|
|
57
|
+
const key = keys[i]!
|
|
58
|
+
const result = cb(list[key]!, key, o)
|
|
59
|
+
if (typeof result !== "undefined") return result
|
|
60
|
+
}
|
|
61
|
+
return undefined
|
|
62
|
+
},
|
|
63
|
+
|
|
64
|
+
/**
|
|
65
|
+
* Set a property on an object
|
|
66
|
+
*/
|
|
67
|
+
put: (o: Record<string, unknown> | undefined, key: string, value: unknown): Record<string, unknown> => {
|
|
68
|
+
const target = o || {}
|
|
69
|
+
target[key] = value
|
|
70
|
+
return target
|
|
71
|
+
},
|
|
72
|
+
|
|
73
|
+
/**
|
|
74
|
+
* Delete a property from an object
|
|
75
|
+
*/
|
|
76
|
+
del: <T>(o: Record<string, T> | undefined, key: string): Record<string, T> | undefined => {
|
|
77
|
+
if (!o) return undefined
|
|
78
|
+
o[key] = null as T
|
|
79
|
+
delete o[key]
|
|
80
|
+
return o
|
|
81
|
+
},
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
// ============================================================================
|
|
85
|
+
// Relation Utilities
|
|
86
|
+
// ============================================================================
|
|
87
|
+
|
|
88
|
+
const map_soul = (soul: unknown, key: string, o: { id: string | false }): void => {
|
|
89
|
+
// If id is already defined AND we're still looping through the object,
|
|
90
|
+
// then it is considered invalid
|
|
91
|
+
if (o.id) {
|
|
92
|
+
o.id = false
|
|
93
|
+
return
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
if (key === "#" && typeof soul === "string") {
|
|
97
|
+
o.id = soul
|
|
98
|
+
return
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
// If there exists anything else on the object that isn't the soul,
|
|
102
|
+
// then it is considered invalid
|
|
103
|
+
o.id = false
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
export const rel = {
|
|
107
|
+
/**
|
|
108
|
+
* Check if value is a soul relation and return the soul ID
|
|
109
|
+
*/
|
|
110
|
+
is: (value: unknown): string | false => {
|
|
111
|
+
if (
|
|
112
|
+
value &&
|
|
113
|
+
typeof value === "object" &&
|
|
114
|
+
"#" in value &&
|
|
115
|
+
!("_" in value) &&
|
|
116
|
+
obj.is(value)
|
|
117
|
+
) {
|
|
118
|
+
const o: { id: string | false } = { id: "" }
|
|
119
|
+
obj.map(value as Record<string, unknown>, map_soul as (value: unknown, key: string, o?: Record<string, unknown>) => void, o)
|
|
120
|
+
if (o.id) return o.id
|
|
121
|
+
}
|
|
122
|
+
return false
|
|
123
|
+
},
|
|
124
|
+
|
|
125
|
+
/**
|
|
126
|
+
* Convert a soul into a relation and return it
|
|
127
|
+
*/
|
|
128
|
+
ify: (soul: Soul): Relation => obj.put({}, "#", soul) as Relation,
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
// ============================================================================
|
|
132
|
+
// User Metadata Keys
|
|
133
|
+
// ============================================================================
|
|
134
|
+
|
|
135
|
+
export const userSignature = "_holster_user_signature"
|
|
136
|
+
export const userPublicKey = "_holster_user_public_key"
|
|
137
|
+
|
|
138
|
+
// ============================================================================
|
|
139
|
+
// Graph Construction
|
|
140
|
+
// ============================================================================
|
|
141
|
+
|
|
142
|
+
/**
|
|
143
|
+
* Create a graph from soul and data with optional signature metadata
|
|
144
|
+
*/
|
|
145
|
+
export const graph = (
|
|
146
|
+
soul: Soul,
|
|
147
|
+
data: Record<string, GraphValue>,
|
|
148
|
+
sig?: string,
|
|
149
|
+
pub?: string,
|
|
150
|
+
timestamp?: number
|
|
151
|
+
): Graph => {
|
|
152
|
+
const ts = timestamp || Date.now()
|
|
153
|
+
const stateVector: Record<string, number> = {}
|
|
154
|
+
const node: GraphNode = {
|
|
155
|
+
_: { "#": soul, ">": stateVector },
|
|
156
|
+
}
|
|
157
|
+
|
|
158
|
+
for (const [key, value] of Object.entries(data)) {
|
|
159
|
+
if (key !== "_" && key !== userPublicKey && key !== userSignature) {
|
|
160
|
+
node[key] = value
|
|
161
|
+
stateVector[key] = ts
|
|
162
|
+
}
|
|
163
|
+
}
|
|
164
|
+
|
|
165
|
+
if (sig && pub && timestamp) {
|
|
166
|
+
if (!node._["s"]) {
|
|
167
|
+
node._["s"] = {}
|
|
168
|
+
}
|
|
169
|
+
node._["s"][timestamp.toString()] = sig
|
|
170
|
+
node[userPublicKey] = pub
|
|
171
|
+
stateVector[userPublicKey] = timestamp
|
|
172
|
+
}
|
|
173
|
+
|
|
174
|
+
return { [soul]: node }
|
|
175
|
+
}
|
|
176
|
+
|
|
177
|
+
// ============================================================================
|
|
178
|
+
// Pattern Matching
|
|
179
|
+
// ============================================================================
|
|
180
|
+
|
|
181
|
+
/**
|
|
182
|
+
* Match a lex filter against a key
|
|
183
|
+
*/
|
|
184
|
+
export const match = (lex: LexFilter, key?: string): boolean => {
|
|
185
|
+
// Null is used to match listeners on souls, which don't provide a key
|
|
186
|
+
if (typeof key === "undefined") return lex === null
|
|
187
|
+
|
|
188
|
+
if (typeof lex === "undefined") return true
|
|
189
|
+
|
|
190
|
+
if (typeof lex === "string") return lex === key
|
|
191
|
+
|
|
192
|
+
// Array support: check if key is in the array
|
|
193
|
+
if (Array.isArray(lex)) return lex.includes(key)
|
|
194
|
+
|
|
195
|
+
if (!obj.is(lex) || !key) return false
|
|
196
|
+
|
|
197
|
+
// Prefix match
|
|
198
|
+
if ("*" in lex) {
|
|
199
|
+
const prefix = lex["*"] as string
|
|
200
|
+
return key.slice(0, prefix.length) === prefix
|
|
201
|
+
}
|
|
202
|
+
|
|
203
|
+
// Range match
|
|
204
|
+
const gt = ">" in lex ? (lex[">"] as string) : undefined
|
|
205
|
+
const lt = "<" in lex ? (lex["<"] as string) : undefined
|
|
206
|
+
|
|
207
|
+
if (gt && lt) return key >= gt && key <= lt
|
|
208
|
+
if (gt) return key >= gt
|
|
209
|
+
if (lt) return key <= lt
|
|
210
|
+
|
|
211
|
+
return false
|
|
212
|
+
}
|
|
213
|
+
|
|
214
|
+
// ============================================================================
|
|
215
|
+
// Text Utilities
|
|
216
|
+
// ============================================================================
|
|
217
|
+
|
|
218
|
+
export const text = {
|
|
219
|
+
/**
|
|
220
|
+
* Generate a random string of specified length
|
|
221
|
+
*/
|
|
222
|
+
random: (length?: number): string => {
|
|
223
|
+
let s = ""
|
|
224
|
+
const c = "0123456789ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz"
|
|
225
|
+
const len = length || 24
|
|
226
|
+
for (let i = 0; i < len; i++) {
|
|
227
|
+
s += c.charAt(Math.floor(Math.random() * c.length))
|
|
228
|
+
}
|
|
229
|
+
return s
|
|
230
|
+
},
|
|
231
|
+
}
|
|
232
|
+
|