@mblaney/holster 2.2.3 → 2.3.1
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 +3 -3
- package/dist/holster.js.map +3 -3
- package/package.json +1 -1
- package/src/holster.js +49 -15
- package/src/wire.js +33 -40
- package/ts/src/get.ts +4 -1
- package/ts/src/holster.ts +101 -27
- package/ts/src/user.ts +19 -7
- package/ts/src/wire.ts +33 -40
package/package.json
CHANGED
package/src/holster.js
CHANGED
|
@@ -73,17 +73,31 @@ const Holster = opt => {
|
|
|
73
73
|
const data = await new Promise(res => {
|
|
74
74
|
const _ctxid = utils.text.random()
|
|
75
75
|
allctx.set(_ctxid, {chain: [{item: null, soul: id}], user: ctxUser})
|
|
76
|
-
api(_ctxid).next(
|
|
77
|
-
null,
|
|
78
|
-
res,
|
|
79
|
-
retries === 0 ? utils.obj.put(_opt, "fast", true) : _opt,
|
|
80
|
-
)
|
|
76
|
+
api(_ctxid).next(null, res, _opt)
|
|
81
77
|
})
|
|
82
78
|
if (data !== null || retries >= 5) return data
|
|
83
79
|
await new Promise(resolve => setTimeout(resolve, 50))
|
|
84
80
|
return readRelTarget(id, ctxUser, _opt, retries + 1)
|
|
85
81
|
}
|
|
86
82
|
|
|
83
|
+
// Wraps a not-yet-terminal chain object (api(ctxid), returned wherever no
|
|
84
|
+
// callback was given to resolve it yet) with a .then, so awaiting it
|
|
85
|
+
// resolves the same way a callback given right here would - mirrors
|
|
86
|
+
// readRelTarget's own api(ctxid).next(null, cb) idiom, which resolves the
|
|
87
|
+
// current chain without pushing a new key onto it. There's no separate
|
|
88
|
+
// error channel on this path (get/next's own callback only ever receives
|
|
89
|
+
// a value, never an error), so reject is simply never called - a
|
|
90
|
+
// rejection-less promise is still a valid one. Deliberately only applied
|
|
91
|
+
// at these specific "chain not yet resolved" return points, not to
|
|
92
|
+
// api()'s own bare object (see holster.user(), which Object.assigns that
|
|
93
|
+
// onto the long-lived user object) - that object has no chain to resolve
|
|
94
|
+
// at all, and giving user itself a .then would make a bare `await user`
|
|
95
|
+
// (or passing it to Promise.all, etc) silently try to resolve one.
|
|
96
|
+
const awaitable = chainApi => ({
|
|
97
|
+
...chainApi,
|
|
98
|
+
then: resolve => chainApi.next(null, resolve),
|
|
99
|
+
})
|
|
100
|
+
|
|
87
101
|
const api = ctxid => {
|
|
88
102
|
const get = (lex, soul, ack, _opt) => {
|
|
89
103
|
wire.get(
|
|
@@ -412,7 +426,7 @@ const Holster = opt => {
|
|
|
412
426
|
// to be objects.
|
|
413
427
|
ctxid = utils.text.random()
|
|
414
428
|
allctx.set(ctxid, {chain: [{item: String(key), soul: "root"}], cb: cb})
|
|
415
|
-
if (!cb) return api(ctxid)
|
|
429
|
+
if (!cb) return awaitable(api(ctxid))
|
|
416
430
|
|
|
417
431
|
const _done = done(ctxid)
|
|
418
432
|
// When there's a callback need to resolve the context first.
|
|
@@ -467,7 +481,7 @@ const Holster = opt => {
|
|
|
467
481
|
// Push the key to the context as it needs a soul lookup.
|
|
468
482
|
// (null is used to call the api with updated context)
|
|
469
483
|
if (key !== null) ctx.chain.push({item: String(key), soul: null})
|
|
470
|
-
if (!ctx.cb) return api(ctxid)
|
|
484
|
+
if (!ctx.cb) return awaitable(api(ctxid))
|
|
471
485
|
|
|
472
486
|
// When there's a callback need to resolve the context first.
|
|
473
487
|
const {soul} = resolve({get: lex, _opt: _opt}, _ack)
|
|
@@ -478,6 +492,25 @@ const Holster = opt => {
|
|
|
478
492
|
cb = set
|
|
479
493
|
set = false
|
|
480
494
|
}
|
|
495
|
+
// Unlike get/next (see awaitable above), put's own callback always
|
|
496
|
+
// receives an error channel - an error string, or undefined on
|
|
497
|
+
// success - so a promise here can reject, not just resolve.
|
|
498
|
+
if (!cb) {
|
|
499
|
+
const promise = new Promise((resolve, reject) => {
|
|
500
|
+
api(ctxid).put(data, set, err =>
|
|
501
|
+
err ? reject(new Error(err)) : resolve(),
|
|
502
|
+
)
|
|
503
|
+
})
|
|
504
|
+
// A caller using the old fire-and-forget idiom (put(data) with
|
|
505
|
+
// no callback, result never touched) shouldn't crash the
|
|
506
|
+
// process on an unhandled rejection if something genuinely
|
|
507
|
+
// fails now that this returns a promise - doesn't suppress the
|
|
508
|
+
// rejection for a caller that does await/catch it, since a
|
|
509
|
+
// promise can have more than one handler attached (same
|
|
510
|
+
// pattern as createRel's own promise above).
|
|
511
|
+
promise.catch(() => {})
|
|
512
|
+
return promise
|
|
513
|
+
}
|
|
481
514
|
const ack = ctxid => {
|
|
482
515
|
return data => {
|
|
483
516
|
if (cb) {
|
|
@@ -619,13 +652,14 @@ const Holster = opt => {
|
|
|
619
652
|
|
|
620
653
|
const _ctxid = utils.text.random()
|
|
621
654
|
const chain = [{item: item, soul: soul}]
|
|
622
|
-
|
|
623
|
-
|
|
624
|
-
|
|
625
|
-
|
|
626
|
-
|
|
627
|
-
|
|
628
|
-
|
|
655
|
+
allctx.set(_ctxid, {chain: chain, user: ctx.user})
|
|
656
|
+
// Pass on the previous ctx's own callback (if any)
|
|
657
|
+
// positionally rather than pre-registering it via
|
|
658
|
+
// allctx - otherwise it's invisible to put's own "no cb
|
|
659
|
+
// given" check above, which would then wrap this retry
|
|
660
|
+
// in a brand new promise competing with (and never
|
|
661
|
+
// actually calling) the real one.
|
|
662
|
+
api(_ctxid).put(data, ctx.cb)
|
|
629
663
|
})
|
|
630
664
|
return
|
|
631
665
|
}
|
|
@@ -952,7 +986,7 @@ const Holster = opt => {
|
|
|
952
986
|
ctxid = utils.text.random()
|
|
953
987
|
const chain = [{item: String(key), soul: "~" + pub}]
|
|
954
988
|
allctx.set(ctxid, {chain: chain, user: user.is, cb: cb})
|
|
955
|
-
if (!cb) return api(ctxid)
|
|
989
|
+
if (!cb) return awaitable(api(ctxid))
|
|
956
990
|
|
|
957
991
|
// When there's a callback need to resolve the context first.
|
|
958
992
|
const _done = done(ctxid)
|
package/src/wire.js
CHANGED
|
@@ -534,12 +534,12 @@ const Wire = opt => {
|
|
|
534
534
|
return
|
|
535
535
|
}
|
|
536
536
|
|
|
537
|
-
store.get(lex, (err,
|
|
538
|
-
if (
|
|
537
|
+
store.get(lex, (err, stored) => {
|
|
538
|
+
if (stored) {
|
|
539
539
|
// Populate in-memory graph from store data before sending the server
|
|
540
540
|
// request, so stale server responses with older state are correctly
|
|
541
541
|
// rejected by ham.mix.
|
|
542
|
-
for (const [soul, node] of Object.entries(
|
|
542
|
+
for (const [soul, node] of Object.entries(stored)) {
|
|
543
543
|
if (!node) continue
|
|
544
544
|
if (!graph[soul]) {
|
|
545
545
|
graph[soul] = {...node}
|
|
@@ -567,40 +567,44 @@ const Wire = opt => {
|
|
|
567
567
|
}
|
|
568
568
|
}
|
|
569
569
|
// Full-node fetch: the graph is now complete for this soul.
|
|
570
|
-
if (!lex["."] &&
|
|
571
|
-
|
|
572
|
-
|
|
573
|
-
|
|
574
|
-
|
|
575
|
-
|
|
576
|
-
|
|
577
|
-
|
|
578
|
-
|
|
579
|
-
|
|
580
|
-
|
|
581
|
-
|
|
582
|
-
|
|
583
|
-
|
|
584
|
-
|
|
585
|
-
|
|
586
|
-
|
|
587
|
-
|
|
588
|
-
|
|
589
|
-
|
|
590
|
-
|
|
591
|
-
|
|
592
|
-
}
|
|
593
|
-
} else {
|
|
570
|
+
if (!lex["."] && stored[lex["#"]]) hasNode.add(lex["#"])
|
|
571
|
+
}
|
|
572
|
+
|
|
573
|
+
// Answered from the graph rather than the raw store result: the merge
|
|
574
|
+
// above keeps any newer in-memory values, so a local put that store
|
|
575
|
+
// hasn't finished persisting yet is still included, while everything
|
|
576
|
+
// already on disk fills in the rest of the node.
|
|
577
|
+
const ack = {...stored, ...Get(lex, graph, true)}
|
|
578
|
+
// Only serve locally if the requested property is present.
|
|
579
|
+
const node = ack[lex["#"]]
|
|
580
|
+
const hasProperty =
|
|
581
|
+
node &&
|
|
582
|
+
Object.keys(node).some(key => key !== "_" && utils.match(lex["."], key))
|
|
583
|
+
if (hasProperty) {
|
|
584
|
+
// Public keys are immutable once written - never need a wire check.
|
|
585
|
+
if (!isNode && lex["."] !== utils.userPublicKey) {
|
|
586
|
+
if (!lex["."] || typeof lex["."] === "string") {
|
|
587
|
+
const checkKey = lex["#"] + (lex["."] ? "." + lex["."] : "")
|
|
588
|
+
const now = Date.now()
|
|
589
|
+
const lastCheck = recentChecks.get(checkKey)
|
|
590
|
+
if (!lastCheck || now - lastCheck > recentCheckTTL) {
|
|
591
|
+
recentChecks.set(checkKey, now)
|
|
594
592
|
const sendResult = send(request)
|
|
595
593
|
if (sendResult && sendResult.err) {
|
|
596
594
|
cb({err: sendResult.err})
|
|
597
595
|
return
|
|
598
596
|
}
|
|
599
597
|
}
|
|
598
|
+
} else {
|
|
599
|
+
const sendResult = send(request)
|
|
600
|
+
if (sendResult && sendResult.err) {
|
|
601
|
+
cb({err: sendResult.err})
|
|
602
|
+
return
|
|
603
|
+
}
|
|
600
604
|
}
|
|
601
|
-
cb(err ? {put: ack, err: err} : {put: ack})
|
|
602
|
-
return
|
|
603
605
|
}
|
|
606
|
+
cb(err ? {put: ack, err: err} : {put: ack})
|
|
607
|
+
return
|
|
604
608
|
}
|
|
605
609
|
|
|
606
610
|
// Data wasn't found locally, log errors and call back after a timeout.
|
|
@@ -678,17 +682,6 @@ const Wire = opt => {
|
|
|
678
682
|
return
|
|
679
683
|
}
|
|
680
684
|
|
|
681
|
-
// Ham.mix has already merged this locally-authored update into
|
|
682
|
-
// graph[soul], synchronously, before store.put below even starts
|
|
683
|
-
// persisting it - mark it as fully known now rather than waiting
|
|
684
|
-
// for a future disk fetch to do so, so a read landing in that
|
|
685
|
-
// window doesn't get forced through a disk round-trip that could
|
|
686
|
-
// itself race ahead of this same write and find nothing, even
|
|
687
|
-
// though the correct answer was already sitting in memory.
|
|
688
|
-
for (const soul of Object.keys(update.now)) {
|
|
689
|
-
hasNode.add(soul)
|
|
690
|
-
}
|
|
691
|
-
|
|
692
685
|
if (opt.userLimit) {
|
|
693
686
|
for (const [soul, node] of Object.entries(update.now)) {
|
|
694
687
|
const pub = node[utils.userPublicKey]
|
package/ts/src/get.ts
CHANGED
|
@@ -59,9 +59,10 @@ const Get = (lex: Lex, graph: Graph, fast?: boolean): Graph | undefined => {
|
|
|
59
59
|
// return undefined to ensure we get all matching properties from store.
|
|
60
60
|
if (!fast) return undefined
|
|
61
61
|
|
|
62
|
+
let found = false
|
|
62
63
|
for (const key of Object.keys(graph[soul]!)) {
|
|
63
64
|
// Always include userPublicKey for verification, regardless of filter
|
|
64
|
-
if (key === userPublicKey || match(lex["."], key)) {
|
|
65
|
+
if (key !== "_" && (key === userPublicKey || match(lex["."], key))) {
|
|
65
66
|
node[key] = graph[soul]![key]!
|
|
66
67
|
node._[">"][key] = graph[soul]!._[">"][key]!
|
|
67
68
|
const state = graph[soul]!._[">"][key]!
|
|
@@ -69,8 +70,10 @@ const Get = (lex: Lex, graph: Graph, fast?: boolean): Graph | undefined => {
|
|
|
69
70
|
if (sigs && sigs[state]) {
|
|
70
71
|
signatures[state.toString()] = sigs[state]!
|
|
71
72
|
}
|
|
73
|
+
found = true
|
|
72
74
|
}
|
|
73
75
|
}
|
|
76
|
+
if (!found) return undefined
|
|
74
77
|
}
|
|
75
78
|
|
|
76
79
|
if (Object.keys(signatures).length > 0) {
|
package/ts/src/holster.ts
CHANGED
|
@@ -28,34 +28,54 @@ export interface HolsterAPI {
|
|
|
28
28
|
get: {
|
|
29
29
|
(
|
|
30
30
|
key: string | null,
|
|
31
|
-
lex
|
|
32
|
-
cb
|
|
31
|
+
lex: LexWithDot,
|
|
32
|
+
cb: (data: unknown) => void,
|
|
33
33
|
_opt?: WireOptions,
|
|
34
34
|
): HolsterAPI
|
|
35
35
|
(
|
|
36
36
|
key: string | null,
|
|
37
|
-
cb
|
|
37
|
+
cb: (data: unknown) => void,
|
|
38
38
|
_opt?: WireOptions,
|
|
39
39
|
): HolsterAPI
|
|
40
|
+
(
|
|
41
|
+
key: string | null,
|
|
42
|
+
lex?: LexWithDot,
|
|
43
|
+
_opt?: WireOptions,
|
|
44
|
+
): AwaitableHolsterAPI
|
|
40
45
|
}
|
|
41
46
|
next: {
|
|
42
47
|
(
|
|
43
48
|
key: string | null,
|
|
44
|
-
lex
|
|
45
|
-
cb
|
|
49
|
+
lex: LexWithDot,
|
|
50
|
+
cb: (data: unknown) => void,
|
|
46
51
|
_opt?: WireOptions,
|
|
47
52
|
): HolsterAPI
|
|
48
53
|
(
|
|
49
54
|
key: string | null,
|
|
50
|
-
cb
|
|
55
|
+
cb: (data: unknown) => void,
|
|
51
56
|
_opt?: WireOptions,
|
|
52
57
|
): HolsterAPI
|
|
58
|
+
(
|
|
59
|
+
key: string | null,
|
|
60
|
+
lex?: LexWithDot,
|
|
61
|
+
_opt?: WireOptions,
|
|
62
|
+
): AwaitableHolsterAPI
|
|
63
|
+
}
|
|
64
|
+
put: {
|
|
65
|
+
(
|
|
66
|
+
data: GraphValue | Record<string, GraphValue>,
|
|
67
|
+
set: boolean,
|
|
68
|
+
cb: (data?: string | null) => void,
|
|
69
|
+
): void
|
|
70
|
+
(
|
|
71
|
+
data: GraphValue | Record<string, GraphValue>,
|
|
72
|
+
cb: (data?: string | null) => void,
|
|
73
|
+
): void
|
|
74
|
+
(
|
|
75
|
+
data: GraphValue | Record<string, GraphValue>,
|
|
76
|
+
set?: boolean,
|
|
77
|
+
): Promise<void>
|
|
53
78
|
}
|
|
54
|
-
put: (
|
|
55
|
-
data: GraphValue | Record<string, GraphValue>,
|
|
56
|
-
set?: boolean | ((data?: string | null) => void),
|
|
57
|
-
cb?: (data?: string | null) => void,
|
|
58
|
-
) => HolsterAPI | void
|
|
59
79
|
on: {
|
|
60
80
|
(
|
|
61
81
|
lex: LexWithDot,
|
|
@@ -71,6 +91,23 @@ export interface HolsterAPI {
|
|
|
71
91
|
SEA: typeof SEA
|
|
72
92
|
}
|
|
73
93
|
|
|
94
|
+
// Deliberately not the built-in PromiseLike<T> - that also requires a
|
|
95
|
+
// return value and an onrejected param awaitable's own then() never uses
|
|
96
|
+
// (there's no separate error channel on this path, see awaitable's own
|
|
97
|
+
// comment). TS's Awaited<T> only needs a callable then(onfulfilled, ...)
|
|
98
|
+
// to recognize something as thenable - this minimal shape is enough for
|
|
99
|
+
// `await` to work and to correctly infer the resolved type as unknown.
|
|
100
|
+
interface Thenable {
|
|
101
|
+
then: (resolve: (data: unknown) => void) => void
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
// A chain object (api(ctxid), returned wherever no callback was given to
|
|
105
|
+
// resolve it yet) with a .then added, so `await`-ing it resolves the same
|
|
106
|
+
// way a callback given right here would (see awaitable in holster.ts).
|
|
107
|
+
// Only the "no callback given" get/next overloads above return this -
|
|
108
|
+
// the "has a real callback" ones genuinely don't have a .then.
|
|
109
|
+
export type AwaitableHolsterAPI = HolsterAPI & Thenable
|
|
110
|
+
|
|
74
111
|
const Holster = (opt?: HolsterOptions | string | string[]): HolsterAPI => {
|
|
75
112
|
let options: HolsterOptions
|
|
76
113
|
if (typeof opt === "string") options = {peers: [opt]}
|
|
@@ -145,17 +182,32 @@ const Holster = (opt?: HolsterOptions | string | string[]): HolsterAPI => {
|
|
|
145
182
|
const data = await new Promise<unknown>(res => {
|
|
146
183
|
const _ctxid = utils.text.random()
|
|
147
184
|
allctx.set(_ctxid, {chain: [{item: null, soul: id}], user: ctxUser})
|
|
148
|
-
api(_ctxid).next(
|
|
149
|
-
null as never,
|
|
150
|
-
res as never,
|
|
151
|
-
(retries === 0 ? utils.obj.put(opt, "fast", true) : opt) as never,
|
|
152
|
-
)
|
|
185
|
+
api(_ctxid).next(null as never, res as never, opt as never)
|
|
153
186
|
})
|
|
154
187
|
if (data !== null || retries >= 5) return data
|
|
155
188
|
await new Promise(resolve => setTimeout(resolve, 50))
|
|
156
189
|
return readRelTarget(id, ctxUser, opt, retries + 1)
|
|
157
190
|
}
|
|
158
191
|
|
|
192
|
+
// Wraps a not-yet-terminal chain object (api(ctxid), returned wherever no
|
|
193
|
+
// callback was given to resolve it yet) with a .then, so awaiting it
|
|
194
|
+
// resolves the same way a callback given right here would - mirrors
|
|
195
|
+
// readRelTarget's own api(ctxid).next(null, cb) idiom, which resolves the
|
|
196
|
+
// current chain without pushing a new key onto it. There's no separate
|
|
197
|
+
// error channel on this path (get/next's own callback only ever receives
|
|
198
|
+
// a value, never an error), so reject is simply never called - a
|
|
199
|
+
// rejection-less promise is still a valid one. Deliberately only applied
|
|
200
|
+
// at these specific "chain not yet resolved" return points, not to
|
|
201
|
+
// api()'s own bare object (see holster.user(), which Object.assigns that
|
|
202
|
+
// onto the long-lived user object) - that object has no chain to resolve
|
|
203
|
+
// at all, and giving user itself a .then would make a bare `await user`
|
|
204
|
+
// (or passing it to Promise.all, etc) silently try to resolve one.
|
|
205
|
+
const awaitable = (chainApi: HolsterAPI): AwaitableHolsterAPI => ({
|
|
206
|
+
...chainApi,
|
|
207
|
+
then: (resolve: (data: unknown) => void) =>
|
|
208
|
+
chainApi.next(null, resolve as never),
|
|
209
|
+
})
|
|
210
|
+
|
|
159
211
|
const api = (initCtxid?: string): HolsterAPI => {
|
|
160
212
|
let ctxid = initCtxid
|
|
161
213
|
const get = (
|
|
@@ -559,7 +611,7 @@ const Holster = (opt?: HolsterOptions | string | string[]): HolsterAPI => {
|
|
|
559
611
|
chain: [{item: String(key), soul: "root"}],
|
|
560
612
|
cb: callback,
|
|
561
613
|
})
|
|
562
|
-
if (!callback) return api(ctxid)
|
|
614
|
+
if (!callback) return awaitable(api(ctxid))
|
|
563
615
|
|
|
564
616
|
const _done = done(ctxid)
|
|
565
617
|
const result = resolve({get: lexFilter, _opt: opts}, _done)
|
|
@@ -624,7 +676,7 @@ const Holster = (opt?: HolsterOptions | string | string[]): HolsterAPI => {
|
|
|
624
676
|
}
|
|
625
677
|
|
|
626
678
|
if (key !== null) ctx.chain.push({item: String(key), soul: null})
|
|
627
|
-
if (!ctx.cb) return api(ctxid)
|
|
679
|
+
if (!ctx.cb) return awaitable(api(ctxid))
|
|
628
680
|
|
|
629
681
|
const result = resolve({get: lexFilter, _opt: opts}, _ack)
|
|
630
682
|
if (result) get(lexFilter as never, result.soul!, _ack, opts)
|
|
@@ -635,7 +687,7 @@ const Holster = (opt?: HolsterOptions | string | string[]): HolsterAPI => {
|
|
|
635
687
|
data: GraphValue | Record<string, GraphValue>,
|
|
636
688
|
set?: boolean | ((data?: string | null) => void),
|
|
637
689
|
cb?: (data?: string | null) => void,
|
|
638
|
-
):
|
|
690
|
+
): void | Promise<void> {
|
|
639
691
|
let isSet = false
|
|
640
692
|
let callback: ((data?: string | null) => void) | undefined
|
|
641
693
|
|
|
@@ -647,6 +699,26 @@ const Holster = (opt?: HolsterOptions | string | string[]): HolsterAPI => {
|
|
|
647
699
|
callback = cb
|
|
648
700
|
}
|
|
649
701
|
|
|
702
|
+
// Unlike get/next (see awaitable above), put's own callback always
|
|
703
|
+
// receives an error channel - an error string, or undefined on
|
|
704
|
+
// success - so a promise here can reject, not just resolve.
|
|
705
|
+
if (!callback) {
|
|
706
|
+
const promise = new Promise<void>((promiseResolve, reject) => {
|
|
707
|
+
api(ctxid).put(data, isSet, err =>
|
|
708
|
+
err ? reject(new Error(err)) : promiseResolve(),
|
|
709
|
+
)
|
|
710
|
+
})
|
|
711
|
+
// A caller using the old fire-and-forget idiom (put(data) with
|
|
712
|
+
// no callback, result never touched) shouldn't crash the
|
|
713
|
+
// process on an unhandled rejection if something genuinely
|
|
714
|
+
// fails now that this returns a promise - doesn't suppress the
|
|
715
|
+
// rejection for a caller that does await/catch it, since a
|
|
716
|
+
// promise can have more than one handler attached (same
|
|
717
|
+
// pattern as createRel's own promise above).
|
|
718
|
+
promise.catch(() => {})
|
|
719
|
+
return promise
|
|
720
|
+
}
|
|
721
|
+
|
|
650
722
|
const ack = (ctxid: string) => {
|
|
651
723
|
return (data?: string | null): void => {
|
|
652
724
|
if (callback) {
|
|
@@ -797,12 +869,14 @@ const Holster = (opt?: HolsterOptions | string | string[]): HolsterAPI => {
|
|
|
797
869
|
|
|
798
870
|
const _ctxid = utils.text.random()
|
|
799
871
|
const chain: ChainItem[] = [{item: item!, soul: soul}]
|
|
800
|
-
allctx.set(_ctxid, {
|
|
801
|
-
|
|
802
|
-
|
|
803
|
-
|
|
804
|
-
|
|
805
|
-
|
|
872
|
+
allctx.set(_ctxid, {chain: chain, user: ctx.user})
|
|
873
|
+
// Pass on the previous ctx's own callback (if any)
|
|
874
|
+
// positionally rather than pre-registering it via
|
|
875
|
+
// allctx - otherwise it's invisible to put's own "no cb
|
|
876
|
+
// given" check above, which would then wrap this retry
|
|
877
|
+
// in a brand new promise competing with (and never
|
|
878
|
+
// actually calling) the real one.
|
|
879
|
+
api(_ctxid).put(data, ctx.cb as never)
|
|
806
880
|
})
|
|
807
881
|
return
|
|
808
882
|
}
|
|
@@ -864,7 +938,7 @@ const Holster = (opt?: HolsterOptions | string | string[]): HolsterAPI => {
|
|
|
864
938
|
)
|
|
865
939
|
}
|
|
866
940
|
attemptRead()
|
|
867
|
-
},
|
|
941
|
+
} as HolsterAPI["put"],
|
|
868
942
|
|
|
869
943
|
on: function (
|
|
870
944
|
lex: LexWithDot | ((data: unknown) => void),
|
|
@@ -1199,7 +1273,7 @@ const Holster = (opt?: HolsterOptions | string | string[]): HolsterAPI => {
|
|
|
1199
1273
|
user: user.is,
|
|
1200
1274
|
cb: callback as never,
|
|
1201
1275
|
})
|
|
1202
|
-
if (!callback) return api(ctxid)
|
|
1276
|
+
if (!callback) return awaitable(api(ctxid))
|
|
1203
1277
|
|
|
1204
1278
|
const _done = done(ctxid)
|
|
1205
1279
|
const resolved = resolve({get: lexFilter, _opt: opts}, _done)
|
package/ts/src/user.ts
CHANGED
|
@@ -12,7 +12,7 @@ import type {
|
|
|
12
12
|
LexFilter,
|
|
13
13
|
WireOptions,
|
|
14
14
|
} from "./schemas.ts"
|
|
15
|
-
import type {HolsterAPI} from "./holster.ts"
|
|
15
|
+
import type {HolsterAPI, AwaitableHolsterAPI} from "./holster.ts"
|
|
16
16
|
|
|
17
17
|
type WireInterface = ReturnType<typeof Wire>
|
|
18
18
|
|
|
@@ -42,12 +42,24 @@ export interface UserInterface {
|
|
|
42
42
|
password: string,
|
|
43
43
|
cb?: (err?: string | null) => void,
|
|
44
44
|
) => void
|
|
45
|
-
get
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
45
|
+
get?: {
|
|
46
|
+
(
|
|
47
|
+
keys: string | string[],
|
|
48
|
+
lex: LexFilter,
|
|
49
|
+
cb: (data: unknown) => void,
|
|
50
|
+
_opt?: WireOptions,
|
|
51
|
+
): HolsterAPI
|
|
52
|
+
(
|
|
53
|
+
keys: string | string[],
|
|
54
|
+
cb: (data: unknown) => void,
|
|
55
|
+
_opt?: WireOptions,
|
|
56
|
+
): HolsterAPI
|
|
57
|
+
(
|
|
58
|
+
keys: string | string[],
|
|
59
|
+
lex?: LexFilter,
|
|
60
|
+
_opt?: WireOptions,
|
|
61
|
+
): AwaitableHolsterAPI
|
|
62
|
+
}
|
|
51
63
|
}
|
|
52
64
|
|
|
53
65
|
/**
|
package/ts/src/wire.ts
CHANGED
|
@@ -593,12 +593,12 @@ const Wire = (opt: HolsterOptions): WireAPI => {
|
|
|
593
593
|
return
|
|
594
594
|
}
|
|
595
595
|
|
|
596
|
-
store.get(lex, (err,
|
|
597
|
-
if (
|
|
596
|
+
store.get(lex, (err, stored) => {
|
|
597
|
+
if (stored) {
|
|
598
598
|
// Populate in-memory graph from store data before sending the server
|
|
599
599
|
// request, so stale server responses with older state are correctly
|
|
600
600
|
// rejected by ham.mix.
|
|
601
|
-
for (const [soul, node] of Object.entries(
|
|
601
|
+
for (const [soul, node] of Object.entries(stored)) {
|
|
602
602
|
if (!node) continue
|
|
603
603
|
if (!graph[soul]) {
|
|
604
604
|
graph[soul] = {...node} as unknown as Graph[string]
|
|
@@ -622,40 +622,44 @@ const Wire = (opt: HolsterOptions): WireAPI => {
|
|
|
622
622
|
}
|
|
623
623
|
}
|
|
624
624
|
// Full-node fetch: the graph is now complete for this soul.
|
|
625
|
-
if (!lex["."] &&
|
|
626
|
-
|
|
627
|
-
|
|
628
|
-
|
|
629
|
-
|
|
630
|
-
|
|
631
|
-
|
|
632
|
-
|
|
633
|
-
|
|
634
|
-
|
|
635
|
-
|
|
636
|
-
|
|
637
|
-
|
|
638
|
-
|
|
639
|
-
|
|
640
|
-
|
|
641
|
-
|
|
642
|
-
|
|
643
|
-
|
|
644
|
-
|
|
645
|
-
|
|
646
|
-
|
|
647
|
-
}
|
|
648
|
-
} else {
|
|
625
|
+
if (!lex["."] && stored[lex["#"]]) hasNode.add(lex["#"])
|
|
626
|
+
}
|
|
627
|
+
|
|
628
|
+
// Answered from the graph rather than the raw store result: the merge
|
|
629
|
+
// above keeps any newer in-memory values, so a local put that store
|
|
630
|
+
// hasn't finished persisting yet is still included, while everything
|
|
631
|
+
// already on disk fills in the rest of the node.
|
|
632
|
+
const ack = {...stored, ...Get(lex, graph, true)}
|
|
633
|
+
// Only serve locally if the requested property is present.
|
|
634
|
+
const node = ack[lex["#"]]
|
|
635
|
+
const hasProperty =
|
|
636
|
+
node &&
|
|
637
|
+
Object.keys(node).some(key => key !== "_" && utils.match(lex["."], key))
|
|
638
|
+
if (hasProperty) {
|
|
639
|
+
// Public keys are immutable once written - never need a wire check.
|
|
640
|
+
if (!isNode && lex["."] !== utils.userPublicKey) {
|
|
641
|
+
if (!lex["."] || typeof lex["."] === "string") {
|
|
642
|
+
const checkKey = lex["#"] + (lex["."] ? "." + lex["."] : "")
|
|
643
|
+
const now = Date.now()
|
|
644
|
+
const lastCheck = recentChecks.get(checkKey)
|
|
645
|
+
if (!lastCheck || now - lastCheck > recentCheckTTL) {
|
|
646
|
+
recentChecks.set(checkKey, now)
|
|
649
647
|
const sendResult = send(request)
|
|
650
648
|
if (sendResult && sendResult.err) {
|
|
651
649
|
cb({err: sendResult.err})
|
|
652
650
|
return
|
|
653
651
|
}
|
|
654
652
|
}
|
|
653
|
+
} else {
|
|
654
|
+
const sendResult = send(request)
|
|
655
|
+
if (sendResult && sendResult.err) {
|
|
656
|
+
cb({err: sendResult.err})
|
|
657
|
+
return
|
|
658
|
+
}
|
|
655
659
|
}
|
|
656
|
-
cb(err ? {put: ack, err: err} : {put: ack})
|
|
657
|
-
return
|
|
658
660
|
}
|
|
661
|
+
cb(err ? {put: ack, err: err} : {put: ack})
|
|
662
|
+
return
|
|
659
663
|
}
|
|
660
664
|
|
|
661
665
|
if (err) console.log(err)
|
|
@@ -725,17 +729,6 @@ const Wire = (opt: HolsterOptions): WireAPI => {
|
|
|
725
729
|
return
|
|
726
730
|
}
|
|
727
731
|
|
|
728
|
-
// Ham.mix has already merged this locally-authored update into
|
|
729
|
-
// graph[soul], synchronously, before store.put below even starts
|
|
730
|
-
// persisting it - mark it as fully known now rather than waiting
|
|
731
|
-
// for a future disk fetch to do so, so a read landing in that
|
|
732
|
-
// window doesn't get forced through a disk round-trip that could
|
|
733
|
-
// itself race ahead of this same write and find nothing, even
|
|
734
|
-
// though the correct answer was already sitting in memory.
|
|
735
|
-
for (const soul of Object.keys(update.now)) {
|
|
736
|
-
hasNode.add(soul)
|
|
737
|
-
}
|
|
738
|
-
|
|
739
732
|
if (options.userLimit) {
|
|
740
733
|
for (const [soul, node] of Object.entries(update.now)) {
|
|
741
734
|
const pub = node![utils.userPublicKey] as string | undefined
|