@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.
- 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-repo.d.ts +39 -14
- package/dist/src/cluster/cluster-repo.d.ts.map +1 -1
- package/dist/src/cluster/cluster-repo.js +226 -119
- package/dist/src/cluster/cluster-repo.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/host.js +34 -11
- package/dist/src/cohort-topic/host.js.map +1 -1
- package/dist/src/cohort-topic/stream-util.d.ts +37 -7
- package/dist/src/cohort-topic/stream-util.d.ts.map +1 -1
- package/dist/src/cohort-topic/stream-util.js +77 -19
- 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 +3 -0
- package/dist/src/index.d.ts.map +1 -1
- package/dist/src/index.js +3 -0
- package/dist/src/index.js.map +1 -1
- package/dist/src/libp2p-key-network.d.ts +88 -2
- package/dist/src/libp2p-key-network.d.ts.map +1 -1
- package/dist/src/libp2p-key-network.js +134 -28
- package/dist/src/libp2p-key-network.js.map +1 -1
- package/dist/src/libp2p-node-base.d.ts.map +1 -1
- package/dist/src/libp2p-node-base.js +25 -1
- package/dist/src/libp2p-node-base.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/matchmaking/query-transport.js +3 -3
- package/dist/src/matchmaking/query-transport.js.map +1 -1
- 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/reactivity/notify-transport.d.ts +4 -4
- package/dist/src/reactivity/notify-transport.js +6 -6
- package/dist/src/reactivity/notify-transport.js.map +1 -1
- package/dist/src/reactivity/push-state-gossip.js +2 -2
- package/dist/src/reactivity/push-state-gossip.js.map +1 -1
- package/dist/src/reactivity/recover-transport.d.ts +6 -2
- package/dist/src/reactivity/recover-transport.d.ts.map +1 -1
- package/dist/src/reactivity/recover-transport.js +7 -3
- package/dist/src/reactivity/recover-transport.js.map +1 -1
- 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 +62 -9
- package/dist/src/repo/coordinator-repo.d.ts.map +1 -1
- package/dist/src/repo/coordinator-repo.js +242 -73
- package/dist/src/repo/coordinator-repo.js.map +1 -1
- package/dist/src/rn.d.ts +3 -0
- package/dist/src/rn.d.ts.map +1 -1
- package/dist/src/rn.js +3 -0
- package/dist/src/rn.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-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/testing/cohort-topic-mesh-harness.d.ts +13 -6
- package/dist/src/testing/cohort-topic-mesh-harness.d.ts.map +1 -1
- package/dist/src/testing/cohort-topic-mesh-harness.js +15 -6
- package/dist/src/testing/cohort-topic-mesh-harness.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 +35 -2
- package/dist/src/testing/raw-storage-conformance.js.map +1 -1
- package/package.json +3 -3
- package/readme.md +668 -668
- 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 -203
- package/src/cluster/cluster-repo.ts +242 -122
- package/src/cluster/cluster-size-coupling.ts +45 -45
- 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 -203
- 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 +42 -11
- 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 +79 -19
- 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 -49
- package/src/libp2p-key-network.ts +1120 -990
- package/src/libp2p-node-base.ts +1675 -1651
- 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 +3 -3
- 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/optimystic-node.ts +36 -36
- 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 +7 -3
- 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 +281 -74
- package/src/repo/types.ts +7 -7
- package/src/rn.ts +39 -36
- package/src/rpc-deadline.ts +45 -45
- package/src/storage/arachnode-partition.ts +74 -74
- package/src/storage/cached-raw-storage.ts +180 -0
- package/src/storage/cached-store-driver.ts +859 -0
- 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 -1014
- package/src/testing/cohort-topic-mesh-harness.ts +673 -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 -417
- 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,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
|
+
}
|