@optimystic/db-p2p 0.22.0 → 0.24.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.
Files changed (194) hide show
  1. package/dist/src/cluster/client.d.ts +10 -0
  2. package/dist/src/cluster/client.d.ts.map +1 -1
  3. package/dist/src/cluster/client.js +30 -1
  4. package/dist/src/cluster/client.js.map +1 -1
  5. package/dist/src/cluster/cluster-repo.d.ts +39 -14
  6. package/dist/src/cluster/cluster-repo.d.ts.map +1 -1
  7. package/dist/src/cluster/cluster-repo.js +226 -119
  8. package/dist/src/cluster/cluster-repo.js.map +1 -1
  9. package/dist/src/cluster/service.d.ts +32 -1
  10. package/dist/src/cluster/service.d.ts.map +1 -1
  11. package/dist/src/cluster/service.js +43 -2
  12. package/dist/src/cluster/service.js.map +1 -1
  13. package/dist/src/cohort-topic/host.js +34 -11
  14. package/dist/src/cohort-topic/host.js.map +1 -1
  15. package/dist/src/cohort-topic/stream-util.d.ts +37 -7
  16. package/dist/src/cohort-topic/stream-util.d.ts.map +1 -1
  17. package/dist/src/cohort-topic/stream-util.js +77 -19
  18. package/dist/src/cohort-topic/stream-util.js.map +1 -1
  19. package/dist/src/dispute/dispute-service.d.ts.map +1 -1
  20. package/dist/src/dispute/dispute-service.js +9 -3
  21. package/dist/src/dispute/dispute-service.js.map +1 -1
  22. package/dist/src/index.d.ts +3 -0
  23. package/dist/src/index.d.ts.map +1 -1
  24. package/dist/src/index.js +3 -0
  25. package/dist/src/index.js.map +1 -1
  26. package/dist/src/libp2p-key-network.d.ts +88 -2
  27. package/dist/src/libp2p-key-network.d.ts.map +1 -1
  28. package/dist/src/libp2p-key-network.js +134 -28
  29. package/dist/src/libp2p-key-network.js.map +1 -1
  30. package/dist/src/libp2p-node-base.d.ts.map +1 -1
  31. package/dist/src/libp2p-node-base.js +25 -1
  32. package/dist/src/libp2p-node-base.js.map +1 -1
  33. package/dist/src/logger.d.ts +17 -1
  34. package/dist/src/logger.d.ts.map +1 -1
  35. package/dist/src/logger.js +19 -2
  36. package/dist/src/logger.js.map +1 -1
  37. package/dist/src/matchmaking/query-transport.js +3 -3
  38. package/dist/src/matchmaking/query-transport.js.map +1 -1
  39. package/dist/src/owned-block-seed.d.ts +6 -3
  40. package/dist/src/owned-block-seed.d.ts.map +1 -1
  41. package/dist/src/owned-block-seed.js +16 -3
  42. package/dist/src/owned-block-seed.js.map +1 -1
  43. package/dist/src/peer-address-book.d.ts +72 -0
  44. package/dist/src/peer-address-book.d.ts.map +1 -0
  45. package/dist/src/peer-address-book.js +123 -0
  46. package/dist/src/peer-address-book.js.map +1 -0
  47. package/dist/src/reactivity/notify-transport.d.ts +4 -4
  48. package/dist/src/reactivity/notify-transport.js +6 -6
  49. package/dist/src/reactivity/notify-transport.js.map +1 -1
  50. package/dist/src/reactivity/push-state-gossip.js +2 -2
  51. package/dist/src/reactivity/push-state-gossip.js.map +1 -1
  52. package/dist/src/reactivity/recover-transport.d.ts +6 -2
  53. package/dist/src/reactivity/recover-transport.d.ts.map +1 -1
  54. package/dist/src/reactivity/recover-transport.js +7 -3
  55. package/dist/src/reactivity/recover-transport.js.map +1 -1
  56. package/dist/src/repo/client.d.ts.map +1 -1
  57. package/dist/src/repo/client.js +11 -2
  58. package/dist/src/repo/client.js.map +1 -1
  59. package/dist/src/repo/cluster-coordinator.d.ts +30 -0
  60. package/dist/src/repo/cluster-coordinator.d.ts.map +1 -1
  61. package/dist/src/repo/cluster-coordinator.js +95 -3
  62. package/dist/src/repo/cluster-coordinator.js.map +1 -1
  63. package/dist/src/repo/coordinator-repo.d.ts +62 -9
  64. package/dist/src/repo/coordinator-repo.d.ts.map +1 -1
  65. package/dist/src/repo/coordinator-repo.js +242 -73
  66. package/dist/src/repo/coordinator-repo.js.map +1 -1
  67. package/dist/src/rn.d.ts +3 -0
  68. package/dist/src/rn.d.ts.map +1 -1
  69. package/dist/src/rn.js +3 -0
  70. package/dist/src/rn.js.map +1 -1
  71. package/dist/src/storage/cached-raw-storage.d.ts +83 -0
  72. package/dist/src/storage/cached-raw-storage.d.ts.map +1 -0
  73. package/dist/src/storage/cached-raw-storage.js +152 -0
  74. package/dist/src/storage/cached-raw-storage.js.map +1 -0
  75. package/dist/src/storage/cached-store-driver.d.ts +186 -0
  76. package/dist/src/storage/cached-store-driver.d.ts.map +1 -0
  77. package/dist/src/storage/cached-store-driver.js +775 -0
  78. package/dist/src/storage/cached-store-driver.js.map +1 -0
  79. package/dist/src/storage/i-raw-storage.d.ts +12 -5
  80. package/dist/src/storage/i-raw-storage.d.ts.map +1 -1
  81. package/dist/src/storage/shared-cache-pool.d.ts +234 -0
  82. package/dist/src/storage/shared-cache-pool.d.ts.map +1 -0
  83. package/dist/src/storage/shared-cache-pool.js +354 -0
  84. package/dist/src/storage/shared-cache-pool.js.map +1 -0
  85. package/dist/src/testing/cohort-topic-mesh-harness.d.ts +13 -6
  86. package/dist/src/testing/cohort-topic-mesh-harness.d.ts.map +1 -1
  87. package/dist/src/testing/cohort-topic-mesh-harness.js +15 -6
  88. package/dist/src/testing/cohort-topic-mesh-harness.js.map +1 -1
  89. package/dist/src/testing/raw-storage-conformance.d.ts +2 -1
  90. package/dist/src/testing/raw-storage-conformance.d.ts.map +1 -1
  91. package/dist/src/testing/raw-storage-conformance.js +35 -2
  92. package/dist/src/testing/raw-storage-conformance.js.map +1 -1
  93. package/package.json +3 -3
  94. package/readme.md +668 -668
  95. package/src/cluster/block-transfer.ts +424 -424
  96. package/src/cluster/client.ts +119 -88
  97. package/src/cluster/cluster-error.ts +64 -64
  98. package/src/cluster/cluster-policy.ts +203 -203
  99. package/src/cluster/cluster-repo.ts +242 -122
  100. package/src/cluster/cluster-size-coupling.ts +45 -45
  101. package/src/cluster/commit-cert.ts +139 -139
  102. package/src/cluster/i-transaction-state-store.ts +43 -43
  103. package/src/cluster/memory-transaction-state-store.ts +56 -56
  104. package/src/cluster/peer-key-binding.ts +37 -37
  105. package/src/cluster/persistent-transaction-state-store.ts +92 -92
  106. package/src/cluster/quorum-restore.ts +223 -223
  107. package/src/cluster/reconcile-block.ts +203 -203
  108. package/src/cluster/service.ts +293 -241
  109. package/src/cluster/supermajority-coupling.ts +37 -37
  110. package/src/cohort-topic/bootstrap-evidence-builder.ts +122 -122
  111. package/src/cohort-topic/bootstrap-evidence-verifiers.ts +132 -132
  112. package/src/cohort-topic/bootstrap-parent-reference.ts +159 -159
  113. package/src/cohort-topic/change-bridge.ts +109 -109
  114. package/src/cohort-topic/cohort-gossip-driver.ts +231 -231
  115. package/src/cohort-topic/cohort-gossip-transport.ts +84 -84
  116. package/src/cohort-topic/fret-trust-anchor.ts +153 -153
  117. package/src/cohort-topic/host.ts +42 -11
  118. package/src/cohort-topic/index.ts +13 -13
  119. package/src/cohort-topic/membership-publish-sink.ts +20 -20
  120. package/src/cohort-topic/membership-source.ts +68 -68
  121. package/src/cohort-topic/peer-codec.ts +31 -31
  122. package/src/cohort-topic/peer-sig.ts +86 -86
  123. package/src/cohort-topic/protocols.ts +71 -71
  124. package/src/cohort-topic/reactivity-membership-gate.ts +77 -77
  125. package/src/cohort-topic/size-estimator.ts +16 -16
  126. package/src/cohort-topic/stream-util.ts +79 -19
  127. package/src/cohort-topic/threshold-crypto.ts +239 -239
  128. package/src/cohort-topic/topic-router.ts +77 -77
  129. package/src/dispute/arbitrator-selection.ts +138 -138
  130. package/src/dispute/cascade.ts +524 -524
  131. package/src/dispute/dispute-service.ts +11 -5
  132. package/src/dispute/invalidation.ts +625 -625
  133. package/src/inbound-authorization.ts +190 -190
  134. package/src/index.ts +52 -49
  135. package/src/libp2p-key-network.ts +1120 -990
  136. package/src/libp2p-node-base.ts +1675 -1651
  137. package/src/libp2p-node-rn.ts +30 -30
  138. package/src/libp2p-node.ts +36 -36
  139. package/src/logger.ts +19 -2
  140. package/src/matchmaking/aggregate-counts.ts +104 -104
  141. package/src/matchmaking/index.ts +20 -20
  142. package/src/matchmaking/module.ts +363 -363
  143. package/src/matchmaking/protocols.ts +51 -51
  144. package/src/matchmaking/provider-manager.ts +95 -95
  145. package/src/matchmaking/query-handler.ts +88 -88
  146. package/src/matchmaking/query-transport.ts +3 -3
  147. package/src/matchmaking/seeker-manager.ts +64 -64
  148. package/src/matchmaking/seeker-walk-client.ts +293 -293
  149. package/src/matchmaking/traffic-validation.ts +195 -195
  150. package/src/optimystic-node.ts +36 -36
  151. package/src/owned-block-seed.ts +53 -40
  152. package/src/peer-address-book.ts +149 -0
  153. package/src/protocol-limits.ts +33 -33
  154. package/src/reactivity/forwarder-host.ts +438 -438
  155. package/src/reactivity/index.ts +19 -19
  156. package/src/reactivity/notify-transport.ts +144 -144
  157. package/src/reactivity/origination-manager.ts +192 -192
  158. package/src/reactivity/protocols.ts +61 -61
  159. package/src/reactivity/push-state-gossip.ts +291 -291
  160. package/src/reactivity/recover-transport.ts +7 -3
  161. package/src/reactivity/rotation-rereg-scheduler.ts +256 -256
  162. package/src/reactivity/subscriber-registry.ts +96 -96
  163. package/src/reactivity/subscription-manager.ts +450 -450
  164. package/src/reactivity/topic-bytes.ts +37 -37
  165. package/src/repo/client.ts +12 -2
  166. package/src/repo/cluster-coordinator.ts +99 -3
  167. package/src/repo/coordinator-repo.ts +281 -74
  168. package/src/repo/types.ts +7 -7
  169. package/src/rn.ts +39 -36
  170. package/src/rpc-deadline.ts +45 -45
  171. package/src/storage/arachnode-partition.ts +74 -74
  172. package/src/storage/cached-raw-storage.ts +180 -0
  173. package/src/storage/cached-store-driver.ts +859 -0
  174. package/src/storage/i-kv-store.ts +8 -8
  175. package/src/storage/i-raw-storage.ts +12 -5
  176. package/src/storage/kv-raw-storage.ts +135 -135
  177. package/src/storage/memory-kv-store.ts +28 -28
  178. package/src/storage/memory-storage.ts +25 -25
  179. package/src/storage/memory-store-driver.ts +157 -157
  180. package/src/storage/raw-store-codec.ts +42 -42
  181. package/src/storage/raw-store-driver.ts +80 -80
  182. package/src/storage/ring-selector.ts +317 -317
  183. package/src/storage/ring-shift-coordinator.ts +271 -271
  184. package/src/storage/shared-cache-pool.ts +452 -0
  185. package/src/storage/storage-repo.ts +1014 -1014
  186. package/src/testing/cohort-topic-mesh-harness.ts +673 -663
  187. package/src/testing/index.ts +8 -8
  188. package/src/testing/matchmaking-mesh-harness.ts +475 -475
  189. package/src/testing/raw-storage-conformance.ts +453 -417
  190. package/src/testing/reactivity-mesh-harness.ts +922 -922
  191. package/dist/src/storage/restoration-coordinator-v2.d.ts +0 -67
  192. package/dist/src/storage/restoration-coordinator-v2.d.ts.map +0 -1
  193. package/dist/src/storage/restoration-coordinator-v2.js +0 -172
  194. package/dist/src/storage/restoration-coordinator-v2.js.map +0 -1
@@ -1,363 +1,363 @@
1
- /**
2
- * Matchmaking — cohesive public module (db-p2p, the cohort-topic-integrated entry points).
3
- *
4
- * `docs/matchmaking.md` §Overview / §Configuration names two public roles. This module composes the
5
- * lower db-core state + db-p2p substrate wiring (managers, seeker walk client, multi-cohort sweep) into
6
- * those roles, plus the `QuorumDiscovery` binding the voting plan consumes:
7
- *
8
- * - {@link MatchmakingProviderSession} — `register(topic, payload)` / `renew()` / `withdraw()` (plus
9
- * `setCapacity` / `signalFull` self-throttling), driving a {@link MatchmakingProvider} through the
10
- * {@link MatchmakingProviderManager} at cohort-topic tier **T2**.
11
- * - {@link MatchmakingSeekerSession} — `register(topic, payload)` (brief T2 registration), `query(q)`
12
- * (one-shot cohort query), and `walk(topic, want)` (the hang-out engine, escalating to the
13
- * multi-cohort sweep on a hot topic).
14
- * - {@link createMatchmakingQuorumDiscovery} — binds the db-core voting `QuorumDiscovery` port to the
15
- * seeker walk (single-cohort) + {@link runMultiCohortSweep} (sweep), so `VotingQuorumAssembler` runs
16
- * over real substrate I/O.
17
- *
18
- * **Naming note (honest):** the db-core `MatchmakingProvider` / `MatchmakingSeeker` are the
19
- * transport-free *state + signed-payload builders*; these `*Session` classes are the substrate-wired
20
- * *public entry points* the doc's `MatchmakingProvider` / `MatchmakingSeeker` sketch refers to. They are
21
- * named `*Session` to avoid colliding with the re-exported db-core state classes.
22
- *
23
- * The substrate I/O is **injected** — the seeker walk transport, the one-shot query, the `d_max`
24
- * estimate, and the sweep ports are all ports the FRET host binds. The module therefore unit-tests
25
- * without a live libp2p stack; the mock-tier e2e that drives these against a real promoted tree is a
26
- * documented follow-on (the same posture as the rest of the subsystem).
27
- */
28
-
29
- import {
30
- MatchmakingProvider,
31
- MatchmakingSeeker,
32
- createMatchTopicAnchor,
33
- runMultiCohortSweep,
34
- type MatchTopicAnchor,
35
- type MatchTopicKind,
36
- type CohortTopicService,
37
- type NodeProfile,
38
- type CapabilityFilter,
39
- type EntrySigVerifier,
40
- type HangOutConfig,
41
- type ProviderEntryV1,
42
- type QueryReplyV1,
43
- type QueryV1,
44
- type MultiCohortSweepPorts,
45
- type QuorumDiscovery,
46
- type QuorumDiscoveryRequest,
47
- type QuorumDiscoverySlice,
48
- } from "@optimystic/db-core";
49
- import { MatchmakingProviderManager } from "./provider-manager.js";
50
- import { MatchmakingSeekerManager } from "./seeker-manager.js";
51
- import { SeekerWalkClient, type SeekerWalkTransport } from "./seeker-walk-client.js";
52
-
53
- /** A matchmaking topic reference: `(kind, label)` resolved to `topicId = H(kind ‖ label ‖ "match")`. */
54
- export interface MatchTopicRef {
55
- readonly kind: MatchTopicKind;
56
- readonly label: string;
57
- }
58
-
59
- // --- provider session ----------------------------------------------------------------------------
60
-
61
- /** The provider registration inputs a {@link MatchmakingProviderSession.register} call carries. */
62
- export interface ProviderRegistrationInput {
63
- readonly capabilities: readonly string[];
64
- readonly capacityBudget: number;
65
- readonly contactHint: string;
66
- readonly serviceUntil?: number;
67
- }
68
-
69
- /** Construction inputs for a {@link MatchmakingProviderSession}. */
70
- export interface MatchmakingProviderSessionDeps {
71
- /** Participant-facing cohort-topic substrate API. */
72
- readonly service: CohortTopicService;
73
- /** Sign the provider registration image (db-p2p supplies the libp2p peer key). */
74
- readonly sign: (payload: Uint8Array) => Promise<string>;
75
- /** Topic anchor; defaults to db-core's ring-hash anchor. */
76
- readonly anchor?: MatchTopicAnchor;
77
- /** Node profile used to derive the provider TTL (Core 90 s / Edge 60 s). */
78
- readonly profile?: NodeProfile;
79
- /** Explicit provider TTL (ms); overrides the profile-derived TTL. */
80
- readonly ttlMs?: number;
81
- /** CSPRNG source for the provider correlation id (injectable for deterministic tests). */
82
- readonly randomBytes?: (n: number) => Uint8Array;
83
- }
84
-
85
- /**
86
- * Public provider entry point: registers a {@link MatchmakingProvider} at a topic (cohort-topic T2) and
87
- * drives its lifecycle. One session owns one live registration; calling {@link register} again
88
- * re-registers (e.g. at a different topic).
89
- */
90
- export class MatchmakingProviderSession {
91
- private readonly deps: MatchmakingProviderSessionDeps;
92
- private readonly anchor: MatchTopicAnchor;
93
- private manager?: MatchmakingProviderManager;
94
-
95
- constructor(deps: MatchmakingProviderSessionDeps) {
96
- this.deps = deps;
97
- this.anchor = deps.anchor ?? createMatchTopicAnchor();
98
- }
99
-
100
- /** The live registration handle, or `undefined` before the first {@link register}. */
101
- get registration(): MatchmakingProviderManager["registration"] {
102
- return this.manager?.registration;
103
- }
104
-
105
- /** Register the provider at `topic` with the signed payload, at cohort-topic tier T2. */
106
- async register(topic: MatchTopicRef, reg: ProviderRegistrationInput): Promise<void> {
107
- const topicId = this.anchor.topicId(topic.kind, topic.label);
108
- const provider = new MatchmakingProvider({
109
- topicId,
110
- capabilities: reg.capabilities,
111
- capacityBudget: reg.capacityBudget,
112
- contactHint: reg.contactHint,
113
- sign: this.deps.sign,
114
- ...(reg.serviceUntil !== undefined ? { serviceUntil: reg.serviceUntil } : {}),
115
- ...(this.deps.randomBytes !== undefined ? { randomBytes: this.deps.randomBytes } : {}),
116
- });
117
- this.manager = new MatchmakingProviderManager({
118
- service: this.deps.service,
119
- provider,
120
- ...(this.deps.ttlMs !== undefined ? { ttlMs: this.deps.ttlMs } : {}),
121
- ...(this.deps.profile !== undefined ? { profile: this.deps.profile } : {}),
122
- });
123
- await this.manager.register();
124
- }
125
-
126
- /** Run one renewal cycle (keep-alive). No-op before {@link register}. */
127
- async renew(): Promise<void> {
128
- await this.manager?.renew();
129
- }
130
-
131
- /** Update the live capacity budget and push it by re-registering (`RenewV1` cannot carry a payload). */
132
- async setCapacity(budget: number): Promise<void> {
133
- await this.requireManager().setCapacity(budget);
134
- }
135
-
136
- /** Signal "available but at capacity" (`capacityBudget = 0`) by re-registering. */
137
- async signalFull(): Promise<void> {
138
- await this.requireManager().signalFull();
139
- }
140
-
141
- /** Withdraw: sends a best-effort signed `RenewV1.withdraw` tombstone so the cohort frees the record
142
- * immediately (an optimization; the record otherwise TTL-expires). No-op before register. */
143
- async withdraw(): Promise<void> {
144
- await this.manager?.withdraw();
145
- }
146
-
147
- private requireManager(): MatchmakingProviderManager {
148
- if (this.manager === undefined) {
149
- throw new Error("MatchmakingProviderSession: register() must precede setCapacity()/signalFull()");
150
- }
151
- return this.manager;
152
- }
153
- }
154
-
155
- // --- seeker session ------------------------------------------------------------------------------
156
-
157
- /** The seeker registration inputs a {@link MatchmakingSeekerSession.register} call carries. */
158
- export interface SeekerRegistrationInput {
159
- readonly wantCount: number;
160
- readonly contactHint: string;
161
- readonly filter?: CapabilityFilter;
162
- readonly pushOnArrival?: boolean;
163
- }
164
-
165
- /** A {@link MatchmakingSeekerSession.walk} request — the hang-out engine's per-task knobs. */
166
- export interface SeekerWalkRequest {
167
- readonly wantCount: number;
168
- readonly patienceMs: number;
169
- readonly filter?: CapabilityFilter;
170
- /** Force the multi-cohort sweep even when the single-cohort walk would suffice (representativeness). */
171
- readonly preferSweep?: boolean;
172
- }
173
-
174
- /** The injected substrate-I/O seam a seeker session drives. The FRET host binds these to libp2p RPCs. */
175
- export interface MatchmakingSeekerSessionDeps {
176
- /** Participant-facing cohort-topic substrate API (seeker registration). */
177
- readonly service: CohortTopicService;
178
- /** Sign the seeker registration image. */
179
- readonly sign: (payload: Uint8Array) => Promise<string>;
180
- /** Per-entry signature verifier (db-p2p binds `verifyPeerSig`). */
181
- readonly verifyEntry: EntrySigVerifier;
182
- /** Topic anchor; defaults to db-core's ring-hash anchor. */
183
- readonly anchor?: MatchTopicAnchor;
184
- /** Build the seeker walk transport (register/query/renew/withdraw at a tier) for a topic. */
185
- readonly walkTransport: (topicId: Uint8Array) => SeekerWalkTransport;
186
- /** Issue a one-shot `QueryV1` (resolves the cohort from `q.topicId`). */
187
- readonly queryCohort: (q: QueryV1) => Promise<QueryReplyV1>;
188
- /** Estimate `d_max` for a topic (the size-estimator seam). */
189
- readonly estimateDMax: (topicId: Uint8Array) => Promise<number>;
190
- /** Build the multi-cohort sweep ports for a topic; absent ⇒ the sweep is unavailable (walk only). */
191
- readonly sweepPorts?: (topicId: Uint8Array) => MultiCohortSweepPorts;
192
- /** Seeker registration TTL (ms); default {@link MatchmakingSeekerManager}'s seeker TTL. */
193
- readonly ttlMs?: number;
194
- /** Hang-out decision config (default {@link import("@optimystic/db-core").DEFAULT_HANG_OUT_CONFIG}). */
195
- readonly config?: HangOutConfig;
196
- /** Assumed competing-seeker mean `wantCount`. */
197
- readonly meanWantCount?: number;
198
- /** Wall clock (unix ms); injectable for tests. */
199
- readonly clock?: () => number;
200
- /** Sleep for the requery cadence; injectable for tests. */
201
- readonly sleep?: (ms: number) => Promise<void>;
202
- }
203
-
204
- /**
205
- * Public seeker entry point: brief registration, one-shot query, and the hang-out `walk` (which escalates
206
- * to the multi-cohort sweep on a hot topic). One session may register and walk independently.
207
- */
208
- export class MatchmakingSeekerSession {
209
- private readonly deps: MatchmakingSeekerSessionDeps;
210
- private readonly anchor: MatchTopicAnchor;
211
- private manager?: MatchmakingSeekerManager;
212
-
213
- constructor(deps: MatchmakingSeekerSessionDeps) {
214
- this.deps = deps;
215
- this.anchor = deps.anchor ?? createMatchTopicAnchor();
216
- }
217
-
218
- /** Resolve the `topicId` for a topic ref. */
219
- topicIdFor(topic: MatchTopicRef): Uint8Array {
220
- return this.anchor.topicId(topic.kind, topic.label);
221
- }
222
-
223
- /** Register the seeker briefly at `topic` (cohort-topic T2, short TTL). */
224
- async register(topic: MatchTopicRef, reg: SeekerRegistrationInput): Promise<void> {
225
- const topicId = this.topicIdFor(topic);
226
- const seeker = new MatchmakingSeeker({
227
- topicId,
228
- wantCount: reg.wantCount,
229
- contactHint: reg.contactHint,
230
- sign: this.deps.sign,
231
- ...(reg.filter !== undefined ? { filter: reg.filter } : {}),
232
- ...(reg.pushOnArrival !== undefined ? { pushOnArrival: reg.pushOnArrival } : {}),
233
- });
234
- this.manager = new MatchmakingSeekerManager({
235
- service: this.deps.service,
236
- seeker,
237
- ...(this.deps.ttlMs !== undefined ? { ttlMs: this.deps.ttlMs } : {}),
238
- });
239
- await this.manager.register();
240
- }
241
-
242
- /** Drop the seeker registration: sends a best-effort tombstone so the cohort frees the record
243
- * immediately (the record otherwise TTL-expires). No-op before register. */
244
- async withdraw(): Promise<void> {
245
- await this.manager?.withdraw();
246
- }
247
-
248
- /** Issue a one-shot `QueryV1` against the topic's cohort (resolved from `q.topicId`). */
249
- async query(q: QueryV1): Promise<QueryReplyV1> {
250
- return this.deps.queryCohort(q);
251
- }
252
-
253
- /**
254
- * Run the hang-out walk for `topic`, then — on a hot topic (`childCohortCount > 0`) or when
255
- * `preferSweep` is set, and when the sweep ports are bound — escalate to the multi-cohort sweep and
256
- * union the deduped providers. Returns the assembled (possibly `< wantCount`) provider set.
257
- */
258
- async walk(topic: MatchTopicRef, want: SeekerWalkRequest): Promise<ProviderEntryV1[]> {
259
- const topicId = this.topicIdFor(topic);
260
- const dMax = await this.deps.estimateDMax(topicId);
261
- const client = new SeekerWalkClient({
262
- transport: this.deps.walkTransport(topicId),
263
- topicId,
264
- wantCount: want.wantCount,
265
- dMax,
266
- patienceMs: want.patienceMs,
267
- verifyEntry: this.deps.verifyEntry,
268
- ...(want.filter !== undefined ? { filter: want.filter } : {}),
269
- ...(this.deps.config !== undefined ? { config: this.deps.config } : {}),
270
- ...(this.deps.meanWantCount !== undefined ? { meanWantCount: this.deps.meanWantCount } : {}),
271
- ...(this.deps.clock !== undefined ? { clock: this.deps.clock } : {}),
272
- ...(this.deps.sleep !== undefined ? { sleep: this.deps.sleep } : {}),
273
- });
274
- const result = await client.run();
275
- const providers = new Map<string, ProviderEntryV1>(result.providers.map((p) => [p.participantId, p]));
276
-
277
- const hot = result.maxChildCohortCount > 0;
278
- const wantSweep = want.preferSweep === true || (hot && providers.size < want.wantCount);
279
- if (wantSweep && this.deps.sweepPorts !== undefined) {
280
- // Pass want.patienceMs as a fresh (coarser) bound — this path does not track a draining
281
- // remainder (SeekerWalkClient ran on its own deadline). Still strictly better than unbounded.
282
- const sweep = await runMultiCohortSweep(this.deps.sweepPorts(topicId), {
283
- topicId,
284
- wantCount: want.wantCount,
285
- verifyEntry: this.deps.verifyEntry,
286
- patienceMs: want.patienceMs,
287
- ...(want.filter !== undefined ? { filter: want.filter } : {}),
288
- ...(this.deps.clock !== undefined ? { clock: this.deps.clock } : {}),
289
- });
290
- for (const entry of sweep.providers) {
291
- providers.set(entry.participantId, entry);
292
- }
293
- }
294
- return [...providers.values()];
295
- }
296
- }
297
-
298
- // --- voting quorum-discovery binding -------------------------------------------------------------
299
-
300
- /** Construction inputs for {@link createMatchmakingQuorumDiscovery}. */
301
- export interface MatchmakingQuorumDiscoveryDeps {
302
- /** Per-entry signature verifier (db-p2p binds `verifyPeerSig`). */
303
- readonly verifyEntry: EntrySigVerifier;
304
- /** Build the seeker walk transport for a topic. */
305
- readonly walkTransport: (topicId: Uint8Array) => SeekerWalkTransport;
306
- /** Estimate `d_max` for a topic. */
307
- readonly estimateDMax: (topicId: Uint8Array) => Promise<number>;
308
- /** Build the multi-cohort sweep ports for a topic. */
309
- readonly sweepPorts: (topicId: Uint8Array) => MultiCohortSweepPorts;
310
- /** Optional capability filter applied to both the walk and the sweep. */
311
- readonly filter?: CapabilityFilter;
312
- /** Hang-out decision config (passed to the walk). */
313
- readonly config?: HangOutConfig;
314
- /** Assumed competing-seeker mean `wantCount` (passed to the walk). */
315
- readonly meanWantCount?: number;
316
- /** Wall clock (unix ms); injectable for tests. */
317
- readonly clock?: () => number;
318
- /** Sleep for the requery cadence; injectable for tests. */
319
- readonly sleep?: (ms: number) => Promise<void>;
320
- }
321
-
322
- /**
323
- * Bind the db-core voting {@link QuorumDiscovery} port (`docs/matchmaking.md` §Voting-quorum assembly)
324
- * to the matchmaking substrate: `walk` runs the single-cohort hang-out walk and surfaces its hotness
325
- * signal (`childCohortCount`); `sweep` runs {@link runMultiCohortSweep} across the high-population tier
326
- * shards. `VotingQuorumAssembler` composes the two — walk, then sweep on a hot topic — re-validating
327
- * every entry itself, so this binding stays a thin transport adapter.
328
- */
329
- export function createMatchmakingQuorumDiscovery(deps: MatchmakingQuorumDiscoveryDeps): QuorumDiscovery {
330
- return {
331
- async walk(req: QuorumDiscoveryRequest): Promise<QuorumDiscoverySlice> {
332
- const dMax = await deps.estimateDMax(req.topicId);
333
- const client = new SeekerWalkClient({
334
- transport: deps.walkTransport(req.topicId),
335
- topicId: req.topicId,
336
- wantCount: req.wantCount,
337
- dMax,
338
- patienceMs: req.patienceMs,
339
- verifyEntry: deps.verifyEntry,
340
- ...(deps.filter !== undefined ? { filter: deps.filter } : {}),
341
- ...(deps.config !== undefined ? { config: deps.config } : {}),
342
- ...(deps.meanWantCount !== undefined ? { meanWantCount: deps.meanWantCount } : {}),
343
- ...(deps.clock !== undefined ? { clock: deps.clock } : {}),
344
- ...(deps.sleep !== undefined ? { sleep: deps.sleep } : {}),
345
- });
346
- const result = await client.run();
347
- return { entries: result.providers, childCohortCount: result.maxChildCohortCount };
348
- },
349
- async sweep(req: QuorumDiscoveryRequest): Promise<QuorumDiscoverySlice> {
350
- const result = await runMultiCohortSweep(deps.sweepPorts(req.topicId), {
351
- topicId: req.topicId,
352
- wantCount: req.wantCount,
353
- verifyEntry: deps.verifyEntry,
354
- patienceMs: req.patienceMs,
355
- ...(deps.filter !== undefined ? { filter: deps.filter } : {}),
356
- ...(deps.clock !== undefined ? { clock: deps.clock } : {}),
357
- });
358
- // The sweep hop has already crossed the ring — its childCohortCount is moot (the escalation
359
- // decision is made). Report 0 so the assembler never double-escalates.
360
- return { entries: result.providers, childCohortCount: 0 };
361
- },
362
- };
363
- }
1
+ /**
2
+ * Matchmaking — cohesive public module (db-p2p, the cohort-topic-integrated entry points).
3
+ *
4
+ * `docs/matchmaking.md` §Overview / §Configuration names two public roles. This module composes the
5
+ * lower db-core state + db-p2p substrate wiring (managers, seeker walk client, multi-cohort sweep) into
6
+ * those roles, plus the `QuorumDiscovery` binding the voting plan consumes:
7
+ *
8
+ * - {@link MatchmakingProviderSession} — `register(topic, payload)` / `renew()` / `withdraw()` (plus
9
+ * `setCapacity` / `signalFull` self-throttling), driving a {@link MatchmakingProvider} through the
10
+ * {@link MatchmakingProviderManager} at cohort-topic tier **T2**.
11
+ * - {@link MatchmakingSeekerSession} — `register(topic, payload)` (brief T2 registration), `query(q)`
12
+ * (one-shot cohort query), and `walk(topic, want)` (the hang-out engine, escalating to the
13
+ * multi-cohort sweep on a hot topic).
14
+ * - {@link createMatchmakingQuorumDiscovery} — binds the db-core voting `QuorumDiscovery` port to the
15
+ * seeker walk (single-cohort) + {@link runMultiCohortSweep} (sweep), so `VotingQuorumAssembler` runs
16
+ * over real substrate I/O.
17
+ *
18
+ * **Naming note (honest):** the db-core `MatchmakingProvider` / `MatchmakingSeeker` are the
19
+ * transport-free *state + signed-payload builders*; these `*Session` classes are the substrate-wired
20
+ * *public entry points* the doc's `MatchmakingProvider` / `MatchmakingSeeker` sketch refers to. They are
21
+ * named `*Session` to avoid colliding with the re-exported db-core state classes.
22
+ *
23
+ * The substrate I/O is **injected** — the seeker walk transport, the one-shot query, the `d_max`
24
+ * estimate, and the sweep ports are all ports the FRET host binds. The module therefore unit-tests
25
+ * without a live libp2p stack; the mock-tier e2e that drives these against a real promoted tree is a
26
+ * documented follow-on (the same posture as the rest of the subsystem).
27
+ */
28
+
29
+ import {
30
+ MatchmakingProvider,
31
+ MatchmakingSeeker,
32
+ createMatchTopicAnchor,
33
+ runMultiCohortSweep,
34
+ type MatchTopicAnchor,
35
+ type MatchTopicKind,
36
+ type CohortTopicService,
37
+ type NodeProfile,
38
+ type CapabilityFilter,
39
+ type EntrySigVerifier,
40
+ type HangOutConfig,
41
+ type ProviderEntryV1,
42
+ type QueryReplyV1,
43
+ type QueryV1,
44
+ type MultiCohortSweepPorts,
45
+ type QuorumDiscovery,
46
+ type QuorumDiscoveryRequest,
47
+ type QuorumDiscoverySlice,
48
+ } from "@optimystic/db-core";
49
+ import { MatchmakingProviderManager } from "./provider-manager.js";
50
+ import { MatchmakingSeekerManager } from "./seeker-manager.js";
51
+ import { SeekerWalkClient, type SeekerWalkTransport } from "./seeker-walk-client.js";
52
+
53
+ /** A matchmaking topic reference: `(kind, label)` resolved to `topicId = H(kind ‖ label ‖ "match")`. */
54
+ export interface MatchTopicRef {
55
+ readonly kind: MatchTopicKind;
56
+ readonly label: string;
57
+ }
58
+
59
+ // --- provider session ----------------------------------------------------------------------------
60
+
61
+ /** The provider registration inputs a {@link MatchmakingProviderSession.register} call carries. */
62
+ export interface ProviderRegistrationInput {
63
+ readonly capabilities: readonly string[];
64
+ readonly capacityBudget: number;
65
+ readonly contactHint: string;
66
+ readonly serviceUntil?: number;
67
+ }
68
+
69
+ /** Construction inputs for a {@link MatchmakingProviderSession}. */
70
+ export interface MatchmakingProviderSessionDeps {
71
+ /** Participant-facing cohort-topic substrate API. */
72
+ readonly service: CohortTopicService;
73
+ /** Sign the provider registration image (db-p2p supplies the libp2p peer key). */
74
+ readonly sign: (payload: Uint8Array) => Promise<string>;
75
+ /** Topic anchor; defaults to db-core's ring-hash anchor. */
76
+ readonly anchor?: MatchTopicAnchor;
77
+ /** Node profile used to derive the provider TTL (Core 90 s / Edge 60 s). */
78
+ readonly profile?: NodeProfile;
79
+ /** Explicit provider TTL (ms); overrides the profile-derived TTL. */
80
+ readonly ttlMs?: number;
81
+ /** CSPRNG source for the provider correlation id (injectable for deterministic tests). */
82
+ readonly randomBytes?: (n: number) => Uint8Array;
83
+ }
84
+
85
+ /**
86
+ * Public provider entry point: registers a {@link MatchmakingProvider} at a topic (cohort-topic T2) and
87
+ * drives its lifecycle. One session owns one live registration; calling {@link register} again
88
+ * re-registers (e.g. at a different topic).
89
+ */
90
+ export class MatchmakingProviderSession {
91
+ private readonly deps: MatchmakingProviderSessionDeps;
92
+ private readonly anchor: MatchTopicAnchor;
93
+ private manager?: MatchmakingProviderManager;
94
+
95
+ constructor(deps: MatchmakingProviderSessionDeps) {
96
+ this.deps = deps;
97
+ this.anchor = deps.anchor ?? createMatchTopicAnchor();
98
+ }
99
+
100
+ /** The live registration handle, or `undefined` before the first {@link register}. */
101
+ get registration(): MatchmakingProviderManager["registration"] {
102
+ return this.manager?.registration;
103
+ }
104
+
105
+ /** Register the provider at `topic` with the signed payload, at cohort-topic tier T2. */
106
+ async register(topic: MatchTopicRef, reg: ProviderRegistrationInput): Promise<void> {
107
+ const topicId = this.anchor.topicId(topic.kind, topic.label);
108
+ const provider = new MatchmakingProvider({
109
+ topicId,
110
+ capabilities: reg.capabilities,
111
+ capacityBudget: reg.capacityBudget,
112
+ contactHint: reg.contactHint,
113
+ sign: this.deps.sign,
114
+ ...(reg.serviceUntil !== undefined ? { serviceUntil: reg.serviceUntil } : {}),
115
+ ...(this.deps.randomBytes !== undefined ? { randomBytes: this.deps.randomBytes } : {}),
116
+ });
117
+ this.manager = new MatchmakingProviderManager({
118
+ service: this.deps.service,
119
+ provider,
120
+ ...(this.deps.ttlMs !== undefined ? { ttlMs: this.deps.ttlMs } : {}),
121
+ ...(this.deps.profile !== undefined ? { profile: this.deps.profile } : {}),
122
+ });
123
+ await this.manager.register();
124
+ }
125
+
126
+ /** Run one renewal cycle (keep-alive). No-op before {@link register}. */
127
+ async renew(): Promise<void> {
128
+ await this.manager?.renew();
129
+ }
130
+
131
+ /** Update the live capacity budget and push it by re-registering (`RenewV1` cannot carry a payload). */
132
+ async setCapacity(budget: number): Promise<void> {
133
+ await this.requireManager().setCapacity(budget);
134
+ }
135
+
136
+ /** Signal "available but at capacity" (`capacityBudget = 0`) by re-registering. */
137
+ async signalFull(): Promise<void> {
138
+ await this.requireManager().signalFull();
139
+ }
140
+
141
+ /** Withdraw: sends a best-effort signed `RenewV1.withdraw` tombstone so the cohort frees the record
142
+ * immediately (an optimization; the record otherwise TTL-expires). No-op before register. */
143
+ async withdraw(): Promise<void> {
144
+ await this.manager?.withdraw();
145
+ }
146
+
147
+ private requireManager(): MatchmakingProviderManager {
148
+ if (this.manager === undefined) {
149
+ throw new Error("MatchmakingProviderSession: register() must precede setCapacity()/signalFull()");
150
+ }
151
+ return this.manager;
152
+ }
153
+ }
154
+
155
+ // --- seeker session ------------------------------------------------------------------------------
156
+
157
+ /** The seeker registration inputs a {@link MatchmakingSeekerSession.register} call carries. */
158
+ export interface SeekerRegistrationInput {
159
+ readonly wantCount: number;
160
+ readonly contactHint: string;
161
+ readonly filter?: CapabilityFilter;
162
+ readonly pushOnArrival?: boolean;
163
+ }
164
+
165
+ /** A {@link MatchmakingSeekerSession.walk} request — the hang-out engine's per-task knobs. */
166
+ export interface SeekerWalkRequest {
167
+ readonly wantCount: number;
168
+ readonly patienceMs: number;
169
+ readonly filter?: CapabilityFilter;
170
+ /** Force the multi-cohort sweep even when the single-cohort walk would suffice (representativeness). */
171
+ readonly preferSweep?: boolean;
172
+ }
173
+
174
+ /** The injected substrate-I/O seam a seeker session drives. The FRET host binds these to libp2p RPCs. */
175
+ export interface MatchmakingSeekerSessionDeps {
176
+ /** Participant-facing cohort-topic substrate API (seeker registration). */
177
+ readonly service: CohortTopicService;
178
+ /** Sign the seeker registration image. */
179
+ readonly sign: (payload: Uint8Array) => Promise<string>;
180
+ /** Per-entry signature verifier (db-p2p binds `verifyPeerSig`). */
181
+ readonly verifyEntry: EntrySigVerifier;
182
+ /** Topic anchor; defaults to db-core's ring-hash anchor. */
183
+ readonly anchor?: MatchTopicAnchor;
184
+ /** Build the seeker walk transport (register/query/renew/withdraw at a tier) for a topic. */
185
+ readonly walkTransport: (topicId: Uint8Array) => SeekerWalkTransport;
186
+ /** Issue a one-shot `QueryV1` (resolves the cohort from `q.topicId`). */
187
+ readonly queryCohort: (q: QueryV1) => Promise<QueryReplyV1>;
188
+ /** Estimate `d_max` for a topic (the size-estimator seam). */
189
+ readonly estimateDMax: (topicId: Uint8Array) => Promise<number>;
190
+ /** Build the multi-cohort sweep ports for a topic; absent ⇒ the sweep is unavailable (walk only). */
191
+ readonly sweepPorts?: (topicId: Uint8Array) => MultiCohortSweepPorts;
192
+ /** Seeker registration TTL (ms); default {@link MatchmakingSeekerManager}'s seeker TTL. */
193
+ readonly ttlMs?: number;
194
+ /** Hang-out decision config (default {@link import("@optimystic/db-core").DEFAULT_HANG_OUT_CONFIG}). */
195
+ readonly config?: HangOutConfig;
196
+ /** Assumed competing-seeker mean `wantCount`. */
197
+ readonly meanWantCount?: number;
198
+ /** Wall clock (unix ms); injectable for tests. */
199
+ readonly clock?: () => number;
200
+ /** Sleep for the requery cadence; injectable for tests. */
201
+ readonly sleep?: (ms: number) => Promise<void>;
202
+ }
203
+
204
+ /**
205
+ * Public seeker entry point: brief registration, one-shot query, and the hang-out `walk` (which escalates
206
+ * to the multi-cohort sweep on a hot topic). One session may register and walk independently.
207
+ */
208
+ export class MatchmakingSeekerSession {
209
+ private readonly deps: MatchmakingSeekerSessionDeps;
210
+ private readonly anchor: MatchTopicAnchor;
211
+ private manager?: MatchmakingSeekerManager;
212
+
213
+ constructor(deps: MatchmakingSeekerSessionDeps) {
214
+ this.deps = deps;
215
+ this.anchor = deps.anchor ?? createMatchTopicAnchor();
216
+ }
217
+
218
+ /** Resolve the `topicId` for a topic ref. */
219
+ topicIdFor(topic: MatchTopicRef): Uint8Array {
220
+ return this.anchor.topicId(topic.kind, topic.label);
221
+ }
222
+
223
+ /** Register the seeker briefly at `topic` (cohort-topic T2, short TTL). */
224
+ async register(topic: MatchTopicRef, reg: SeekerRegistrationInput): Promise<void> {
225
+ const topicId = this.topicIdFor(topic);
226
+ const seeker = new MatchmakingSeeker({
227
+ topicId,
228
+ wantCount: reg.wantCount,
229
+ contactHint: reg.contactHint,
230
+ sign: this.deps.sign,
231
+ ...(reg.filter !== undefined ? { filter: reg.filter } : {}),
232
+ ...(reg.pushOnArrival !== undefined ? { pushOnArrival: reg.pushOnArrival } : {}),
233
+ });
234
+ this.manager = new MatchmakingSeekerManager({
235
+ service: this.deps.service,
236
+ seeker,
237
+ ...(this.deps.ttlMs !== undefined ? { ttlMs: this.deps.ttlMs } : {}),
238
+ });
239
+ await this.manager.register();
240
+ }
241
+
242
+ /** Drop the seeker registration: sends a best-effort tombstone so the cohort frees the record
243
+ * immediately (the record otherwise TTL-expires). No-op before register. */
244
+ async withdraw(): Promise<void> {
245
+ await this.manager?.withdraw();
246
+ }
247
+
248
+ /** Issue a one-shot `QueryV1` against the topic's cohort (resolved from `q.topicId`). */
249
+ async query(q: QueryV1): Promise<QueryReplyV1> {
250
+ return this.deps.queryCohort(q);
251
+ }
252
+
253
+ /**
254
+ * Run the hang-out walk for `topic`, then — on a hot topic (`childCohortCount > 0`) or when
255
+ * `preferSweep` is set, and when the sweep ports are bound — escalate to the multi-cohort sweep and
256
+ * union the deduped providers. Returns the assembled (possibly `< wantCount`) provider set.
257
+ */
258
+ async walk(topic: MatchTopicRef, want: SeekerWalkRequest): Promise<ProviderEntryV1[]> {
259
+ const topicId = this.topicIdFor(topic);
260
+ const dMax = await this.deps.estimateDMax(topicId);
261
+ const client = new SeekerWalkClient({
262
+ transport: this.deps.walkTransport(topicId),
263
+ topicId,
264
+ wantCount: want.wantCount,
265
+ dMax,
266
+ patienceMs: want.patienceMs,
267
+ verifyEntry: this.deps.verifyEntry,
268
+ ...(want.filter !== undefined ? { filter: want.filter } : {}),
269
+ ...(this.deps.config !== undefined ? { config: this.deps.config } : {}),
270
+ ...(this.deps.meanWantCount !== undefined ? { meanWantCount: this.deps.meanWantCount } : {}),
271
+ ...(this.deps.clock !== undefined ? { clock: this.deps.clock } : {}),
272
+ ...(this.deps.sleep !== undefined ? { sleep: this.deps.sleep } : {}),
273
+ });
274
+ const result = await client.run();
275
+ const providers = new Map<string, ProviderEntryV1>(result.providers.map((p) => [p.participantId, p]));
276
+
277
+ const hot = result.maxChildCohortCount > 0;
278
+ const wantSweep = want.preferSweep === true || (hot && providers.size < want.wantCount);
279
+ if (wantSweep && this.deps.sweepPorts !== undefined) {
280
+ // Pass want.patienceMs as a fresh (coarser) bound — this path does not track a draining
281
+ // remainder (SeekerWalkClient ran on its own deadline). Still strictly better than unbounded.
282
+ const sweep = await runMultiCohortSweep(this.deps.sweepPorts(topicId), {
283
+ topicId,
284
+ wantCount: want.wantCount,
285
+ verifyEntry: this.deps.verifyEntry,
286
+ patienceMs: want.patienceMs,
287
+ ...(want.filter !== undefined ? { filter: want.filter } : {}),
288
+ ...(this.deps.clock !== undefined ? { clock: this.deps.clock } : {}),
289
+ });
290
+ for (const entry of sweep.providers) {
291
+ providers.set(entry.participantId, entry);
292
+ }
293
+ }
294
+ return [...providers.values()];
295
+ }
296
+ }
297
+
298
+ // --- voting quorum-discovery binding -------------------------------------------------------------
299
+
300
+ /** Construction inputs for {@link createMatchmakingQuorumDiscovery}. */
301
+ export interface MatchmakingQuorumDiscoveryDeps {
302
+ /** Per-entry signature verifier (db-p2p binds `verifyPeerSig`). */
303
+ readonly verifyEntry: EntrySigVerifier;
304
+ /** Build the seeker walk transport for a topic. */
305
+ readonly walkTransport: (topicId: Uint8Array) => SeekerWalkTransport;
306
+ /** Estimate `d_max` for a topic. */
307
+ readonly estimateDMax: (topicId: Uint8Array) => Promise<number>;
308
+ /** Build the multi-cohort sweep ports for a topic. */
309
+ readonly sweepPorts: (topicId: Uint8Array) => MultiCohortSweepPorts;
310
+ /** Optional capability filter applied to both the walk and the sweep. */
311
+ readonly filter?: CapabilityFilter;
312
+ /** Hang-out decision config (passed to the walk). */
313
+ readonly config?: HangOutConfig;
314
+ /** Assumed competing-seeker mean `wantCount` (passed to the walk). */
315
+ readonly meanWantCount?: number;
316
+ /** Wall clock (unix ms); injectable for tests. */
317
+ readonly clock?: () => number;
318
+ /** Sleep for the requery cadence; injectable for tests. */
319
+ readonly sleep?: (ms: number) => Promise<void>;
320
+ }
321
+
322
+ /**
323
+ * Bind the db-core voting {@link QuorumDiscovery} port (`docs/matchmaking.md` §Voting-quorum assembly)
324
+ * to the matchmaking substrate: `walk` runs the single-cohort hang-out walk and surfaces its hotness
325
+ * signal (`childCohortCount`); `sweep` runs {@link runMultiCohortSweep} across the high-population tier
326
+ * shards. `VotingQuorumAssembler` composes the two — walk, then sweep on a hot topic — re-validating
327
+ * every entry itself, so this binding stays a thin transport adapter.
328
+ */
329
+ export function createMatchmakingQuorumDiscovery(deps: MatchmakingQuorumDiscoveryDeps): QuorumDiscovery {
330
+ return {
331
+ async walk(req: QuorumDiscoveryRequest): Promise<QuorumDiscoverySlice> {
332
+ const dMax = await deps.estimateDMax(req.topicId);
333
+ const client = new SeekerWalkClient({
334
+ transport: deps.walkTransport(req.topicId),
335
+ topicId: req.topicId,
336
+ wantCount: req.wantCount,
337
+ dMax,
338
+ patienceMs: req.patienceMs,
339
+ verifyEntry: deps.verifyEntry,
340
+ ...(deps.filter !== undefined ? { filter: deps.filter } : {}),
341
+ ...(deps.config !== undefined ? { config: deps.config } : {}),
342
+ ...(deps.meanWantCount !== undefined ? { meanWantCount: deps.meanWantCount } : {}),
343
+ ...(deps.clock !== undefined ? { clock: deps.clock } : {}),
344
+ ...(deps.sleep !== undefined ? { sleep: deps.sleep } : {}),
345
+ });
346
+ const result = await client.run();
347
+ return { entries: result.providers, childCohortCount: result.maxChildCohortCount };
348
+ },
349
+ async sweep(req: QuorumDiscoveryRequest): Promise<QuorumDiscoverySlice> {
350
+ const result = await runMultiCohortSweep(deps.sweepPorts(req.topicId), {
351
+ topicId: req.topicId,
352
+ wantCount: req.wantCount,
353
+ verifyEntry: deps.verifyEntry,
354
+ patienceMs: req.patienceMs,
355
+ ...(deps.filter !== undefined ? { filter: deps.filter } : {}),
356
+ ...(deps.clock !== undefined ? { clock: deps.clock } : {}),
357
+ });
358
+ // The sweep hop has already crossed the ring — its childCohortCount is moot (the escalation
359
+ // decision is made). Report 0 so the assembler never double-escalates.
360
+ return { entries: result.providers, childCohortCount: 0 };
361
+ },
362
+ };
363
+ }