@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,931 @@
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
+ for (const key of Object.keys(msg.put[soul]!)) {
127
+ const id = utils.rel.is(msg.put[soul]![key] as GraphValue)
128
+ if (id) {
129
+ const attemptRead = async (retries = 0): Promise<unknown> => {
130
+ const data = await new Promise<unknown>(res => {
131
+ const _ctxid = utils.text.random()
132
+ const ctx = allctx.get(ctxid!)
133
+ allctx.set(_ctxid, {
134
+ chain: [{ item: null, soul: id }],
135
+ user: ctx ? ctx.user : null,
136
+ })
137
+ api(_ctxid).next(null as never, res as never, _opt)
138
+ })
139
+ if (data !== null || retries >= 5) {
140
+ return data
141
+ }
142
+ await new Promise(resolve => setTimeout(resolve, 50))
143
+ return attemptRead(retries + 1)
144
+ }
145
+ msg.put[soul]![key] = (await attemptRead()) as GraphValue
146
+ }
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?: boolean; _opt?: WireOptions },
196
+ cb: (data: unknown) => void
197
+ ): void => {
198
+ const handler = (): void => {
199
+ // Bail out if the context was removed by off().
200
+ if (!allctx.has(ctxid!)) {
201
+ wire.off({ "#": soul, ".": item }, handler)
202
+ return
203
+ }
204
+ wire.get(
205
+ { "#": soul, ".": item },
206
+ msg => {
207
+ const node = msg.put && msg.put[soul]
208
+ const id = utils.rel.is(node && node[item] as GraphValue)
209
+ if (!id) return
210
+ wire.off({ "#": soul, ".": item }, handler)
211
+ ctx.chain[i]!.soul = id
212
+ allctx.set(ctxid!, { ...ctx })
213
+ api(ctxid).on(request.on!, cb, request._get, request._opt)
214
+ },
215
+ { ...request._opt, secure: (typeof ctx.user === "boolean" ? ctx.user : !!ctx.user) || options.secure }
216
+ )
217
+ }
218
+ wire.on({ "#": soul, ".": item }, handler, false, request._opt)
219
+ }
220
+
221
+ const resolve = (
222
+ request: {
223
+ get?: LexFilter
224
+ put?: GraphValue | Record<string, GraphValue>
225
+ on?: LexFilter
226
+ off?: boolean
227
+ _opt?: WireOptions
228
+ _get?: boolean
229
+ },
230
+ cb?: (data: unknown) => void
231
+ ): ChainItem | false => {
232
+ if (!request) {
233
+ console.log("error resolve request parameter required")
234
+ return false
235
+ }
236
+
237
+ const get = typeof request.get !== "undefined"
238
+ const put = typeof request.put !== "undefined"
239
+ const on = typeof request.on !== "undefined"
240
+ const off = typeof request.off !== "undefined"
241
+
242
+ let found = false
243
+ const ctx = allctx.get(ctxid!)!
244
+ for (let i = 1; i < ctx.chain.length; i++) {
245
+ if (ctx.chain[i]!.soul !== null) continue
246
+ found = true
247
+ break
248
+ }
249
+
250
+ if (found) {
251
+ let i = 1
252
+ while (i < ctx.chain.length && ctx.chain[i]!.soul !== null) i++
253
+ const { item, soul } = ctx.chain[i - 1]!
254
+ wire.get(
255
+ { "#": soul!, ".": item! },
256
+ async msg => {
257
+ if (msg.err) {
258
+ console.log(`error getting ${item} on ${soul}: ${msg.err}`)
259
+ if (cb) cb(null)
260
+ return
261
+ }
262
+
263
+ let node = msg.put && msg.put[soul!]
264
+ if (node && typeof node[item!] !== "undefined") {
265
+ let id = utils.rel.is(node[item!] as GraphValue)
266
+ if (id) {
267
+ ctx.chain[i]!.soul = id
268
+ allctx.set(ctxid!, { ...ctx })
269
+ if (get) {
270
+ api(ctxid).next(null as never, request.get as never, cb as never, request._opt)
271
+ } else if (put) {
272
+ api(ctxid).put(request.put!, cb as never)
273
+ } else if (on) {
274
+ api(ctxid).on(request.on!, cb!, request._get, request._opt)
275
+ } else if (off) {
276
+ api(ctxid).off(cb as never)
277
+ }
278
+ } else if (get) {
279
+ cb!(node[item!])
280
+ } else if (put) {
281
+ id = utils.text.random()
282
+ node[item!] = utils.rel.ify(id)
283
+ const g = await graph(soul!, node as never, ctx.user, cb as never)
284
+ if (g === null) return
285
+
286
+ wire.put(g, err => {
287
+ if (err) {
288
+ (cb as (err: string) => void)(`error putting ${item} on ${soul}: ${err}`)
289
+ return
290
+ }
291
+ if (id) {
292
+ ctx.chain[i]!.soul = id
293
+ }
294
+ api(ctxid).put(request.put!, cb as never)
295
+ })
296
+ } else if (on) {
297
+ // Item is not a rel yet — watch the parent soul for when it
298
+ // becomes one, then retry chain resolution.
299
+ watchForRel(soul!, item!, ctx, i, request, cb!)
300
+ if (request._get) cb!(null)
301
+ } else if (off) {
302
+ if (cb) cb(null)
303
+ }
304
+ } else if (put) {
305
+ const id = utils.text.random()
306
+ if (!node) node = {} as never
307
+ node[item!] = utils.rel.ify(id)
308
+ const g = await graph(soul!, node as never, ctx.user, cb as never)
309
+ if (g === null) return
310
+
311
+ wire.put(g, err => {
312
+ if (err) {
313
+ (cb as (err: string) => void)(`error putting ${item} on ${soul}: ${err}`)
314
+ return
315
+ }
316
+ ctx.chain[i]!.soul = id
317
+ api(ctxid).put(request.put!, cb as never)
318
+ })
319
+ } else {
320
+ if (on) {
321
+ // Node doesn't exist yet — watch for it to appear.
322
+ watchForRel(soul!, item!, ctx, i, request, cb!)
323
+ if (request._get) cb!(null)
324
+ } else {
325
+ if (cb) cb(null)
326
+ }
327
+ }
328
+ },
329
+ { ...request._opt, secure: (typeof ctx.user === "boolean" ? ctx.user : !!ctx.user) || options.secure }
330
+ )
331
+ return false
332
+ }
333
+
334
+ if (get && ctx.chain[ctx.chain.length - 1]!.item !== null) {
335
+ ctx.chain.push({ item: null, soul: null })
336
+ api(ctxid).next(null as never, request.get as never, cb as never, request._opt)
337
+ return false
338
+ }
339
+
340
+ return ctx.chain[ctx.chain.length - 1]!
341
+ }
342
+
343
+ return {
344
+ get: function (
345
+ this: HolsterAPI,
346
+ key: string | null,
347
+ lex?: LexFilter | ((data: unknown) => void),
348
+ cb?: ((data: unknown) => void) | WireOptions,
349
+ _opt?: WireOptions
350
+ ): HolsterAPI {
351
+ let lexFilter: LexFilter | null | undefined
352
+ let callback: ((data: unknown) => void) | undefined
353
+ let opts: WireOptions | undefined
354
+
355
+ if (typeof lex === "function") {
356
+ opts = cb as WireOptions
357
+ callback = lex
358
+ lexFilter = null
359
+ } else {
360
+ lexFilter = lex
361
+ callback = cb as (data: unknown) => void
362
+ opts = _opt
363
+ }
364
+
365
+ if (key === null || key === "" || key === "_") {
366
+ console.log("error please provide a key")
367
+ if (callback) callback(null)
368
+ return this
369
+ }
370
+
371
+ if (lexFilter && typeof callback !== "function") {
372
+ console.log("error lex requires a callback function")
373
+ return this
374
+ }
375
+
376
+ ctxid = utils.text.random()
377
+ allctx.set(ctxid, {
378
+ chain: [{ item: String(key), soul: "root" }],
379
+ cb: callback,
380
+ })
381
+ if (!callback) return api(ctxid)
382
+
383
+ const _done = done(ctxid)
384
+ const result = resolve({ get: lexFilter, _opt: opts }, _done)
385
+ if (result) get(lexFilter as never, result.soul!, _done, opts)
386
+ return this
387
+ } as HolsterAPI["get"],
388
+
389
+ next: function (
390
+ this: HolsterAPI,
391
+ key: string | null,
392
+ lex?: LexFilter | ((data: unknown) => void),
393
+ cb?: ((data: unknown) => void) | WireOptions,
394
+ _opt?: WireOptions
395
+ ): HolsterAPI {
396
+ let lexFilter: LexFilter | null | undefined
397
+ let callback: ((data: unknown) => void) | undefined
398
+ let opts: WireOptions | undefined
399
+
400
+ if (typeof lex === "function") {
401
+ opts = cb as WireOptions
402
+ callback = lex
403
+ lexFilter = null
404
+ } else {
405
+ lexFilter = lex
406
+ callback = cb as (data: unknown) => void
407
+ opts = _opt
408
+ }
409
+
410
+ const ack = (ctxid: string) => {
411
+ return (data: unknown): void => {
412
+ if (callback) {
413
+ callback(data)
414
+ } else {
415
+ done(ctxid)(data)
416
+ }
417
+ }
418
+ }
419
+
420
+ if (!ctxid) {
421
+ console.log("error please provide a key using get(key)")
422
+ if (callback) callback(null)
423
+ return this
424
+ }
425
+
426
+ const _ack = ack(ctxid)
427
+ if (key === "" || key === "_") {
428
+ _ack(null)
429
+ return this
430
+ }
431
+
432
+ if (lexFilter && typeof callback !== "function") {
433
+ console.log("error lex requires a callback function")
434
+ return this
435
+ }
436
+
437
+ const ctx = allctx.get(ctxid)
438
+ if (!ctx) return this
439
+
440
+ if (callback && typeof ctx.cb === "undefined") {
441
+ ctx.cb = callback
442
+ callback = undefined
443
+ }
444
+
445
+ if (key !== null) ctx.chain.push({ item: String(key), soul: null })
446
+ if (!ctx.cb) return api(ctxid)
447
+
448
+ const result = resolve({ get: lexFilter, _opt: opts }, _ack)
449
+ if (result) get(lexFilter as never, result.soul!, _ack, opts)
450
+ return this
451
+ } as HolsterAPI["next"],
452
+
453
+ put: function (
454
+ data: GraphValue | Record<string, GraphValue>,
455
+ set?: boolean | ((data?: string | null) => void),
456
+ cb?: (data?: string | null) => void
457
+ ): HolsterAPI | void {
458
+ let isSet = false
459
+ let callback: ((data?: string | null) => void) | undefined
460
+
461
+ if (typeof set === "function") {
462
+ callback = set
463
+ isSet = false
464
+ } else {
465
+ isSet = set || false
466
+ callback = cb
467
+ }
468
+
469
+ const ack = (ctxid: string) => {
470
+ return (data?: string | null): void => {
471
+ if (callback) {
472
+ callback(data)
473
+ } else {
474
+ done(ctxid)(data)
475
+ }
476
+ }
477
+ }
478
+
479
+ if (!ctxid) {
480
+ if (callback) callback("error please provide a key using get(key)")
481
+ return
482
+ }
483
+
484
+ const ctx = allctx.get(ctxid!)
485
+ if (!ctx) return
486
+
487
+ if (!ctx.cb) {
488
+ if (!callback) return
489
+ ctx.cb = callback as never
490
+ callback = undefined
491
+ }
492
+
493
+ if (isSet) data = { [utils.text.random()]: data } as Record<string, GraphValue>
494
+
495
+ const _ack = ack(ctxid)
496
+ const result = check(data)
497
+ if (typeof result === "string") {
498
+ _ack(result)
499
+ return
500
+ }
501
+
502
+ const resolved = resolve({ put: data }, _ack as never)
503
+ if (!resolved) return
504
+
505
+ const { item, soul } = resolved
506
+ if (!soul) return
507
+
508
+ if (result === true) {
509
+ wire.get(
510
+ { "#": soul, ".": item! },
511
+ async msg => {
512
+ if (msg.err) {
513
+ _ack(`error getting ${soul}: ${msg.err}`)
514
+ return
515
+ }
516
+
517
+ let node = msg.put && msg.put[soul]
518
+ const current = node && node[item!]
519
+ const id = utils.rel.is(current as GraphValue)
520
+ if (!id) {
521
+ if (!node) node = {} as never
522
+ node[item!] = (isSet ? data : data) as GraphValue
523
+ const g = await graph(soul, node as never, ctx.user, _ack as never)
524
+ if (g === null) return
525
+
526
+ wire.put(g, _ack as never)
527
+ return
528
+ }
529
+
530
+ wire.get({ "#": id }, async msg => {
531
+ if (msg.err) {
532
+ _ack(`error getting ${id}: ${msg.err}`)
533
+ return
534
+ }
535
+
536
+ if (!msg.put || !msg.put[id]) {
537
+ _ack(`error ${id} not found`)
538
+ return
539
+ }
540
+
541
+ for (const key of Object.keys(msg.put[id]!)) {
542
+ if (
543
+ key === "_" ||
544
+ key === utils.userPublicKey ||
545
+ key === utils.userSignature
546
+ ) {
547
+ continue
548
+ }
549
+
550
+ const err = await new Promise<string | null | undefined>(res => {
551
+ const _ctxid = utils.text.random()
552
+ const chain: ChainItem[] = [{ item: key, soul: id }]
553
+ allctx.set(_ctxid, { chain: chain, user: ctx.user })
554
+ api(_ctxid).put(null!, res as never)
555
+ })
556
+ if (err) {
557
+ _ack(err)
558
+ return
559
+ }
560
+ }
561
+ if (!node) node = {} as never
562
+ node[item!] = data as GraphValue
563
+ const g = await graph(soul, node as never, ctx.user, _ack as never)
564
+ if (g === null) return
565
+
566
+ wire.put(g, _ack as never)
567
+ })
568
+ },
569
+ { secure: (typeof ctx.user === "boolean" ? ctx.user : !!ctx.user) || options.secure }
570
+ )
571
+ return
572
+ }
573
+
574
+ const attemptRead = (retries = 0): void => {
575
+ wire.get(
576
+ { "#": soul, ".": item! },
577
+ async msg => {
578
+ if (msg.err) {
579
+ _ack(`error getting ${soul}.${item}: ${msg.err}`)
580
+ return
581
+ }
582
+
583
+ let node = msg.put && msg.put[soul]
584
+ const current = node && node[item!]
585
+
586
+ if (!current && retries < 5) {
587
+ await new Promise(r => setTimeout(r, 50))
588
+ return attemptRead(retries + 1)
589
+ }
590
+
591
+ const id = utils.rel.is(current as GraphValue)
592
+ if (!id) {
593
+ if (!node) node = {} as never
594
+ node[item!] = utils.rel.ify(utils.text.random())
595
+ const g = await graph(soul, node as never, ctx.user, _ack as never)
596
+ if (g === null) return
597
+
598
+ wire.put(g, err => {
599
+ if (err) {
600
+ _ack(`error putting ${item} on ${soul}: ${err}`)
601
+ } else {
602
+ const _ctxid = utils.text.random()
603
+ const chain: ChainItem[] = [{ item: item!, soul: soul }]
604
+ allctx.set(_ctxid, {
605
+ chain: chain,
606
+ user: ctx.user,
607
+ cb: ctx.cb,
608
+ })
609
+ api(_ctxid).put(data)
610
+ }
611
+ })
612
+ return
613
+ }
614
+
615
+ const update: string[] = []
616
+ for (const key of result as string[]) {
617
+ const err = await new Promise<string | null | undefined>(res => {
618
+ if (
619
+ utils.obj.is((data as Record<string, unknown>)[key]) &&
620
+ !utils.rel.is((data as Record<string, GraphValue>)[key])
621
+ ) {
622
+ const _ctxid = utils.text.random()
623
+ const chain: ChainItem[] = [{ item: key, soul: id }]
624
+ allctx.set(_ctxid, { chain: chain, user: ctx.user })
625
+ api(_ctxid).put((data as Record<string, GraphValue>)[key]!, res as never)
626
+ } else {
627
+ update.push(key)
628
+ res(null)
629
+ }
630
+ })
631
+ if (err) {
632
+ _ack(err)
633
+ return
634
+ }
635
+ }
636
+
637
+ if (update.length === 0) {
638
+ _ack(null)
639
+ return
640
+ }
641
+
642
+ // Only write the primitive update keys. Rels created by the
643
+ // recursive puts above are already in the in-memory graph.
644
+ const updateNode: Record<string, GraphValue> = {}
645
+ update.forEach(key => {
646
+ updateNode[key] = (data as Record<string, GraphValue>)[key]!
647
+ })
648
+ const g = await graph(id, updateNode as never, ctx.user, _ack as never)
649
+ if (g === null) return
650
+
651
+ wire.put(g, _ack as never)
652
+ },
653
+ { secure: (typeof ctx.user === "boolean" ? ctx.user : !!ctx.user) || options.secure }
654
+ )
655
+ }
656
+ attemptRead()
657
+ },
658
+
659
+ on: function (
660
+ lex: LexWithDot | ((data: unknown) => void),
661
+ cb?: ((data: unknown) => void) | boolean,
662
+ _get?: boolean,
663
+ _opt?: WireOptions
664
+ ): void {
665
+ let lexFilter: LexFilter | undefined
666
+ let callback: (data: unknown) => void
667
+ let opts: WireOptions | undefined
668
+
669
+ if (typeof lex === "function") {
670
+ opts = _get as never
671
+ _get = cb as never
672
+ callback = lex
673
+ lexFilter = null
674
+ } else {
675
+ lexFilter = lex as LexFilter
676
+ callback = (typeof cb === "function" ? cb : undefined)!
677
+ opts = _opt
678
+ }
679
+
680
+ if (typeof callback !== "function") {
681
+ console.log("error on() requires a callback function")
682
+ return
683
+ }
684
+
685
+ if (!ctxid) {
686
+ console.log("error please provide a key using get(key)")
687
+ callback(null)
688
+ return
689
+ }
690
+
691
+ const resolved = resolve({ on: lexFilter, _get: _get, _opt: opts }, callback)
692
+ if (!resolved) return
693
+
694
+ const { item, soul } = resolved
695
+ if (!soul) return
696
+
697
+ const ctx = allctx.get(ctxid)
698
+
699
+ allctx.set(ctxid, {
700
+ chain: [{ item: item, soul: soul }],
701
+ on: true,
702
+ user: ctx ? ctx.user : null,
703
+ })
704
+
705
+ // Shared retry state for this on() subscription.
706
+ let retryCount = 0
707
+ const maxRetries = 5
708
+ const retryDelay = 1000
709
+ let retryTimer: ReturnType<typeof setTimeout> | null = null
710
+
711
+ map.set(callback, () => {
712
+ // Bail out if off() has already cleaned up this context — the mapped
713
+ // callback can fire twice (once from wire.on and once from the _get
714
+ // immediate-read path) and the second call after async context cleanup
715
+ // would crash in resolve(). Matches the guard in watchForRel.
716
+ if (!allctx.has(ctxid!)) return
717
+ // Cancel any pending retry — the persistent listener firing means we
718
+ // have an update and should not call callback twice.
719
+ clearTimeout(retryTimer!)
720
+ retryTimer = null
721
+ wire.get(
722
+ { "#": soul, ".": item! },
723
+ msg => {
724
+ const node = msg.put && msg.put[soul]
725
+ const current = node && node[item!]
726
+ const id = utils.rel.is(current as GraphValue)
727
+ if (id) {
728
+ const attemptRead = async (retries = 0): Promise<void> => {
729
+ const data = await new Promise<unknown>(res => {
730
+ const _ctxid = utils.text.random()
731
+ allctx.set(_ctxid, {
732
+ chain: [{ item: null, soul: id }],
733
+ user: ctx ? ctx.user : null,
734
+ })
735
+ api(_ctxid).next(null as never, res as never, (retries === 0 ? utils.obj.put(opts, "fast", true) : opts) as never)
736
+ })
737
+ if (data !== null || retries >= 5) {
738
+ retryCount = 0
739
+ callback(data)
740
+ } else {
741
+ await new Promise(resolve => setTimeout(resolve, 50))
742
+ attemptRead(retries + 1)
743
+ }
744
+ }
745
+ attemptRead()
746
+ } else {
747
+ const nodeExists = !!node
748
+ if (nodeExists) {
749
+ retryCount = 0
750
+ callback(current !== undefined ? current : null)
751
+ } else if (retryCount < maxRetries) {
752
+ // No data yet — keep polling until we get a response or
753
+ // exhaust retries. on() with _get can return null on a cache
754
+ // miss and never fire again if the data doesn't change while
755
+ // the listener is attached, so we must poll for initial load.
756
+ const secureOpt = { ...opts, secure: ctx ? ((typeof ctx.user === "boolean" ? ctx.user : !!ctx.user) || options.secure) : options.secure }
757
+ const retry = (): void => {
758
+ const delay = Math.min(retryDelay * Math.pow(2, retryCount), 30000)
759
+ retryCount++
760
+ retryTimer = setTimeout(() => {
761
+ wire.get(
762
+ { "#": soul, ".": item! },
763
+ retryMsg => {
764
+ const retryNode = retryMsg.put && retryMsg.put[soul]
765
+ if (retryNode) {
766
+ retryCount = 0
767
+ retryTimer = null
768
+ const retryValue = retryNode[item!]
769
+ callback(retryValue !== undefined ? retryValue : null)
770
+ } else if (retryCount < maxRetries) {
771
+ retry()
772
+ } else {
773
+ retryTimer = null
774
+ callback(null)
775
+ }
776
+ },
777
+ secureOpt,
778
+ )
779
+ }, delay)
780
+ }
781
+ retry()
782
+ } else {
783
+ callback(null)
784
+ }
785
+ }
786
+ },
787
+ { ...opts, secure: ctx ? ((typeof ctx.user === "boolean" ? ctx.user : !!ctx.user) || options.secure) : options.secure }
788
+ )
789
+ })
790
+
791
+ let initialLex: { "#": string; "."?: string | number } = lexFilter
792
+ ? (utils.obj.put(lexFilter as never, "#", soul) as { "#": string; "."?: string | number })
793
+ : { "#": soul, ".": item! }
794
+ if (initialLex["."] != null && typeof initialLex["."] === "number") {
795
+ initialLex = { ...initialLex, ".": String(initialLex["."]) }
796
+ }
797
+ wire.on(initialLex as never, map.get(callback)!, false, opts)
798
+
799
+ wire.get(
800
+ { "#": soul, ".": item! },
801
+ msg => {
802
+ if (msg.err) {
803
+ console.log(`error getting ${soul}.${item}: ${msg.err}`)
804
+ return
805
+ }
806
+
807
+ const current = msg.put && msg.put[soul] && msg.put[soul]![item!]
808
+ const id = utils.rel.is(current as GraphValue)
809
+ if (id) {
810
+ wire.off(initialLex as never, map.get(callback)!)
811
+ const relLex = (lexFilter
812
+ ? utils.obj.put(initialLex as never, "#", id)
813
+ : { "#": id, ".": null }) as Lex
814
+ wire.on(relLex, map.get(callback)!, false, opts)
815
+ if (_get) map.get(callback)!()
816
+ } else if (_get) {
817
+ // Not a rel, but _get was requested, so trigger callback.
818
+ map.get(callback)!()
819
+ }
820
+ },
821
+ { ...opts, secure: ctx ? ((typeof ctx.user === "boolean" ? ctx.user : !!ctx.user) || options.secure) : options.secure }
822
+ )
823
+ },
824
+
825
+ off: function (cb?: (data: unknown) => void): HolsterAPI | void {
826
+ if (!ctxid) {
827
+ console.log("error please provide a key using get(key)")
828
+ if (cb) cb(null)
829
+ return
830
+ }
831
+
832
+ const resolved = resolve({ off: true }, cb)
833
+ if (!resolved) return
834
+
835
+ const { item, soul } = resolved
836
+ if (!soul) return
837
+
838
+ wire.off({ "#": soul } as never, map.get(cb!)!)
839
+
840
+ wire.get({ "#": soul, ".": item! }, msg => {
841
+ if (msg.err) {
842
+ map.delete(cb!)
843
+ allctx.delete(ctxid!)
844
+ return
845
+ }
846
+
847
+ const current = msg.put && msg.put[soul] && msg.put[soul]![item!]
848
+ const id = utils.rel.is(current as GraphValue)
849
+ if (id) wire.off({ "#": id } as never, map.get(cb!)!)
850
+ map.delete(cb!)
851
+ allctx.delete(ctxid!)
852
+ })
853
+ },
854
+
855
+ user: () => {
856
+ if (!(user as { get?: unknown }).get) {
857
+ Object.assign(user, api())
858
+ ;(user as UserInterface & HolsterAPI).get = function (
859
+ this: HolsterAPI,
860
+ keys: string | string[],
861
+ lex?: LexFilter | ((data: unknown) => void),
862
+ cb?: ((data: unknown) => void) | WireOptions,
863
+ _opt?: WireOptions
864
+ ): HolsterAPI {
865
+ let lexFilter: LexFilter | null | undefined
866
+ let callback: ((data: unknown) => void) | undefined
867
+ let opts: WireOptions | undefined
868
+
869
+ if (typeof lex === "function") {
870
+ opts = cb as WireOptions
871
+ callback = lex
872
+ lexFilter = null
873
+ } else {
874
+ lexFilter = lex
875
+ callback = cb as (data: unknown) => void
876
+ opts = _opt
877
+ }
878
+
879
+ let pub: string | null = null
880
+ let key: string | null = null
881
+ if (user.is) pub = user.is.pub
882
+ if (typeof keys === "string") {
883
+ key = keys
884
+ } else if (keys instanceof Array) {
885
+ if (keys.length === 2 && keys[0] && keys[1]) {
886
+ pub = keys[0]
887
+ key = keys[1]
888
+ } else if (keys.length === 1 && keys[0]) {
889
+ key = keys[0]
890
+ }
891
+ }
892
+ if (!pub) {
893
+ console.log("error please log in or provide a public key")
894
+ if (callback) callback(null)
895
+ return this
896
+ }
897
+
898
+ if (key === null || key === "" || key === "_") {
899
+ console.log("error please provide a key")
900
+ if (callback) callback(null)
901
+ return this
902
+ }
903
+
904
+ if (lexFilter && !callback) {
905
+ console.log("error lex requires a callback function")
906
+ return this
907
+ }
908
+
909
+ ctxid = utils.text.random()
910
+ const chain: ChainItem[] = [{ item: String(key), soul: "~" + pub }]
911
+ allctx.set(ctxid, { chain: chain, user: user.is, cb: callback as never })
912
+ if (!callback) return api(ctxid)
913
+
914
+ const _done = done(ctxid)
915
+ const resolved = resolve({ get: lexFilter, _opt: opts }, _done)
916
+ if (resolved) get(lexFilter as never, resolved.soul!, _done, opts)
917
+ return this
918
+ } as never
919
+ }
920
+ return user as UserInterface & HolsterAPI
921
+ },
922
+
923
+ wire: wire,
924
+ SEA: SEA,
925
+ }
926
+ }
927
+ return api()
928
+ }
929
+
930
+ export default Holster
931
+