@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,968 @@
1
+ /**
2
+ * Holster - Main API for graph database operations
3
+ * Provides chainable interface for get, put, on, off operations
4
+ */
5
+
6
+ import * as utils from "./utils.ts"
7
+ import Wire, { type WireAPI } from "./wire.ts"
8
+ import User, { type UserInterface } from "./user.ts"
9
+ import SEA from "./sea.ts"
10
+ import type {
11
+ HolsterOptions,
12
+ ChainItem,
13
+ ApiContext,
14
+ Graph,
15
+ GraphValue,
16
+ Lex,
17
+ LexFilter,
18
+ LexWithDot,
19
+ UserIdentity,
20
+ WireOptions,
21
+ } from "./schemas.ts"
22
+
23
+ /**
24
+ * Holster API interface
25
+ */
26
+ export interface HolsterAPI {
27
+ get: {
28
+ (key: string | null, lex?: LexWithDot, cb?: (data: unknown) => void, _opt?: WireOptions): HolsterAPI
29
+ (
30
+ key: string | null,
31
+ cb?: (data: unknown) => void,
32
+ _opt?: WireOptions
33
+ ): HolsterAPI
34
+ }
35
+ next: {
36
+ (key: string | null, lex?: LexWithDot, cb?: (data: unknown) => void, _opt?: WireOptions): HolsterAPI
37
+ (
38
+ key: string | null,
39
+ cb?: (data: unknown) => void,
40
+ _opt?: WireOptions
41
+ ): HolsterAPI
42
+ }
43
+ put: (
44
+ data: GraphValue | Record<string, GraphValue>,
45
+ set?: boolean | ((data?: string | null) => void),
46
+ cb?: (data?: string | null) => void
47
+ ) => HolsterAPI | void
48
+ on: (
49
+ lex: LexWithDot | ((data: unknown) => void),
50
+ cb?: ((data: unknown) => void) | boolean,
51
+ _get?: boolean,
52
+ _opt?: WireOptions
53
+ ) => void
54
+ off: (cb?: (data: unknown) => void) => HolsterAPI | void
55
+ user: () => UserInterface & HolsterAPI
56
+ wire: WireAPI
57
+ SEA: typeof SEA
58
+ }
59
+
60
+ const Holster = (opt?: HolsterOptions | string | string[]): HolsterAPI => {
61
+ let options: HolsterOptions
62
+ if (typeof opt === "string") options = { peers: [opt] }
63
+ else if (opt instanceof Array) options = { peers: opt }
64
+ else if (!utils.obj.is(opt)) options = {}
65
+ else options = opt
66
+
67
+ const wire = Wire(options)
68
+ const user = User(options, wire as never)
69
+ const map = new Map<(data: unknown) => void, () => void>()
70
+ const allctx = new Map<string, ApiContext>()
71
+
72
+ const ok = (data: GraphValue): boolean => {
73
+ return (
74
+ data === null ||
75
+ data === true ||
76
+ data === false ||
77
+ typeof data === "string" ||
78
+ !!utils.rel.is(data) ||
79
+ utils.num.is(data)
80
+ )
81
+ }
82
+
83
+ const check = (
84
+ data: GraphValue | Record<string, GraphValue>
85
+ ): true | string | string[] => {
86
+ if (ok(data as GraphValue)) return true
87
+
88
+ if (utils.obj.is(data)) {
89
+ const keys: string[] = []
90
+ for (const [key, value] of Object.entries(data)) {
91
+ if (key === "_") {
92
+ return "error underscore cannot be used as a property name"
93
+ }
94
+ if (utils.obj.is(value) || ok(value as GraphValue)) {
95
+ keys.push(key)
96
+ continue
97
+ }
98
+ if (typeof value === "undefined") {
99
+ return `error undefined ${key} cannot be converted to a graph`
100
+ }
101
+ const error = JSON.stringify({ [key]: value })
102
+ return `error ${error} cannot be converted to a graph`
103
+ }
104
+ if (keys.length !== 0) return keys
105
+ }
106
+ const error = JSON.stringify(data)
107
+ return `error ${error} cannot be converted to a graph`
108
+ }
109
+
110
+ const api = (initCtxid?: string): HolsterAPI => {
111
+ let ctxid = initCtxid
112
+ const get = (
113
+ lex: LexFilter,
114
+ soul: string,
115
+ ack: (data: unknown) => void,
116
+ _opt?: WireOptions
117
+ ): void => {
118
+ wire.get(
119
+ utils.obj.put(lex as never, "#", soul) as Lex,
120
+ async msg => {
121
+ if (msg.err) console.log(msg.err)
122
+ if (msg.put && msg.put[soul]) {
123
+ delete (msg.put[soul] as { _?: unknown })._
124
+ delete (msg.put[soul] as Record<string, unknown>)[utils.userPublicKey]
125
+ delete (msg.put[soul] as Record<string, unknown>)[utils.userSignature]
126
+ const node = msg.put[soul]!
127
+ await Promise.all(Object.keys(node).map(async key => {
128
+ const id = utils.rel.is(node[key] as GraphValue)
129
+ if (!id) return
130
+ const attemptRead = async (retries = 0): Promise<unknown> => {
131
+ const data = await new Promise<unknown>(res => {
132
+ const _ctxid = utils.text.random()
133
+ const ctx = allctx.get(ctxid!)
134
+ allctx.set(_ctxid, {
135
+ chain: [{ item: null, soul: id }],
136
+ user: ctx ? ctx.user : null,
137
+ })
138
+ api(_ctxid).next(null as never, res as never, _opt)
139
+ })
140
+ if (data !== null || retries >= 5) {
141
+ return data
142
+ }
143
+ await new Promise(resolve => setTimeout(resolve, 50))
144
+ return attemptRead(retries + 1)
145
+ }
146
+ node[key] = (await attemptRead()) as GraphValue
147
+ }))
148
+ ack(msg.put[soul])
149
+ } else {
150
+ ack(null)
151
+ }
152
+ },
153
+ _opt
154
+ )
155
+ }
156
+
157
+ const graph = async (
158
+ soul: string,
159
+ data: Record<string, GraphValue>,
160
+ userctx?: UserIdentity | null,
161
+ cb?: (err?: string) => void
162
+ ): Promise<Graph | null> => {
163
+ if (userctx) {
164
+ const timestamp = Date.now()
165
+ const sig = await SEA.signTimestamp(timestamp, userctx)
166
+ return utils.graph(soul, data, sig!, userctx.pub, timestamp)
167
+ }
168
+
169
+ if (options.secure) {
170
+ if (!cb) cb = console.log
171
+ cb(`error putting data on ${soul}: user required in secure mode`)
172
+ return null
173
+ }
174
+
175
+ return utils.graph(soul, data)
176
+ }
177
+
178
+ const done = (ctxid: string) => {
179
+ return (data: unknown): void => {
180
+ const ctx = allctx.get(ctxid)
181
+ if (ctx && typeof ctx.cb !== "undefined") {
182
+ setTimeout(() => ctx.cb!(data as never), 1)
183
+ } else if (data) {
184
+ console.log("error no callback for data", data, "ctx", ctx)
185
+ }
186
+ if (ctx && !ctx.on) allctx.delete(ctxid)
187
+ }
188
+ }
189
+
190
+ const watchForRel = (
191
+ soul: string,
192
+ item: string,
193
+ ctx: ApiContext,
194
+ i: number,
195
+ request: { on?: LexFilter; get?: LexFilter; _get?: boolean; _opt?: WireOptions },
196
+ cb: (data: unknown) => void,
197
+ on = true
198
+ ): void => {
199
+ let timer: ReturnType<typeof setTimeout> | null = null
200
+ const handler = (): void => {
201
+ // Bail out if the context was removed by off().
202
+ if (!allctx.has(ctxid!)) {
203
+ wire.off({ "#": soul, ".": item }, handler)
204
+ if (timer) clearTimeout(timer)
205
+ return
206
+ }
207
+ wire.get(
208
+ { "#": soul, ".": item },
209
+ msg => {
210
+ const node = msg.put && msg.put[soul]
211
+ const id = utils.rel.is(node && node[item] as GraphValue)
212
+ if (!id) {
213
+ // If it's a plain value (not a rel) and this is a get, deliver it.
214
+ // node._ guards against timeout null-acks which lack metadata.
215
+ if (!on && node && node._ && typeof node[item] !== "undefined") {
216
+ if (timer) clearTimeout(timer)
217
+ wire.off({ "#": soul, ".": item }, handler)
218
+ cb(node[item])
219
+ }
220
+ return
221
+ }
222
+ if (timer) clearTimeout(timer)
223
+ wire.off({ "#": soul, ".": item }, handler)
224
+ ctx.chain[i]!.soul = id
225
+ allctx.set(ctxid!, { ...ctx })
226
+ if (on) {
227
+ api(ctxid).on(request.on!, cb, request._get, request._opt)
228
+ } else {
229
+ api(ctxid).next(null as never, request.get as never, cb as never, request._opt)
230
+ }
231
+ },
232
+ { ...request._opt, secure: (typeof ctx.user === "boolean" ? ctx.user : !!ctx.user) || options.secure }
233
+ )
234
+ }
235
+ wire.on({ "#": soul, ".": item }, handler, false, request._opt)
236
+ // Time out after the same total duration as the on() retry loop
237
+ // (1+2+4+8+16 = 31s) so callers are not blocked forever if the node
238
+ // genuinely doesn't exist.
239
+ if (!on) {
240
+ timer = setTimeout(() => {
241
+ wire.off({ "#": soul, ".": item }, handler)
242
+ if (cb) cb(null)
243
+ }, 31000)
244
+ }
245
+ }
246
+
247
+ const resolve = (
248
+ request: {
249
+ get?: LexFilter
250
+ put?: GraphValue | Record<string, GraphValue>
251
+ on?: LexFilter
252
+ off?: boolean
253
+ _opt?: WireOptions
254
+ _get?: boolean
255
+ },
256
+ cb?: (data: unknown) => void
257
+ ): ChainItem | false => {
258
+ if (!request) {
259
+ console.log("error resolve request parameter required")
260
+ return false
261
+ }
262
+
263
+ const get = typeof request.get !== "undefined"
264
+ const put = typeof request.put !== "undefined"
265
+ const on = typeof request.on !== "undefined"
266
+ const off = typeof request.off !== "undefined"
267
+
268
+ let found = false
269
+ const ctx = allctx.get(ctxid!)
270
+ if (!ctx) return {item: null, soul: null}
271
+ for (let i = 1; i < ctx.chain.length; i++) {
272
+ if (ctx.chain[i]!.soul !== null) continue
273
+ found = true
274
+ break
275
+ }
276
+
277
+ if (found) {
278
+ let i = 1
279
+ while (i < ctx.chain.length && ctx.chain[i]!.soul !== null) i++
280
+ const { item, soul } = ctx.chain[i - 1]!
281
+ wire.get(
282
+ { "#": soul!, ".": item! },
283
+ async msg => {
284
+ if (msg.err) {
285
+ console.log(`error getting ${item} on ${soul}: ${msg.err}`)
286
+ if (cb) cb(null)
287
+ return
288
+ }
289
+
290
+ let node = msg.put && msg.put[soul!]
291
+ if (node && typeof node[item!] !== "undefined") {
292
+ let id = utils.rel.is(node[item!] as GraphValue)
293
+ if (id) {
294
+ ctx.chain[i]!.soul = id
295
+ allctx.set(ctxid!, { ...ctx })
296
+ if (get) {
297
+ api(ctxid).next(null as never, request.get as never, cb as never, request._opt)
298
+ } else if (put) {
299
+ api(ctxid).put(request.put!, cb as never)
300
+ } else if (on) {
301
+ api(ctxid).on(request.on!, cb!, request._get, request._opt)
302
+ } else if (off) {
303
+ api(ctxid).off(cb as never)
304
+ }
305
+ } else if (get) {
306
+ cb!(node[item!])
307
+ } else if (put) {
308
+ id = utils.text.random()
309
+ node[item!] = utils.rel.ify(id)
310
+ const g = await graph(soul!, node as never, ctx.user, cb as never)
311
+ if (g === null) return
312
+
313
+ wire.put(g, err => {
314
+ if (err) {
315
+ (cb as (err: string) => void)(`error putting ${item} on ${soul}: ${err}`)
316
+ return
317
+ }
318
+ if (id) {
319
+ ctx.chain[i]!.soul = id
320
+ }
321
+ api(ctxid).put(request.put!, cb as never)
322
+ })
323
+ } else if (on) {
324
+ // Item is not a rel yet — watch the parent soul for when it
325
+ // becomes one, then retry chain resolution.
326
+ watchForRel(soul!, item!, ctx, i, request, cb!)
327
+ if (request._get) cb!(null)
328
+ } else if (off) {
329
+ if (cb) cb(null)
330
+ }
331
+ } else if (put) {
332
+ const id = utils.text.random()
333
+ if (!node) node = {} as never
334
+ node[item!] = utils.rel.ify(id)
335
+ const g = await graph(soul!, node as never, ctx.user, cb as never)
336
+ if (g === null) return
337
+
338
+ wire.put(g, err => {
339
+ if (err) {
340
+ (cb as (err: string) => void)(`error putting ${item} on ${soul}: ${err}`)
341
+ return
342
+ }
343
+ ctx.chain[i]!.soul = id
344
+ api(ctxid).put(request.put!, cb as never)
345
+ })
346
+ } else {
347
+ if (on) {
348
+ // Node doesn't exist yet — watch for it to appear.
349
+ watchForRel(soul!, item!, ctx, i, request, cb!)
350
+ if (request._get) cb!(null)
351
+ } else if (get && node) {
352
+ const sv = (node as Record<string, unknown> & {_?: {">": Record<string, unknown>}})._?.[">"]
353
+ if (sv && Object.keys(sv).length > 0 && typeof sv[item!] === "undefined") {
354
+ // State vector has other properties but not this one — never written.
355
+ if (cb) cb(null)
356
+ } else {
357
+ // Empty state vector or item is tracked — may arrive via push.
358
+ watchForRel(soul!, item!, ctx, i, request, cb!, false)
359
+ }
360
+ } else {
361
+ // Soul doesn't exist or no get — return null.
362
+ if (cb) cb(null)
363
+ }
364
+ }
365
+ },
366
+ { ...request._opt, secure: (typeof ctx.user === "boolean" ? ctx.user : !!ctx.user) || options.secure }
367
+ )
368
+ return false
369
+ }
370
+
371
+ if (get && ctx.chain[ctx.chain.length - 1]!.item !== null) {
372
+ ctx.chain.push({ item: null, soul: null })
373
+ api(ctxid).next(null as never, request.get as never, cb as never, request._opt)
374
+ return false
375
+ }
376
+
377
+ return ctx.chain[ctx.chain.length - 1]!
378
+ }
379
+
380
+ return {
381
+ get: function (
382
+ this: HolsterAPI,
383
+ key: string | null,
384
+ lex?: LexFilter | ((data: unknown) => void),
385
+ cb?: ((data: unknown) => void) | WireOptions,
386
+ _opt?: WireOptions
387
+ ): HolsterAPI {
388
+ let lexFilter: LexFilter | null | undefined
389
+ let callback: ((data: unknown) => void) | undefined
390
+ let opts: WireOptions | undefined
391
+
392
+ if (typeof lex === "function") {
393
+ opts = cb as WireOptions
394
+ callback = lex
395
+ lexFilter = null
396
+ } else {
397
+ lexFilter = lex
398
+ callback = cb as (data: unknown) => void
399
+ opts = _opt
400
+ }
401
+
402
+ if (key === null || key === "" || key === "_") {
403
+ console.log("error please provide a key")
404
+ if (callback) callback(null)
405
+ return this
406
+ }
407
+
408
+ if (lexFilter && typeof callback !== "function") {
409
+ console.log("error lex requires a callback function")
410
+ return this
411
+ }
412
+
413
+ ctxid = utils.text.random()
414
+ allctx.set(ctxid, {
415
+ chain: [{ item: String(key), soul: "root" }],
416
+ cb: callback,
417
+ })
418
+ if (!callback) return api(ctxid)
419
+
420
+ const _done = done(ctxid)
421
+ const result = resolve({ get: lexFilter, _opt: opts }, _done)
422
+ if (result) get(lexFilter as never, result.soul!, _done, opts)
423
+ return this
424
+ } as HolsterAPI["get"],
425
+
426
+ next: function (
427
+ this: HolsterAPI,
428
+ key: string | null,
429
+ lex?: LexFilter | ((data: unknown) => void),
430
+ cb?: ((data: unknown) => void) | WireOptions,
431
+ _opt?: WireOptions
432
+ ): HolsterAPI {
433
+ let lexFilter: LexFilter | null | undefined
434
+ let callback: ((data: unknown) => void) | undefined
435
+ let opts: WireOptions | undefined
436
+
437
+ if (typeof lex === "function") {
438
+ opts = cb as WireOptions
439
+ callback = lex
440
+ lexFilter = null
441
+ } else {
442
+ lexFilter = lex
443
+ callback = cb as (data: unknown) => void
444
+ opts = _opt
445
+ }
446
+
447
+ const ack = (ctxid: string) => {
448
+ return (data: unknown): void => {
449
+ if (callback) {
450
+ callback(data)
451
+ } else {
452
+ done(ctxid)(data)
453
+ }
454
+ }
455
+ }
456
+
457
+ if (!ctxid) {
458
+ console.log("error please provide a key using get(key)")
459
+ if (callback) callback(null)
460
+ return this
461
+ }
462
+
463
+ const _ack = ack(ctxid)
464
+ if (key === "" || key === "_") {
465
+ _ack(null)
466
+ return this
467
+ }
468
+
469
+ if (lexFilter && typeof callback !== "function") {
470
+ console.log("error lex requires a callback function")
471
+ return this
472
+ }
473
+
474
+ const ctx = allctx.get(ctxid)
475
+ if (!ctx) return this
476
+
477
+ if (callback && typeof ctx.cb === "undefined") {
478
+ ctx.cb = callback
479
+ callback = undefined
480
+ }
481
+
482
+ if (key !== null) ctx.chain.push({ item: String(key), soul: null })
483
+ if (!ctx.cb) return api(ctxid)
484
+
485
+ const result = resolve({ get: lexFilter, _opt: opts }, _ack)
486
+ if (result) get(lexFilter as never, result.soul!, _ack, opts)
487
+ return this
488
+ } as HolsterAPI["next"],
489
+
490
+ put: function (
491
+ data: GraphValue | Record<string, GraphValue>,
492
+ set?: boolean | ((data?: string | null) => void),
493
+ cb?: (data?: string | null) => void
494
+ ): HolsterAPI | void {
495
+ let isSet = false
496
+ let callback: ((data?: string | null) => void) | undefined
497
+
498
+ if (typeof set === "function") {
499
+ callback = set
500
+ isSet = false
501
+ } else {
502
+ isSet = set || false
503
+ callback = cb
504
+ }
505
+
506
+ const ack = (ctxid: string) => {
507
+ return (data?: string | null): void => {
508
+ if (callback) {
509
+ callback(data)
510
+ } else {
511
+ done(ctxid)(data)
512
+ }
513
+ }
514
+ }
515
+
516
+ if (!ctxid) {
517
+ if (callback) callback("error please provide a key using get(key)")
518
+ return
519
+ }
520
+
521
+ const ctx = allctx.get(ctxid!)
522
+ if (!ctx) return
523
+
524
+ if (!ctx.cb) {
525
+ if (!callback) return
526
+ ctx.cb = callback as never
527
+ callback = undefined
528
+ }
529
+
530
+ if (isSet) data = { [utils.text.random()]: data } as Record<string, GraphValue>
531
+
532
+ const _ack = ack(ctxid)
533
+ const result = check(data)
534
+ if (typeof result === "string") {
535
+ _ack(result)
536
+ return
537
+ }
538
+
539
+ const resolved = resolve({ put: data, _opt: {put: true} }, _ack as never)
540
+ if (!resolved) return
541
+
542
+ const { item, soul } = resolved
543
+ if (!soul) return
544
+
545
+ if (result === true) {
546
+ wire.get(
547
+ { "#": soul, ".": item! },
548
+ async msg => {
549
+ if (msg.err) {
550
+ _ack(`error getting ${soul}: ${msg.err}`)
551
+ return
552
+ }
553
+
554
+ let node = msg.put && msg.put[soul]
555
+ const current = node && node[item!]
556
+ const id = utils.rel.is(current as GraphValue)
557
+ if (!id) {
558
+ if (!node) node = {} as never
559
+ node[item!] = (isSet ? data : data) as GraphValue
560
+ const g = await graph(soul, node as never, ctx.user, _ack as never)
561
+ if (g === null) return
562
+
563
+ wire.put(g, _ack as never)
564
+ return
565
+ }
566
+
567
+ wire.get({ "#": id }, async msg => {
568
+ if (msg.err) {
569
+ _ack(`error getting ${id}: ${msg.err}`)
570
+ return
571
+ }
572
+
573
+ if (!msg.put || !msg.put[id]) {
574
+ _ack(`error ${id} not found`)
575
+ return
576
+ }
577
+
578
+ for (const key of Object.keys(msg.put[id]!)) {
579
+ if (
580
+ key === "_" ||
581
+ key === utils.userPublicKey ||
582
+ key === utils.userSignature
583
+ ) {
584
+ continue
585
+ }
586
+
587
+ const err = await new Promise<string | null | undefined>(res => {
588
+ const _ctxid = utils.text.random()
589
+ const chain: ChainItem[] = [{ item: key, soul: id }]
590
+ allctx.set(_ctxid, { chain: chain, user: ctx.user })
591
+ api(_ctxid).put(null!, res as never)
592
+ })
593
+ if (err) {
594
+ _ack(err)
595
+ return
596
+ }
597
+ }
598
+ if (!node) node = {} as never
599
+ node[item!] = data as GraphValue
600
+ const g = await graph(soul, node as never, ctx.user, _ack as never)
601
+ if (g === null) return
602
+
603
+ wire.put(g, _ack as never)
604
+ })
605
+ },
606
+ { secure: (typeof ctx.user === "boolean" ? ctx.user : !!ctx.user) || options.secure, put: true }
607
+ )
608
+ return
609
+ }
610
+
611
+ const attemptRead = (retries = 0): void => {
612
+ wire.get(
613
+ { "#": soul, ".": item! },
614
+ async msg => {
615
+ if (msg.err) {
616
+ _ack(`error getting ${soul}.${item}: ${msg.err}`)
617
+ return
618
+ }
619
+
620
+ let node = msg.put && msg.put[soul]
621
+ const current = node && node[item!]
622
+
623
+ if (!current && retries < 5) {
624
+ await new Promise(r => setTimeout(r, 50))
625
+ return attemptRead(retries + 1)
626
+ }
627
+
628
+ const id = utils.rel.is(current as GraphValue)
629
+ if (!id) {
630
+ if (!node) node = {} as never
631
+ node[item!] = utils.rel.ify(utils.text.random())
632
+ const g = await graph(soul, node as never, ctx.user, _ack as never)
633
+ if (g === null) return
634
+
635
+ wire.put(g, err => {
636
+ if (err) {
637
+ _ack(`error putting ${item} on ${soul}: ${err}`)
638
+ } else {
639
+ const _ctxid = utils.text.random()
640
+ const chain: ChainItem[] = [{ item: item!, soul: soul }]
641
+ allctx.set(_ctxid, {
642
+ chain: chain,
643
+ user: ctx.user,
644
+ cb: ctx.cb,
645
+ })
646
+ api(_ctxid).put(data)
647
+ }
648
+ })
649
+ return
650
+ }
651
+
652
+ const update: string[] = []
653
+ for (const key of result as string[]) {
654
+ const err = await new Promise<string | null | undefined>(res => {
655
+ if (
656
+ utils.obj.is((data as Record<string, unknown>)[key]) &&
657
+ !utils.rel.is((data as Record<string, GraphValue>)[key])
658
+ ) {
659
+ const _ctxid = utils.text.random()
660
+ const chain: ChainItem[] = [{ item: key, soul: id }]
661
+ allctx.set(_ctxid, { chain: chain, user: ctx.user })
662
+ api(_ctxid).put((data as Record<string, GraphValue>)[key]!, res as never)
663
+ } else {
664
+ update.push(key)
665
+ res(null)
666
+ }
667
+ })
668
+ if (err) {
669
+ _ack(err)
670
+ return
671
+ }
672
+ }
673
+
674
+ if (update.length === 0) {
675
+ _ack(null)
676
+ return
677
+ }
678
+
679
+ // Only write the primitive update keys. Rels created by the
680
+ // recursive puts above are already in the in-memory graph.
681
+ const updateNode: Record<string, GraphValue> = {}
682
+ update.forEach(key => {
683
+ updateNode[key] = (data as Record<string, GraphValue>)[key]!
684
+ })
685
+ const g = await graph(id, updateNode as never, ctx.user, _ack as never)
686
+ if (g === null) return
687
+
688
+ wire.put(g, _ack as never)
689
+ },
690
+ { secure: (typeof ctx.user === "boolean" ? ctx.user : !!ctx.user) || options.secure, put: true }
691
+ )
692
+ }
693
+ attemptRead()
694
+ },
695
+
696
+ on: function (
697
+ lex: LexWithDot | ((data: unknown) => void),
698
+ cb?: ((data: unknown) => void) | boolean,
699
+ _get?: boolean,
700
+ _opt?: WireOptions
701
+ ): void {
702
+ let lexFilter: LexFilter | undefined
703
+ let callback: (data: unknown) => void
704
+ let opts: WireOptions | undefined
705
+
706
+ if (typeof lex === "function") {
707
+ opts = _get as never
708
+ _get = cb as never
709
+ callback = lex
710
+ lexFilter = null
711
+ } else {
712
+ lexFilter = lex as LexFilter
713
+ callback = (typeof cb === "function" ? cb : undefined)!
714
+ opts = _opt
715
+ }
716
+
717
+ if (typeof callback !== "function") {
718
+ console.log("error on() requires a callback function")
719
+ return
720
+ }
721
+
722
+ if (!ctxid) {
723
+ console.log("error please provide a key using get(key)")
724
+ callback(null)
725
+ return
726
+ }
727
+
728
+ const resolved = resolve({ on: lexFilter, _get: _get, _opt: opts }, callback)
729
+ if (!resolved) return
730
+
731
+ const { item, soul } = resolved
732
+ if (!soul) return
733
+
734
+ const ctx = allctx.get(ctxid)
735
+
736
+ allctx.set(ctxid, {
737
+ chain: [{ item: item, soul: soul }],
738
+ on: true,
739
+ user: ctx ? ctx.user : null,
740
+ })
741
+
742
+ // Shared retry state for this on() subscription.
743
+ let retryCount = 0
744
+ const maxRetries = 5
745
+ const retryDelay = 1000
746
+ let retryTimer: ReturnType<typeof setTimeout> | null = null
747
+
748
+ map.set(callback, () => {
749
+ // Bail out if off() has already cleaned up this context — the mapped
750
+ // callback can fire twice (once from wire.on and once from the _get
751
+ // immediate-read path) and the second call after async context cleanup
752
+ // would crash in resolve(). Matches the guard in watchForRel.
753
+ if (!allctx.has(ctxid!)) return
754
+ // Cancel any pending retry — the persistent listener firing means we
755
+ // have an update and should not call callback twice.
756
+ clearTimeout(retryTimer!)
757
+ retryTimer = null
758
+ wire.get(
759
+ { "#": soul, ".": item! },
760
+ msg => {
761
+ const node = msg.put && msg.put[soul]
762
+ const current = node && node[item!]
763
+ const id = utils.rel.is(current as GraphValue)
764
+ if (id) {
765
+ const attemptRead = async (retries = 0): Promise<void> => {
766
+ const data = await new Promise<unknown>(res => {
767
+ const _ctxid = utils.text.random()
768
+ allctx.set(_ctxid, {
769
+ chain: [{ item: null, soul: id }],
770
+ user: ctx ? ctx.user : null,
771
+ })
772
+ api(_ctxid).next(null as never, res as never, (retries === 0 ? utils.obj.put(opts, "fast", true) : opts) as never)
773
+ })
774
+ if (data !== null || retries >= 5) {
775
+ retryCount = 0
776
+ callback(data)
777
+ } else {
778
+ await new Promise(resolve => setTimeout(resolve, 50))
779
+ attemptRead(retries + 1)
780
+ }
781
+ }
782
+ attemptRead()
783
+ } else {
784
+ const nodeExists = !!node
785
+ if (nodeExists) {
786
+ retryCount = 0
787
+ callback(current !== undefined ? current : null)
788
+ } else if (retryCount < maxRetries) {
789
+ // No data yet — keep polling until we get a response or
790
+ // exhaust retries. on() with _get can return null on a cache
791
+ // miss and never fire again if the data doesn't change while
792
+ // the listener is attached, so we must poll for initial load.
793
+ const secureOpt = { ...opts, secure: ctx ? ((typeof ctx.user === "boolean" ? ctx.user : !!ctx.user) || options.secure) : options.secure }
794
+ const retry = (): void => {
795
+ const delay = Math.min(retryDelay * Math.pow(2, retryCount), 30000)
796
+ retryCount++
797
+ retryTimer = setTimeout(() => {
798
+ wire.get(
799
+ { "#": soul, ".": item! },
800
+ retryMsg => {
801
+ const retryNode = retryMsg.put && retryMsg.put[soul]
802
+ if (retryNode) {
803
+ retryCount = 0
804
+ retryTimer = null
805
+ const retryValue = retryNode[item!]
806
+ callback(retryValue !== undefined ? retryValue : null)
807
+ } else if (retryCount < maxRetries) {
808
+ retry()
809
+ } else {
810
+ retryTimer = null
811
+ callback(null)
812
+ }
813
+ },
814
+ secureOpt,
815
+ )
816
+ }, delay)
817
+ }
818
+ retry()
819
+ } else {
820
+ callback(null)
821
+ }
822
+ }
823
+ },
824
+ { ...opts, secure: ctx ? ((typeof ctx.user === "boolean" ? ctx.user : !!ctx.user) || options.secure) : options.secure }
825
+ )
826
+ })
827
+
828
+ let initialLex: { "#": string; "."?: string | number } = lexFilter
829
+ ? (utils.obj.put(lexFilter as never, "#", soul) as { "#": string; "."?: string | number })
830
+ : { "#": soul, ".": item! }
831
+ if (initialLex["."] != null && typeof initialLex["."] === "number") {
832
+ initialLex = { ...initialLex, ".": String(initialLex["."]) }
833
+ }
834
+ wire.on(initialLex as never, map.get(callback)!, false, opts)
835
+
836
+ wire.get(
837
+ { "#": soul, ".": item! },
838
+ msg => {
839
+ if (msg.err) {
840
+ console.log(`error getting ${soul}.${item}: ${msg.err}`)
841
+ return
842
+ }
843
+
844
+ const current = msg.put && msg.put[soul] && msg.put[soul]![item!]
845
+ const id = utils.rel.is(current as GraphValue)
846
+ if (id) {
847
+ wire.off(initialLex as never, map.get(callback)!)
848
+ const relLex = (lexFilter
849
+ ? utils.obj.put(initialLex as never, "#", id)
850
+ : { "#": id, ".": null }) as Lex
851
+ wire.on(relLex, map.get(callback)!, false, opts)
852
+ if (_get) map.get(callback)!()
853
+ } else if (_get) {
854
+ // Not a rel, but _get was requested, so trigger callback.
855
+ map.get(callback)!()
856
+ }
857
+ },
858
+ { ...opts, secure: ctx ? ((typeof ctx.user === "boolean" ? ctx.user : !!ctx.user) || options.secure) : options.secure }
859
+ )
860
+ },
861
+
862
+ off: function (cb?: (data: unknown) => void): HolsterAPI | void {
863
+ if (!ctxid) {
864
+ console.log("error please provide a key using get(key)")
865
+ if (cb) cb(null)
866
+ return
867
+ }
868
+
869
+ const resolved = resolve({ off: true }, cb)
870
+ if (!resolved) return
871
+
872
+ const { item, soul } = resolved
873
+ if (!soul) return
874
+
875
+ wire.off({ "#": soul } as never, map.get(cb!)!)
876
+
877
+ wire.get({ "#": soul, ".": item! }, msg => {
878
+ if (msg.err) {
879
+ map.delete(cb!)
880
+ allctx.delete(ctxid!)
881
+ return
882
+ }
883
+
884
+ const current = msg.put && msg.put[soul] && msg.put[soul]![item!]
885
+ const id = utils.rel.is(current as GraphValue)
886
+ if (id) wire.off({ "#": id } as never, map.get(cb!)!)
887
+ map.delete(cb!)
888
+ allctx.delete(ctxid!)
889
+ })
890
+ },
891
+
892
+ user: () => {
893
+ if (!(user as { get?: unknown }).get) {
894
+ Object.assign(user, api())
895
+ ;(user as UserInterface & HolsterAPI).get = function (
896
+ this: HolsterAPI,
897
+ keys: string | string[],
898
+ lex?: LexFilter | ((data: unknown) => void),
899
+ cb?: ((data: unknown) => void) | WireOptions,
900
+ _opt?: WireOptions
901
+ ): HolsterAPI {
902
+ let lexFilter: LexFilter | null | undefined
903
+ let callback: ((data: unknown) => void) | undefined
904
+ let opts: WireOptions | undefined
905
+
906
+ if (typeof lex === "function") {
907
+ opts = cb as WireOptions
908
+ callback = lex
909
+ lexFilter = null
910
+ } else {
911
+ lexFilter = lex
912
+ callback = cb as (data: unknown) => void
913
+ opts = _opt
914
+ }
915
+
916
+ let pub: string | null = null
917
+ let key: string | null = null
918
+ if (user.is) pub = user.is.pub
919
+ if (typeof keys === "string") {
920
+ key = keys
921
+ } else if (keys instanceof Array) {
922
+ if (keys.length === 2 && keys[0] && keys[1]) {
923
+ pub = keys[0]
924
+ key = keys[1]
925
+ } else if (keys.length === 1 && keys[0]) {
926
+ key = keys[0]
927
+ }
928
+ }
929
+ if (!pub) {
930
+ console.log("error please log in or provide a public key")
931
+ if (callback) callback(null)
932
+ return this
933
+ }
934
+
935
+ if (key === null || key === "" || key === "_") {
936
+ console.log("error please provide a key")
937
+ if (callback) callback(null)
938
+ return this
939
+ }
940
+
941
+ if (lexFilter && !callback) {
942
+ console.log("error lex requires a callback function")
943
+ return this
944
+ }
945
+
946
+ ctxid = utils.text.random()
947
+ const chain: ChainItem[] = [{ item: String(key), soul: "~" + pub }]
948
+ allctx.set(ctxid, { chain: chain, user: user.is, cb: callback as never })
949
+ if (!callback) return api(ctxid)
950
+
951
+ const _done = done(ctxid)
952
+ const resolved = resolve({ get: lexFilter, _opt: opts }, _done)
953
+ if (resolved) get(lexFilter as never, resolved.soul!, _done, opts)
954
+ return this
955
+ } as never
956
+ }
957
+ return user as UserInterface & HolsterAPI
958
+ },
959
+
960
+ wire: wire,
961
+ SEA: SEA,
962
+ }
963
+ }
964
+ return api()
965
+ }
966
+
967
+ export default Holster
968
+