@optimystic/db-p2p 0.21.0 → 0.24.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.
- package/dist/src/cluster/client.d.ts +10 -0
- package/dist/src/cluster/client.d.ts.map +1 -1
- package/dist/src/cluster/client.js +30 -1
- package/dist/src/cluster/client.js.map +1 -1
- package/dist/src/cluster/cluster-policy.d.ts +13 -2
- package/dist/src/cluster/cluster-policy.d.ts.map +1 -1
- package/dist/src/cluster/cluster-policy.js +51 -4
- package/dist/src/cluster/cluster-policy.js.map +1 -1
- package/dist/src/cluster/cluster-repo.d.ts +42 -17
- package/dist/src/cluster/cluster-repo.d.ts.map +1 -1
- package/dist/src/cluster/cluster-repo.js +229 -122
- package/dist/src/cluster/cluster-repo.js.map +1 -1
- package/dist/src/cluster/cluster-size-coupling.d.ts +28 -0
- package/dist/src/cluster/cluster-size-coupling.d.ts.map +1 -0
- package/dist/src/cluster/cluster-size-coupling.js +35 -0
- package/dist/src/cluster/cluster-size-coupling.js.map +1 -0
- package/dist/src/cluster/quorum-restore.d.ts +6 -0
- package/dist/src/cluster/quorum-restore.d.ts.map +1 -1
- package/dist/src/cluster/quorum-restore.js +1 -1
- package/dist/src/cluster/quorum-restore.js.map +1 -1
- package/dist/src/cluster/reconcile-block.d.ts.map +1 -1
- package/dist/src/cluster/reconcile-block.js +15 -3
- package/dist/src/cluster/reconcile-block.js.map +1 -1
- package/dist/src/cluster/service.d.ts +32 -1
- package/dist/src/cluster/service.d.ts.map +1 -1
- package/dist/src/cluster/service.js +43 -2
- package/dist/src/cluster/service.js.map +1 -1
- package/dist/src/cohort-topic/stream-util.d.ts +22 -6
- package/dist/src/cohort-topic/stream-util.d.ts.map +1 -1
- package/dist/src/cohort-topic/stream-util.js +56 -10
- package/dist/src/cohort-topic/stream-util.js.map +1 -1
- package/dist/src/dispute/dispute-service.d.ts.map +1 -1
- package/dist/src/dispute/dispute-service.js +9 -3
- package/dist/src/dispute/dispute-service.js.map +1 -1
- package/dist/src/index.d.ts +5 -0
- package/dist/src/index.d.ts.map +1 -1
- package/dist/src/index.js +5 -0
- package/dist/src/index.js.map +1 -1
- package/dist/src/libp2p-key-network.d.ts +134 -7
- package/dist/src/libp2p-key-network.d.ts.map +1 -1
- package/dist/src/libp2p-key-network.js +174 -37
- package/dist/src/libp2p-key-network.js.map +1 -1
- package/dist/src/libp2p-node-base.d.ts +3 -2
- package/dist/src/libp2p-node-base.d.ts.map +1 -1
- package/dist/src/libp2p-node-base.js +859 -778
- package/dist/src/libp2p-node-base.js.map +1 -1
- package/dist/src/libp2p-node-rn.d.ts +2 -2
- package/dist/src/libp2p-node-rn.d.ts.map +1 -1
- package/dist/src/libp2p-node-rn.js.map +1 -1
- package/dist/src/libp2p-node.d.ts +2 -2
- package/dist/src/libp2p-node.d.ts.map +1 -1
- package/dist/src/libp2p-node.js.map +1 -1
- package/dist/src/logger.d.ts +17 -1
- package/dist/src/logger.d.ts.map +1 -1
- package/dist/src/logger.js +19 -2
- package/dist/src/logger.js.map +1 -1
- package/dist/src/network/network-manager-service.d.ts +2 -0
- package/dist/src/network/network-manager-service.d.ts.map +1 -1
- package/dist/src/network/network-manager-service.js +4 -0
- package/dist/src/network/network-manager-service.js.map +1 -1
- package/dist/src/optimystic-node.d.ts +35 -0
- package/dist/src/optimystic-node.d.ts.map +1 -0
- package/dist/src/optimystic-node.js +2 -0
- package/dist/src/optimystic-node.js.map +1 -0
- package/dist/src/owned-block-seed.d.ts +6 -3
- package/dist/src/owned-block-seed.d.ts.map +1 -1
- package/dist/src/owned-block-seed.js +16 -3
- package/dist/src/owned-block-seed.js.map +1 -1
- package/dist/src/peer-address-book.d.ts +72 -0
- package/dist/src/peer-address-book.d.ts.map +1 -0
- package/dist/src/peer-address-book.js +123 -0
- package/dist/src/peer-address-book.js.map +1 -0
- package/dist/src/repo/client.d.ts.map +1 -1
- package/dist/src/repo/client.js +11 -2
- package/dist/src/repo/client.js.map +1 -1
- package/dist/src/repo/cluster-coordinator.d.ts +30 -0
- package/dist/src/repo/cluster-coordinator.d.ts.map +1 -1
- package/dist/src/repo/cluster-coordinator.js +95 -3
- package/dist/src/repo/cluster-coordinator.js.map +1 -1
- package/dist/src/repo/coordinator-repo.d.ts +78 -14
- package/dist/src/repo/coordinator-repo.d.ts.map +1 -1
- package/dist/src/repo/coordinator-repo.js +266 -81
- package/dist/src/repo/coordinator-repo.js.map +1 -1
- package/dist/src/rn.d.ts +5 -0
- package/dist/src/rn.d.ts.map +1 -1
- package/dist/src/rn.js +5 -0
- package/dist/src/rn.js.map +1 -1
- package/dist/src/storage/block-storage.d.ts.map +1 -1
- package/dist/src/storage/block-storage.js +57 -5
- package/dist/src/storage/block-storage.js.map +1 -1
- package/dist/src/storage/cached-raw-storage.d.ts +83 -0
- package/dist/src/storage/cached-raw-storage.d.ts.map +1 -0
- package/dist/src/storage/cached-raw-storage.js +152 -0
- package/dist/src/storage/cached-raw-storage.js.map +1 -0
- package/dist/src/storage/cached-store-driver.d.ts +186 -0
- package/dist/src/storage/cached-store-driver.d.ts.map +1 -0
- package/dist/src/storage/cached-store-driver.js +775 -0
- package/dist/src/storage/cached-store-driver.js.map +1 -0
- package/dist/src/storage/i-block-storage.d.ts +20 -1
- package/dist/src/storage/i-block-storage.d.ts.map +1 -1
- package/dist/src/storage/i-raw-storage.d.ts +12 -5
- package/dist/src/storage/i-raw-storage.d.ts.map +1 -1
- package/dist/src/storage/shared-cache-pool.d.ts +234 -0
- package/dist/src/storage/shared-cache-pool.d.ts.map +1 -0
- package/dist/src/storage/shared-cache-pool.js +354 -0
- package/dist/src/storage/shared-cache-pool.js.map +1 -0
- package/dist/src/storage/storage-repo.d.ts +56 -3
- package/dist/src/storage/storage-repo.d.ts.map +1 -1
- package/dist/src/storage/storage-repo.js +124 -18
- package/dist/src/storage/storage-repo.js.map +1 -1
- package/dist/src/testing/raw-storage-conformance.d.ts +2 -1
- package/dist/src/testing/raw-storage-conformance.d.ts.map +1 -1
- package/dist/src/testing/raw-storage-conformance.js +52 -2
- package/dist/src/testing/raw-storage-conformance.js.map +1 -1
- package/package.json +3 -3
- package/readme.md +668 -653
- package/src/cluster/block-transfer.ts +424 -424
- package/src/cluster/client.ts +119 -88
- package/src/cluster/cluster-error.ts +64 -64
- package/src/cluster/cluster-policy.ts +203 -152
- package/src/cluster/cluster-repo.ts +245 -125
- package/src/cluster/cluster-size-coupling.ts +45 -0
- package/src/cluster/commit-cert.ts +139 -139
- package/src/cluster/i-transaction-state-store.ts +43 -43
- package/src/cluster/memory-transaction-state-store.ts +56 -56
- package/src/cluster/peer-key-binding.ts +37 -37
- package/src/cluster/persistent-transaction-state-store.ts +92 -92
- package/src/cluster/quorum-restore.ts +223 -223
- package/src/cluster/reconcile-block.ts +203 -191
- package/src/cluster/service.ts +293 -241
- package/src/cluster/supermajority-coupling.ts +37 -37
- package/src/cohort-topic/bootstrap-evidence-builder.ts +122 -122
- package/src/cohort-topic/bootstrap-evidence-verifiers.ts +132 -132
- package/src/cohort-topic/bootstrap-parent-reference.ts +159 -159
- package/src/cohort-topic/change-bridge.ts +109 -109
- package/src/cohort-topic/cohort-gossip-driver.ts +231 -231
- package/src/cohort-topic/cohort-gossip-transport.ts +84 -84
- package/src/cohort-topic/fret-trust-anchor.ts +153 -153
- package/src/cohort-topic/host.ts +2901 -2901
- package/src/cohort-topic/index.ts +13 -13
- package/src/cohort-topic/membership-publish-sink.ts +20 -20
- package/src/cohort-topic/membership-source.ts +68 -68
- package/src/cohort-topic/peer-codec.ts +31 -31
- package/src/cohort-topic/peer-sig.ts +86 -86
- package/src/cohort-topic/protocols.ts +71 -71
- package/src/cohort-topic/reactivity-membership-gate.ts +77 -77
- package/src/cohort-topic/size-estimator.ts +16 -16
- package/src/cohort-topic/stream-util.ts +135 -87
- package/src/cohort-topic/threshold-crypto.ts +239 -239
- package/src/cohort-topic/topic-router.ts +77 -77
- package/src/dispute/arbitrator-selection.ts +138 -138
- package/src/dispute/cascade.ts +524 -524
- package/src/dispute/dispute-service.ts +11 -5
- package/src/dispute/invalidation.ts +625 -625
- package/src/inbound-authorization.ts +190 -190
- package/src/index.ts +52 -47
- package/src/libp2p-key-network.ts +1120 -958
- package/src/libp2p-node-base.ts +1675 -1591
- package/src/libp2p-node-rn.ts +30 -30
- package/src/libp2p-node.ts +36 -36
- package/src/logger.ts +19 -2
- package/src/matchmaking/aggregate-counts.ts +104 -104
- package/src/matchmaking/index.ts +20 -20
- package/src/matchmaking/module.ts +363 -363
- package/src/matchmaking/protocols.ts +51 -51
- package/src/matchmaking/provider-manager.ts +95 -95
- package/src/matchmaking/query-handler.ts +88 -88
- package/src/matchmaking/query-transport.ts +492 -492
- package/src/matchmaking/seeker-manager.ts +64 -64
- package/src/matchmaking/seeker-walk-client.ts +293 -293
- package/src/matchmaking/traffic-validation.ts +195 -195
- package/src/network/network-manager-service.ts +5 -0
- package/src/optimystic-node.ts +36 -0
- package/src/owned-block-seed.ts +53 -40
- package/src/peer-address-book.ts +149 -0
- package/src/protocol-limits.ts +33 -33
- package/src/reactivity/forwarder-host.ts +438 -438
- package/src/reactivity/index.ts +19 -19
- package/src/reactivity/notify-transport.ts +144 -144
- package/src/reactivity/origination-manager.ts +192 -192
- package/src/reactivity/protocols.ts +61 -61
- package/src/reactivity/push-state-gossip.ts +291 -291
- package/src/reactivity/recover-transport.ts +408 -408
- package/src/reactivity/rotation-rereg-scheduler.ts +256 -256
- package/src/reactivity/subscriber-registry.ts +96 -96
- package/src/reactivity/subscription-manager.ts +450 -450
- package/src/reactivity/topic-bytes.ts +37 -37
- package/src/repo/client.ts +12 -2
- package/src/repo/cluster-coordinator.ts +99 -3
- package/src/repo/coordinator-repo.ts +305 -82
- package/src/repo/types.ts +7 -7
- package/src/rn.ts +39 -34
- package/src/rpc-deadline.ts +45 -45
- package/src/storage/arachnode-partition.ts +74 -74
- package/src/storage/block-storage.ts +59 -6
- package/src/storage/cached-raw-storage.ts +180 -0
- package/src/storage/cached-store-driver.ts +859 -0
- package/src/storage/i-block-storage.ts +20 -1
- package/src/storage/i-kv-store.ts +8 -8
- package/src/storage/i-raw-storage.ts +12 -5
- package/src/storage/kv-raw-storage.ts +135 -135
- package/src/storage/memory-kv-store.ts +28 -28
- package/src/storage/memory-storage.ts +25 -25
- package/src/storage/memory-store-driver.ts +157 -157
- package/src/storage/raw-store-codec.ts +42 -42
- package/src/storage/raw-store-driver.ts +80 -80
- package/src/storage/ring-selector.ts +317 -317
- package/src/storage/ring-shift-coordinator.ts +271 -271
- package/src/storage/shared-cache-pool.ts +452 -0
- package/src/storage/storage-repo.ts +1014 -903
- package/src/testing/cohort-topic-mesh-harness.ts +663 -663
- package/src/testing/index.ts +8 -8
- package/src/testing/matchmaking-mesh-harness.ts +475 -475
- package/src/testing/raw-storage-conformance.ts +453 -397
- package/src/testing/reactivity-mesh-harness.ts +922 -922
- package/dist/src/storage/restoration-coordinator-v2.d.ts +0 -67
- package/dist/src/storage/restoration-coordinator-v2.d.ts.map +0 -1
- package/dist/src/storage/restoration-coordinator-v2.js +0 -172
- package/dist/src/storage/restoration-coordinator-v2.js.map +0 -1
|
@@ -1,293 +1,293 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* Matchmaking — seeker walk client (db-p2p, drives the hang-out-vs-continue walk over the substrate).
|
|
3
|
-
*
|
|
4
|
-
* `docs/matchmaking.md` §Hang-out vs. continue. This is the RPC orchestration on top of the pure
|
|
5
|
-
* db-core {@link decide} engine: starting at `d_max`, it registers the seeker, issues `QueryV1`,
|
|
6
|
-
* re-validates and dedupes returned providers, and on each `Accepted` reply asks {@link decide} whether
|
|
7
|
-
* to finish, hang out (renew + requery on the `requery_interval_ms` poll cadence), or escalate (withdraw
|
|
8
|
-
* + re-register one tier toward the root). At `d = 0` there is nowhere left to walk, so it hangs out for
|
|
9
|
-
* the remaining patience and returns whatever matched (possibly `< wantCount`).
|
|
10
|
-
*
|
|
11
|
-
* Patience is a single budget that **drains across hops** (`docs/matchmaking.md` §Patience budgeting):
|
|
12
|
-
* a wall-clock deadline is fixed at the start, so every walked tier and every hang-out poll consumes
|
|
13
|
-
* the same `patienceMs` — escalation makes the next tier's hang-out progressively less attractive, which
|
|
14
|
-
* is correct since the root is terminal.
|
|
15
|
-
*
|
|
16
|
-
* The transport, clock, sleep, and per-entry verifier are all injected, so the walk unit-tests without a
|
|
17
|
-
* live libp2p stack (mock-tier e2e is a documented follow-on). This client implements the **poll path**
|
|
18
|
-
* only; the arrival-push path (`pushOnArrival`) is a separate slice.
|
|
19
|
-
*
|
|
20
|
-
* Edge cases encoded here (`docs/matchmaking.md` §Edge cases — the rest live in {@link decide}):
|
|
21
|
-
* - **`topicTraffic` absent (case 1):** the cohort is treated as zero-rate; the seeker issues one query
|
|
22
|
-
* (immediate-match) and walks one tier toward the root **without** hanging out.
|
|
23
|
-
* - **Stale `arrivalsPerMin = 0` after an epoch change (case 2):** never withdraw on a single zero
|
|
24
|
-
* reading — the immediate `QueryV1` runs first; a cohort that actually holds `>= wantCount` providers
|
|
25
|
-
* resolves to `done`, and only a query that also yields below threshold escalates.
|
|
26
|
-
* - **`UnwillingCohort`/`UnwillingMember` (case 3):** the hang-out decision is never entered (no
|
|
27
|
-
* `Accepted`); the walk terminates with whatever matched and standard substrate back-off applies.
|
|
28
|
-
*/
|
|
29
|
-
|
|
30
|
-
import {
|
|
31
|
-
decide,
|
|
32
|
-
filterAcceptRatio,
|
|
33
|
-
matchesFilter,
|
|
34
|
-
newFilterAcceptRatioState,
|
|
35
|
-
observeYield,
|
|
36
|
-
verifyProviderEntry,
|
|
37
|
-
DEFAULT_HANG_OUT_CONFIG,
|
|
38
|
-
DEFAULT_MEAN_WANT_COUNT,
|
|
39
|
-
FILTER_ACCEPT_RATIO_INITIAL,
|
|
40
|
-
type CapabilityFilter,
|
|
41
|
-
type EntrySigVerifier,
|
|
42
|
-
type FilterAcceptRatioState,
|
|
43
|
-
type HangOutConfig,
|
|
44
|
-
type ProviderEntryV1,
|
|
45
|
-
type QueryReplyV1,
|
|
46
|
-
} from "@optimystic/db-core";
|
|
47
|
-
|
|
48
|
-
/** A seeker register/probe reply at a tree tier (the matchmaking-relevant subset of `RegisterReplyV1`). */
|
|
49
|
-
export interface SeekerProbeReply {
|
|
50
|
-
readonly result: "accepted" | "no_state" | "promoted" | "unwilling_member" | "unwilling_cohort";
|
|
51
|
-
/** Present on `accepted` and `promoted` (the substrate's barometer); absent triggers edge case 1. */
|
|
52
|
-
readonly topicTraffic?: QueryReplyV1["topicTraffic"];
|
|
53
|
-
/** Present on `promoted` — the tier to descend to. */
|
|
54
|
-
readonly targetTier?: number;
|
|
55
|
-
}
|
|
56
|
-
|
|
57
|
-
/** The substrate seam the walk drives: register/query/renew/withdraw against a tree tier. */
|
|
58
|
-
export interface SeekerWalkTransport {
|
|
59
|
-
/** Register (or re-register) the seeker at tree tier `treeTier`; resolves the probe reply. */
|
|
60
|
-
register(treeTier: number): Promise<SeekerProbeReply>;
|
|
61
|
-
/** Issue a `QueryV1` against the cohort the seeker is currently registered with. */
|
|
62
|
-
query(treeTier: number): Promise<QueryReplyV1>;
|
|
63
|
-
/** Renew the live seeker registration (hang-out keep-alive via TTL renewal). */
|
|
64
|
-
renew(): Promise<void>;
|
|
65
|
-
/** Withdraw the seeker registration before escalating (polite `RenewV1` TTL = 0; optional). */
|
|
66
|
-
withdraw(): Promise<void>;
|
|
67
|
-
}
|
|
68
|
-
|
|
69
|
-
/** Construction inputs for {@link SeekerWalkClient}. */
|
|
70
|
-
export interface SeekerWalkClientDeps {
|
|
71
|
-
readonly transport: SeekerWalkTransport;
|
|
72
|
-
/** The matchmaking topic id (used to re-validate each forwarded entry's `registrationSig`). */
|
|
73
|
-
readonly topicId: Uint8Array;
|
|
74
|
-
/** Providers the seeker needs. */
|
|
75
|
-
readonly wantCount: number;
|
|
76
|
-
/** The starting tree tier `d_max`. */
|
|
77
|
-
readonly dMax: number;
|
|
78
|
-
/** Total patience budget (ms); drains across hops + hang-out. */
|
|
79
|
-
readonly patienceMs: number;
|
|
80
|
-
/** Optional capability filter (re-applied seeker-side over the returned set). */
|
|
81
|
-
readonly filter?: CapabilityFilter;
|
|
82
|
-
/** Per-entry signature verifier (db-p2p binds `verifyPeerSig`). */
|
|
83
|
-
readonly verifyEntry: EntrySigVerifier;
|
|
84
|
-
/** Hang-out decision config. Default {@link DEFAULT_HANG_OUT_CONFIG}. */
|
|
85
|
-
readonly config?: HangOutConfig;
|
|
86
|
-
/** Assumed competing-seeker mean `wantCount`. Default {@link DEFAULT_MEAN_WANT_COUNT}. */
|
|
87
|
-
readonly meanWantCount?: number;
|
|
88
|
-
/** Starting `filterAcceptRatio` (refined per walk). Default {@link FILTER_ACCEPT_RATIO_INITIAL}. */
|
|
89
|
-
readonly filterAcceptRatioInitial?: number;
|
|
90
|
-
/** Wall clock (unix ms); injectable for tests. Default `Date.now`. */
|
|
91
|
-
readonly clock?: () => number;
|
|
92
|
-
/** Sleep for the requery cadence; injectable for tests. Default a real timer. */
|
|
93
|
-
readonly sleep?: (ms: number) => Promise<void>;
|
|
94
|
-
}
|
|
95
|
-
|
|
96
|
-
/** The result of a completed walk. */
|
|
97
|
-
export interface SeekerWalkResult {
|
|
98
|
-
/** Matched, re-validated, deduped providers (up to whatever accumulated; may be `< wantCount`). */
|
|
99
|
-
readonly providers: ProviderEntryV1[];
|
|
100
|
-
/** Whether `wantCount` was met. */
|
|
101
|
-
readonly metWantCount: boolean;
|
|
102
|
-
/** The tier the walk terminated at. */
|
|
103
|
-
readonly terminalTier: number;
|
|
104
|
-
/** Total register hops issued (probes + escalations + descends). */
|
|
105
|
-
readonly hops: number;
|
|
106
|
-
/**
|
|
107
|
-
* Max `topicTraffic.childCohortCount` observed across every `Accepted` reply this walk saw. `> 0`
|
|
108
|
-
* means the topic has promoted (it is hot), so the single-cohort sample is unrepresentative — the
|
|
109
|
-
* public seeker session / voting `QuorumDiscovery` binding uses this to decide whether to escalate to
|
|
110
|
-
* the multi-cohort sweep (`docs/matchmaking.md` §Multi-cohort sweep).
|
|
111
|
-
*/
|
|
112
|
-
readonly maxChildCohortCount: number;
|
|
113
|
-
}
|
|
114
|
-
|
|
115
|
-
/** Outcome of evaluating one `Accepted` tier. */
|
|
116
|
-
type AcceptedOutcome = "done" | "escalate" | "terminal";
|
|
117
|
-
|
|
118
|
-
/** Drives the seeker hang-out-vs-continue walk for one matchmaking topic. See the module header. */
|
|
119
|
-
export class SeekerWalkClient {
|
|
120
|
-
private readonly transport: SeekerWalkTransport;
|
|
121
|
-
private readonly topicId: Uint8Array;
|
|
122
|
-
private readonly wantCount: number;
|
|
123
|
-
private readonly dMax: number;
|
|
124
|
-
private readonly patienceMs: number;
|
|
125
|
-
private readonly filter?: CapabilityFilter;
|
|
126
|
-
private readonly verifyEntry: EntrySigVerifier;
|
|
127
|
-
private readonly config: HangOutConfig;
|
|
128
|
-
private readonly meanWantCount: number;
|
|
129
|
-
private readonly filterAcceptRatioInitial: number;
|
|
130
|
-
private readonly clock: () => number;
|
|
131
|
-
private readonly sleep: (ms: number) => Promise<void>;
|
|
132
|
-
|
|
133
|
-
/** Matched providers, deduped by `participantId` (a provider seen via two queries counts once). */
|
|
134
|
-
private readonly matched = new Map<string, ProviderEntryV1>();
|
|
135
|
-
private ratioState: FilterAcceptRatioState = newFilterAcceptRatioState();
|
|
136
|
-
private deadline = 0;
|
|
137
|
-
private hops = 0;
|
|
138
|
-
/** Max `childCohortCount` seen across `Accepted` replies — the hot-topic / sweep-escalation signal. */
|
|
139
|
-
private maxChildCohortCount = 0;
|
|
140
|
-
|
|
141
|
-
constructor(deps: SeekerWalkClientDeps) {
|
|
142
|
-
this.transport = deps.transport;
|
|
143
|
-
this.topicId = deps.topicId;
|
|
144
|
-
this.wantCount = deps.wantCount;
|
|
145
|
-
this.dMax = deps.dMax;
|
|
146
|
-
this.patienceMs = deps.patienceMs;
|
|
147
|
-
this.filter = deps.filter;
|
|
148
|
-
this.verifyEntry = deps.verifyEntry;
|
|
149
|
-
this.config = deps.config ?? DEFAULT_HANG_OUT_CONFIG;
|
|
150
|
-
this.meanWantCount = deps.meanWantCount ?? DEFAULT_MEAN_WANT_COUNT;
|
|
151
|
-
this.filterAcceptRatioInitial = deps.filterAcceptRatioInitial ?? FILTER_ACCEPT_RATIO_INITIAL;
|
|
152
|
-
this.clock = deps.clock ?? ((): number => Date.now());
|
|
153
|
-
this.sleep = deps.sleep ?? ((ms: number): Promise<void> => new Promise((resolve) => setTimeout(resolve, ms)));
|
|
154
|
-
}
|
|
155
|
-
|
|
156
|
-
/** Run the walk from `d_max` toward the root; resolves the matched providers + termination info. */
|
|
157
|
-
async run(): Promise<SeekerWalkResult> {
|
|
158
|
-
this.deadline = this.clock() + this.patienceMs;
|
|
159
|
-
let d = this.dMax;
|
|
160
|
-
|
|
161
|
-
while (true) {
|
|
162
|
-
this.hops++;
|
|
163
|
-
const reply = await this.transport.register(d);
|
|
164
|
-
switch (reply.result) {
|
|
165
|
-
case "no_state": {
|
|
166
|
-
if (d <= 0) {
|
|
167
|
-
return this.finish(d);
|
|
168
|
-
}
|
|
169
|
-
d -= 1;
|
|
170
|
-
continue;
|
|
171
|
-
}
|
|
172
|
-
case "unwilling_member":
|
|
173
|
-
case "unwilling_cohort": {
|
|
174
|
-
// Substrate-level refusal — never received Accepted, so hang-out is not entered (case 3).
|
|
175
|
-
return this.finish(d);
|
|
176
|
-
}
|
|
177
|
-
case "promoted": {
|
|
178
|
-
const target = reply.targetTier ?? d + 1;
|
|
179
|
-
if (target <= d) {
|
|
180
|
-
return this.finish(d);
|
|
181
|
-
}
|
|
182
|
-
d = target;
|
|
183
|
-
continue;
|
|
184
|
-
}
|
|
185
|
-
case "accepted": {
|
|
186
|
-
const outcome = await this.handleAccepted(d, reply.topicTraffic);
|
|
187
|
-
if (outcome === "done" || outcome === "terminal") {
|
|
188
|
-
return this.finish(d);
|
|
189
|
-
}
|
|
190
|
-
await this.transport.withdraw();
|
|
191
|
-
d -= 1;
|
|
192
|
-
continue;
|
|
193
|
-
}
|
|
194
|
-
}
|
|
195
|
-
}
|
|
196
|
-
}
|
|
197
|
-
|
|
198
|
-
/** Remaining patience (ms); the wall-clock deadline drains uniformly across hops + hang-out. */
|
|
199
|
-
private remaining(): number {
|
|
200
|
-
return Math.max(0, this.deadline - this.clock());
|
|
201
|
-
}
|
|
202
|
-
|
|
203
|
-
/** Filter, re-validate (`registrationSig`), and dedupe a query reply's providers into {@link matched}. */
|
|
204
|
-
private collect(reply: QueryReplyV1): void {
|
|
205
|
-
const returned = reply.providers ?? [];
|
|
206
|
-
let matchedThisQuery = 0;
|
|
207
|
-
for (const entry of returned) {
|
|
208
|
-
if (!matchesFilter(entry, this.filter)) {
|
|
209
|
-
continue;
|
|
210
|
-
}
|
|
211
|
-
if (!verifyProviderEntry(this.topicId, entry, this.verifyEntry)) {
|
|
212
|
-
continue;
|
|
213
|
-
}
|
|
214
|
-
matchedThisQuery++;
|
|
215
|
-
this.matched.set(entry.participantId, entry);
|
|
216
|
-
}
|
|
217
|
-
this.ratioState = observeYield(this.ratioState, matchedThisQuery, returned.length);
|
|
218
|
-
}
|
|
219
|
-
|
|
220
|
-
/** Evaluate one `Accepted` tier: immediate query, then {@link decide} → done / hangOut / escalate. */
|
|
221
|
-
private async handleAccepted(d: number, traffic: SeekerProbeReply["topicTraffic"]): Promise<AcceptedOutcome> {
|
|
222
|
-
// Record the hottest tier seen as soon as this Accepted reply's traffic is available — *before* the
|
|
223
|
-
// immediate-match/done short-circuit below, so the single-cohort-vs-sweep decision (public session /
|
|
224
|
-
// voting QuorumDiscovery binding) still sees a hot topic even when one cohort's query already met
|
|
225
|
-
// wantCount. Folding it only on the `decide` path would drop the signal for a small quorum that a
|
|
226
|
-
// single hot cohort satisfies, leaving the assembler with a prefix-biased sample it should sweep.
|
|
227
|
-
if (traffic !== undefined) {
|
|
228
|
-
this.maxChildCohortCount = Math.max(this.maxChildCohortCount, traffic.childCohortCount);
|
|
229
|
-
}
|
|
230
|
-
|
|
231
|
-
// Immediate-match check runs in every case — also satisfies edge case 2 (a stale arrivalsPerMin=0
|
|
232
|
-
// cohort that actually holds enough providers resolves here rather than escalating spuriously).
|
|
233
|
-
this.collect(await this.transport.query(d));
|
|
234
|
-
if (this.matched.size >= this.wantCount) {
|
|
235
|
-
return "done";
|
|
236
|
-
}
|
|
237
|
-
|
|
238
|
-
// Edge case 1: a reply without topicTraffic is treated as zero-rate — walk one tier toward the
|
|
239
|
-
// root without hanging out (no estimation against absent inputs).
|
|
240
|
-
if (traffic === undefined) {
|
|
241
|
-
return d <= 0 ? "terminal" : "escalate";
|
|
242
|
-
}
|
|
243
|
-
|
|
244
|
-
const decision = decide(
|
|
245
|
-
{
|
|
246
|
-
currentMatches: this.matched.size,
|
|
247
|
-
directParticipants: traffic.directParticipants,
|
|
248
|
-
arrivalsPerMin: traffic.arrivalsPerMin,
|
|
249
|
-
queriesPerMin: traffic.queriesPerMin,
|
|
250
|
-
childCohortCount: traffic.childCohortCount,
|
|
251
|
-
wantCount: this.wantCount,
|
|
252
|
-
patienceMsRemaining: this.remaining(),
|
|
253
|
-
filterAcceptRatio: filterAcceptRatio(this.ratioState, this.filterAcceptRatioInitial),
|
|
254
|
-
meanWantCount: this.meanWantCount,
|
|
255
|
-
},
|
|
256
|
-
this.config,
|
|
257
|
-
);
|
|
258
|
-
|
|
259
|
-
if (decision.action === "hangOut") {
|
|
260
|
-
await this.hangOut(d, decision.requeryIntervalMs);
|
|
261
|
-
if (this.matched.size >= this.wantCount) {
|
|
262
|
-
return "done";
|
|
263
|
-
}
|
|
264
|
-
return d <= 0 ? "terminal" : "escalate";
|
|
265
|
-
}
|
|
266
|
-
|
|
267
|
-
// escalate — but at the root there is nowhere to walk: hang out the remaining patience, then end.
|
|
268
|
-
if (d <= 0) {
|
|
269
|
-
await this.hangOut(d, this.config.requeryIntervalMs);
|
|
270
|
-
return "terminal";
|
|
271
|
-
}
|
|
272
|
-
return "escalate";
|
|
273
|
-
}
|
|
274
|
-
|
|
275
|
-
/** Keep the registration alive and re-query on the poll cadence until `wantCount` met or patience drains. */
|
|
276
|
-
private async hangOut(d: number, requeryIntervalMs: number): Promise<void> {
|
|
277
|
-
while (this.remaining() > 0 && this.matched.size < this.wantCount) {
|
|
278
|
-
await this.sleep(Math.min(requeryIntervalMs, this.remaining()));
|
|
279
|
-
await this.transport.renew();
|
|
280
|
-
this.collect(await this.transport.query(d));
|
|
281
|
-
}
|
|
282
|
-
}
|
|
283
|
-
|
|
284
|
-
private finish(terminalTier: number): SeekerWalkResult {
|
|
285
|
-
return {
|
|
286
|
-
providers: [...this.matched.values()],
|
|
287
|
-
metWantCount: this.matched.size >= this.wantCount,
|
|
288
|
-
terminalTier,
|
|
289
|
-
hops: this.hops,
|
|
290
|
-
maxChildCohortCount: this.maxChildCohortCount,
|
|
291
|
-
};
|
|
292
|
-
}
|
|
293
|
-
}
|
|
1
|
+
/**
|
|
2
|
+
* Matchmaking — seeker walk client (db-p2p, drives the hang-out-vs-continue walk over the substrate).
|
|
3
|
+
*
|
|
4
|
+
* `docs/matchmaking.md` §Hang-out vs. continue. This is the RPC orchestration on top of the pure
|
|
5
|
+
* db-core {@link decide} engine: starting at `d_max`, it registers the seeker, issues `QueryV1`,
|
|
6
|
+
* re-validates and dedupes returned providers, and on each `Accepted` reply asks {@link decide} whether
|
|
7
|
+
* to finish, hang out (renew + requery on the `requery_interval_ms` poll cadence), or escalate (withdraw
|
|
8
|
+
* + re-register one tier toward the root). At `d = 0` there is nowhere left to walk, so it hangs out for
|
|
9
|
+
* the remaining patience and returns whatever matched (possibly `< wantCount`).
|
|
10
|
+
*
|
|
11
|
+
* Patience is a single budget that **drains across hops** (`docs/matchmaking.md` §Patience budgeting):
|
|
12
|
+
* a wall-clock deadline is fixed at the start, so every walked tier and every hang-out poll consumes
|
|
13
|
+
* the same `patienceMs` — escalation makes the next tier's hang-out progressively less attractive, which
|
|
14
|
+
* is correct since the root is terminal.
|
|
15
|
+
*
|
|
16
|
+
* The transport, clock, sleep, and per-entry verifier are all injected, so the walk unit-tests without a
|
|
17
|
+
* live libp2p stack (mock-tier e2e is a documented follow-on). This client implements the **poll path**
|
|
18
|
+
* only; the arrival-push path (`pushOnArrival`) is a separate slice.
|
|
19
|
+
*
|
|
20
|
+
* Edge cases encoded here (`docs/matchmaking.md` §Edge cases — the rest live in {@link decide}):
|
|
21
|
+
* - **`topicTraffic` absent (case 1):** the cohort is treated as zero-rate; the seeker issues one query
|
|
22
|
+
* (immediate-match) and walks one tier toward the root **without** hanging out.
|
|
23
|
+
* - **Stale `arrivalsPerMin = 0` after an epoch change (case 2):** never withdraw on a single zero
|
|
24
|
+
* reading — the immediate `QueryV1` runs first; a cohort that actually holds `>= wantCount` providers
|
|
25
|
+
* resolves to `done`, and only a query that also yields below threshold escalates.
|
|
26
|
+
* - **`UnwillingCohort`/`UnwillingMember` (case 3):** the hang-out decision is never entered (no
|
|
27
|
+
* `Accepted`); the walk terminates with whatever matched and standard substrate back-off applies.
|
|
28
|
+
*/
|
|
29
|
+
|
|
30
|
+
import {
|
|
31
|
+
decide,
|
|
32
|
+
filterAcceptRatio,
|
|
33
|
+
matchesFilter,
|
|
34
|
+
newFilterAcceptRatioState,
|
|
35
|
+
observeYield,
|
|
36
|
+
verifyProviderEntry,
|
|
37
|
+
DEFAULT_HANG_OUT_CONFIG,
|
|
38
|
+
DEFAULT_MEAN_WANT_COUNT,
|
|
39
|
+
FILTER_ACCEPT_RATIO_INITIAL,
|
|
40
|
+
type CapabilityFilter,
|
|
41
|
+
type EntrySigVerifier,
|
|
42
|
+
type FilterAcceptRatioState,
|
|
43
|
+
type HangOutConfig,
|
|
44
|
+
type ProviderEntryV1,
|
|
45
|
+
type QueryReplyV1,
|
|
46
|
+
} from "@optimystic/db-core";
|
|
47
|
+
|
|
48
|
+
/** A seeker register/probe reply at a tree tier (the matchmaking-relevant subset of `RegisterReplyV1`). */
|
|
49
|
+
export interface SeekerProbeReply {
|
|
50
|
+
readonly result: "accepted" | "no_state" | "promoted" | "unwilling_member" | "unwilling_cohort";
|
|
51
|
+
/** Present on `accepted` and `promoted` (the substrate's barometer); absent triggers edge case 1. */
|
|
52
|
+
readonly topicTraffic?: QueryReplyV1["topicTraffic"];
|
|
53
|
+
/** Present on `promoted` — the tier to descend to. */
|
|
54
|
+
readonly targetTier?: number;
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
/** The substrate seam the walk drives: register/query/renew/withdraw against a tree tier. */
|
|
58
|
+
export interface SeekerWalkTransport {
|
|
59
|
+
/** Register (or re-register) the seeker at tree tier `treeTier`; resolves the probe reply. */
|
|
60
|
+
register(treeTier: number): Promise<SeekerProbeReply>;
|
|
61
|
+
/** Issue a `QueryV1` against the cohort the seeker is currently registered with. */
|
|
62
|
+
query(treeTier: number): Promise<QueryReplyV1>;
|
|
63
|
+
/** Renew the live seeker registration (hang-out keep-alive via TTL renewal). */
|
|
64
|
+
renew(): Promise<void>;
|
|
65
|
+
/** Withdraw the seeker registration before escalating (polite `RenewV1` TTL = 0; optional). */
|
|
66
|
+
withdraw(): Promise<void>;
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
/** Construction inputs for {@link SeekerWalkClient}. */
|
|
70
|
+
export interface SeekerWalkClientDeps {
|
|
71
|
+
readonly transport: SeekerWalkTransport;
|
|
72
|
+
/** The matchmaking topic id (used to re-validate each forwarded entry's `registrationSig`). */
|
|
73
|
+
readonly topicId: Uint8Array;
|
|
74
|
+
/** Providers the seeker needs. */
|
|
75
|
+
readonly wantCount: number;
|
|
76
|
+
/** The starting tree tier `d_max`. */
|
|
77
|
+
readonly dMax: number;
|
|
78
|
+
/** Total patience budget (ms); drains across hops + hang-out. */
|
|
79
|
+
readonly patienceMs: number;
|
|
80
|
+
/** Optional capability filter (re-applied seeker-side over the returned set). */
|
|
81
|
+
readonly filter?: CapabilityFilter;
|
|
82
|
+
/** Per-entry signature verifier (db-p2p binds `verifyPeerSig`). */
|
|
83
|
+
readonly verifyEntry: EntrySigVerifier;
|
|
84
|
+
/** Hang-out decision config. Default {@link DEFAULT_HANG_OUT_CONFIG}. */
|
|
85
|
+
readonly config?: HangOutConfig;
|
|
86
|
+
/** Assumed competing-seeker mean `wantCount`. Default {@link DEFAULT_MEAN_WANT_COUNT}. */
|
|
87
|
+
readonly meanWantCount?: number;
|
|
88
|
+
/** Starting `filterAcceptRatio` (refined per walk). Default {@link FILTER_ACCEPT_RATIO_INITIAL}. */
|
|
89
|
+
readonly filterAcceptRatioInitial?: number;
|
|
90
|
+
/** Wall clock (unix ms); injectable for tests. Default `Date.now`. */
|
|
91
|
+
readonly clock?: () => number;
|
|
92
|
+
/** Sleep for the requery cadence; injectable for tests. Default a real timer. */
|
|
93
|
+
readonly sleep?: (ms: number) => Promise<void>;
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
/** The result of a completed walk. */
|
|
97
|
+
export interface SeekerWalkResult {
|
|
98
|
+
/** Matched, re-validated, deduped providers (up to whatever accumulated; may be `< wantCount`). */
|
|
99
|
+
readonly providers: ProviderEntryV1[];
|
|
100
|
+
/** Whether `wantCount` was met. */
|
|
101
|
+
readonly metWantCount: boolean;
|
|
102
|
+
/** The tier the walk terminated at. */
|
|
103
|
+
readonly terminalTier: number;
|
|
104
|
+
/** Total register hops issued (probes + escalations + descends). */
|
|
105
|
+
readonly hops: number;
|
|
106
|
+
/**
|
|
107
|
+
* Max `topicTraffic.childCohortCount` observed across every `Accepted` reply this walk saw. `> 0`
|
|
108
|
+
* means the topic has promoted (it is hot), so the single-cohort sample is unrepresentative — the
|
|
109
|
+
* public seeker session / voting `QuorumDiscovery` binding uses this to decide whether to escalate to
|
|
110
|
+
* the multi-cohort sweep (`docs/matchmaking.md` §Multi-cohort sweep).
|
|
111
|
+
*/
|
|
112
|
+
readonly maxChildCohortCount: number;
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
/** Outcome of evaluating one `Accepted` tier. */
|
|
116
|
+
type AcceptedOutcome = "done" | "escalate" | "terminal";
|
|
117
|
+
|
|
118
|
+
/** Drives the seeker hang-out-vs-continue walk for one matchmaking topic. See the module header. */
|
|
119
|
+
export class SeekerWalkClient {
|
|
120
|
+
private readonly transport: SeekerWalkTransport;
|
|
121
|
+
private readonly topicId: Uint8Array;
|
|
122
|
+
private readonly wantCount: number;
|
|
123
|
+
private readonly dMax: number;
|
|
124
|
+
private readonly patienceMs: number;
|
|
125
|
+
private readonly filter?: CapabilityFilter;
|
|
126
|
+
private readonly verifyEntry: EntrySigVerifier;
|
|
127
|
+
private readonly config: HangOutConfig;
|
|
128
|
+
private readonly meanWantCount: number;
|
|
129
|
+
private readonly filterAcceptRatioInitial: number;
|
|
130
|
+
private readonly clock: () => number;
|
|
131
|
+
private readonly sleep: (ms: number) => Promise<void>;
|
|
132
|
+
|
|
133
|
+
/** Matched providers, deduped by `participantId` (a provider seen via two queries counts once). */
|
|
134
|
+
private readonly matched = new Map<string, ProviderEntryV1>();
|
|
135
|
+
private ratioState: FilterAcceptRatioState = newFilterAcceptRatioState();
|
|
136
|
+
private deadline = 0;
|
|
137
|
+
private hops = 0;
|
|
138
|
+
/** Max `childCohortCount` seen across `Accepted` replies — the hot-topic / sweep-escalation signal. */
|
|
139
|
+
private maxChildCohortCount = 0;
|
|
140
|
+
|
|
141
|
+
constructor(deps: SeekerWalkClientDeps) {
|
|
142
|
+
this.transport = deps.transport;
|
|
143
|
+
this.topicId = deps.topicId;
|
|
144
|
+
this.wantCount = deps.wantCount;
|
|
145
|
+
this.dMax = deps.dMax;
|
|
146
|
+
this.patienceMs = deps.patienceMs;
|
|
147
|
+
this.filter = deps.filter;
|
|
148
|
+
this.verifyEntry = deps.verifyEntry;
|
|
149
|
+
this.config = deps.config ?? DEFAULT_HANG_OUT_CONFIG;
|
|
150
|
+
this.meanWantCount = deps.meanWantCount ?? DEFAULT_MEAN_WANT_COUNT;
|
|
151
|
+
this.filterAcceptRatioInitial = deps.filterAcceptRatioInitial ?? FILTER_ACCEPT_RATIO_INITIAL;
|
|
152
|
+
this.clock = deps.clock ?? ((): number => Date.now());
|
|
153
|
+
this.sleep = deps.sleep ?? ((ms: number): Promise<void> => new Promise((resolve) => setTimeout(resolve, ms)));
|
|
154
|
+
}
|
|
155
|
+
|
|
156
|
+
/** Run the walk from `d_max` toward the root; resolves the matched providers + termination info. */
|
|
157
|
+
async run(): Promise<SeekerWalkResult> {
|
|
158
|
+
this.deadline = this.clock() + this.patienceMs;
|
|
159
|
+
let d = this.dMax;
|
|
160
|
+
|
|
161
|
+
while (true) {
|
|
162
|
+
this.hops++;
|
|
163
|
+
const reply = await this.transport.register(d);
|
|
164
|
+
switch (reply.result) {
|
|
165
|
+
case "no_state": {
|
|
166
|
+
if (d <= 0) {
|
|
167
|
+
return this.finish(d);
|
|
168
|
+
}
|
|
169
|
+
d -= 1;
|
|
170
|
+
continue;
|
|
171
|
+
}
|
|
172
|
+
case "unwilling_member":
|
|
173
|
+
case "unwilling_cohort": {
|
|
174
|
+
// Substrate-level refusal — never received Accepted, so hang-out is not entered (case 3).
|
|
175
|
+
return this.finish(d);
|
|
176
|
+
}
|
|
177
|
+
case "promoted": {
|
|
178
|
+
const target = reply.targetTier ?? d + 1;
|
|
179
|
+
if (target <= d) {
|
|
180
|
+
return this.finish(d);
|
|
181
|
+
}
|
|
182
|
+
d = target;
|
|
183
|
+
continue;
|
|
184
|
+
}
|
|
185
|
+
case "accepted": {
|
|
186
|
+
const outcome = await this.handleAccepted(d, reply.topicTraffic);
|
|
187
|
+
if (outcome === "done" || outcome === "terminal") {
|
|
188
|
+
return this.finish(d);
|
|
189
|
+
}
|
|
190
|
+
await this.transport.withdraw();
|
|
191
|
+
d -= 1;
|
|
192
|
+
continue;
|
|
193
|
+
}
|
|
194
|
+
}
|
|
195
|
+
}
|
|
196
|
+
}
|
|
197
|
+
|
|
198
|
+
/** Remaining patience (ms); the wall-clock deadline drains uniformly across hops + hang-out. */
|
|
199
|
+
private remaining(): number {
|
|
200
|
+
return Math.max(0, this.deadline - this.clock());
|
|
201
|
+
}
|
|
202
|
+
|
|
203
|
+
/** Filter, re-validate (`registrationSig`), and dedupe a query reply's providers into {@link matched}. */
|
|
204
|
+
private collect(reply: QueryReplyV1): void {
|
|
205
|
+
const returned = reply.providers ?? [];
|
|
206
|
+
let matchedThisQuery = 0;
|
|
207
|
+
for (const entry of returned) {
|
|
208
|
+
if (!matchesFilter(entry, this.filter)) {
|
|
209
|
+
continue;
|
|
210
|
+
}
|
|
211
|
+
if (!verifyProviderEntry(this.topicId, entry, this.verifyEntry)) {
|
|
212
|
+
continue;
|
|
213
|
+
}
|
|
214
|
+
matchedThisQuery++;
|
|
215
|
+
this.matched.set(entry.participantId, entry);
|
|
216
|
+
}
|
|
217
|
+
this.ratioState = observeYield(this.ratioState, matchedThisQuery, returned.length);
|
|
218
|
+
}
|
|
219
|
+
|
|
220
|
+
/** Evaluate one `Accepted` tier: immediate query, then {@link decide} → done / hangOut / escalate. */
|
|
221
|
+
private async handleAccepted(d: number, traffic: SeekerProbeReply["topicTraffic"]): Promise<AcceptedOutcome> {
|
|
222
|
+
// Record the hottest tier seen as soon as this Accepted reply's traffic is available — *before* the
|
|
223
|
+
// immediate-match/done short-circuit below, so the single-cohort-vs-sweep decision (public session /
|
|
224
|
+
// voting QuorumDiscovery binding) still sees a hot topic even when one cohort's query already met
|
|
225
|
+
// wantCount. Folding it only on the `decide` path would drop the signal for a small quorum that a
|
|
226
|
+
// single hot cohort satisfies, leaving the assembler with a prefix-biased sample it should sweep.
|
|
227
|
+
if (traffic !== undefined) {
|
|
228
|
+
this.maxChildCohortCount = Math.max(this.maxChildCohortCount, traffic.childCohortCount);
|
|
229
|
+
}
|
|
230
|
+
|
|
231
|
+
// Immediate-match check runs in every case — also satisfies edge case 2 (a stale arrivalsPerMin=0
|
|
232
|
+
// cohort that actually holds enough providers resolves here rather than escalating spuriously).
|
|
233
|
+
this.collect(await this.transport.query(d));
|
|
234
|
+
if (this.matched.size >= this.wantCount) {
|
|
235
|
+
return "done";
|
|
236
|
+
}
|
|
237
|
+
|
|
238
|
+
// Edge case 1: a reply without topicTraffic is treated as zero-rate — walk one tier toward the
|
|
239
|
+
// root without hanging out (no estimation against absent inputs).
|
|
240
|
+
if (traffic === undefined) {
|
|
241
|
+
return d <= 0 ? "terminal" : "escalate";
|
|
242
|
+
}
|
|
243
|
+
|
|
244
|
+
const decision = decide(
|
|
245
|
+
{
|
|
246
|
+
currentMatches: this.matched.size,
|
|
247
|
+
directParticipants: traffic.directParticipants,
|
|
248
|
+
arrivalsPerMin: traffic.arrivalsPerMin,
|
|
249
|
+
queriesPerMin: traffic.queriesPerMin,
|
|
250
|
+
childCohortCount: traffic.childCohortCount,
|
|
251
|
+
wantCount: this.wantCount,
|
|
252
|
+
patienceMsRemaining: this.remaining(),
|
|
253
|
+
filterAcceptRatio: filterAcceptRatio(this.ratioState, this.filterAcceptRatioInitial),
|
|
254
|
+
meanWantCount: this.meanWantCount,
|
|
255
|
+
},
|
|
256
|
+
this.config,
|
|
257
|
+
);
|
|
258
|
+
|
|
259
|
+
if (decision.action === "hangOut") {
|
|
260
|
+
await this.hangOut(d, decision.requeryIntervalMs);
|
|
261
|
+
if (this.matched.size >= this.wantCount) {
|
|
262
|
+
return "done";
|
|
263
|
+
}
|
|
264
|
+
return d <= 0 ? "terminal" : "escalate";
|
|
265
|
+
}
|
|
266
|
+
|
|
267
|
+
// escalate — but at the root there is nowhere to walk: hang out the remaining patience, then end.
|
|
268
|
+
if (d <= 0) {
|
|
269
|
+
await this.hangOut(d, this.config.requeryIntervalMs);
|
|
270
|
+
return "terminal";
|
|
271
|
+
}
|
|
272
|
+
return "escalate";
|
|
273
|
+
}
|
|
274
|
+
|
|
275
|
+
/** Keep the registration alive and re-query on the poll cadence until `wantCount` met or patience drains. */
|
|
276
|
+
private async hangOut(d: number, requeryIntervalMs: number): Promise<void> {
|
|
277
|
+
while (this.remaining() > 0 && this.matched.size < this.wantCount) {
|
|
278
|
+
await this.sleep(Math.min(requeryIntervalMs, this.remaining()));
|
|
279
|
+
await this.transport.renew();
|
|
280
|
+
this.collect(await this.transport.query(d));
|
|
281
|
+
}
|
|
282
|
+
}
|
|
283
|
+
|
|
284
|
+
private finish(terminalTier: number): SeekerWalkResult {
|
|
285
|
+
return {
|
|
286
|
+
providers: [...this.matched.values()],
|
|
287
|
+
metWantCount: this.matched.size >= this.wantCount,
|
|
288
|
+
terminalTier,
|
|
289
|
+
hops: this.hops,
|
|
290
|
+
maxChildCohortCount: this.maxChildCohortCount,
|
|
291
|
+
};
|
|
292
|
+
}
|
|
293
|
+
}
|