@optimystic/db-core 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/README.md +336 -336
- package/dist/src/cluster/structs.d.ts +39 -1
- package/dist/src/cluster/structs.d.ts.map +1 -1
- package/dist/src/cluster/structs.js +24 -0
- package/dist/src/cluster/structs.js.map +1 -1
- package/dist/src/collection/collection.d.ts +17 -0
- package/dist/src/collection/collection.d.ts.map +1 -1
- package/dist/src/collection/collection.js +24 -2
- package/dist/src/collection/collection.js.map +1 -1
- package/dist/src/collections/tree/tree.d.ts +5 -0
- package/dist/src/collections/tree/tree.d.ts.map +1 -1
- package/dist/src/collections/tree/tree.js +7 -0
- package/dist/src/collections/tree/tree.js.map +1 -1
- package/dist/src/network/i-peer-network.d.ts +16 -0
- package/dist/src/network/i-peer-network.d.ts.map +1 -1
- package/dist/src/network/struct.d.ts +39 -2
- package/dist/src/network/struct.d.ts.map +1 -1
- package/dist/src/network/struct.js +18 -0
- package/dist/src/network/struct.js.map +1 -1
- package/dist/src/testing/test-transactor.d.ts +95 -8
- package/dist/src/testing/test-transactor.d.ts.map +1 -1
- package/dist/src/testing/test-transactor.js +121 -8
- package/dist/src/testing/test-transactor.js.map +1 -1
- package/dist/src/transaction/transaction.d.ts +1 -1
- package/dist/src/transaction/transaction.js +1 -1
- package/dist/src/transactor/network-transactor.d.ts.map +1 -1
- package/dist/src/transactor/network-transactor.js +48 -13
- package/dist/src/transactor/network-transactor.js.map +1 -1
- package/dist/src/transactor/transactor-source.d.ts.map +1 -1
- package/dist/src/transactor/transactor-source.js +25 -2
- package/dist/src/transactor/transactor-source.js.map +1 -1
- package/package.json +1 -1
- package/src/cluster/membership.ts +85 -85
- package/src/cluster/structs.ts +43 -4
- package/src/cohort-topic/addressing.ts +120 -120
- package/src/cohort-topic/antidos/bootstrap-evidence-envelope.ts +253 -253
- package/src/cohort-topic/antidos/bootstrap-evidence.ts +106 -106
- package/src/cohort-topic/antidos/index.ts +5 -5
- package/src/cohort-topic/antidos/rate-limiter.ts +210 -210
- package/src/cohort-topic/antidos/replay-guard.ts +146 -146
- package/src/cohort-topic/antidos/topic-budget.ts +160 -160
- package/src/cohort-topic/antiflood/index.ts +2 -2
- package/src/cohort-topic/antiflood/invariants.ts +108 -108
- package/src/cohort-topic/antiflood/jitter.ts +117 -117
- package/src/cohort-topic/coldstart.ts +237 -237
- package/src/cohort-topic/dmax.ts +88 -88
- package/src/cohort-topic/gossip/bus.ts +254 -254
- package/src/cohort-topic/gossip/index.ts +3 -3
- package/src/cohort-topic/gossip/records.ts +45 -45
- package/src/cohort-topic/gossip/view.ts +91 -91
- package/src/cohort-topic/index.ts +20 -20
- package/src/cohort-topic/load/barometer.ts +134 -134
- package/src/cohort-topic/load/index.ts +1 -1
- package/src/cohort-topic/member-engine.ts +430 -430
- package/src/cohort-topic/membership/index.ts +3 -3
- package/src/cohort-topic/membership/publisher.ts +163 -163
- package/src/cohort-topic/membership/source.ts +41 -41
- package/src/cohort-topic/membership/verifier.ts +461 -461
- package/src/cohort-topic/ports.ts +157 -157
- package/src/cohort-topic/promotion.ts +405 -405
- package/src/cohort-topic/registration/bytes.ts +37 -37
- package/src/cohort-topic/registration/handoff.ts +154 -154
- package/src/cohort-topic/registration/index.ts +6 -6
- package/src/cohort-topic/registration/renewal.ts +495 -495
- package/src/cohort-topic/registration/sharding.ts +61 -61
- package/src/cohort-topic/registration/store.ts +81 -81
- package/src/cohort-topic/registration/types.ts +91 -91
- package/src/cohort-topic/ring-hash.ts +50 -50
- package/src/cohort-topic/service.ts +416 -416
- package/src/cohort-topic/sig/index.ts +2 -2
- package/src/cohort-topic/sig/payloads.ts +59 -59
- package/src/cohort-topic/sig/threshold.ts +64 -64
- package/src/cohort-topic/tiers.ts +74 -74
- package/src/cohort-topic/traffic.ts +233 -233
- package/src/cohort-topic/walk.ts +326 -326
- package/src/cohort-topic/willingness.ts +237 -237
- package/src/cohort-topic/wire/codec.ts +216 -216
- package/src/cohort-topic/wire/index.ts +18 -18
- package/src/cohort-topic/wire/payloads.ts +126 -126
- package/src/cohort-topic/wire/primitives.ts +188 -188
- package/src/cohort-topic/wire/types.ts +475 -475
- package/src/cohort-topic/wire/validate.ts +512 -512
- package/src/collection/collection-type-registry.ts +37 -37
- package/src/collection/collection.ts +25 -2
- package/src/collections/diary/diary.ts +68 -68
- package/src/collections/tree/readme.md +4 -0
- package/src/collections/tree/tree.ts +320 -312
- package/src/matchmaking/capability-filter.ts +45 -45
- package/src/matchmaking/config.ts +98 -98
- package/src/matchmaking/index.ts +21 -21
- package/src/matchmaking/multi-cohort-seeker.ts +234 -234
- package/src/matchmaking/provider.ts +123 -123
- package/src/matchmaking/query-eval.ts +105 -105
- package/src/matchmaking/seeker-walk.ts +127 -127
- package/src/matchmaking/seeker.ts +86 -86
- package/src/matchmaking/topic-anchor.ts +90 -90
- package/src/matchmaking/voting-quorum.ts +394 -394
- package/src/matchmaking/wire.ts +603 -603
- package/src/network/i-peer-network.ts +17 -0
- package/src/network/stale-failure.ts +43 -43
- package/src/network/struct.ts +41 -2
- package/src/network/types.ts +37 -37
- package/src/reactivity/backfill.ts +220 -220
- package/src/reactivity/backpressure.ts +191 -191
- package/src/reactivity/checkpoint.ts +308 -308
- package/src/reactivity/config.ts +172 -172
- package/src/reactivity/dedupe.ts +132 -132
- package/src/reactivity/forwarder.ts +87 -87
- package/src/reactivity/index.ts +34 -34
- package/src/reactivity/notification.ts +123 -123
- package/src/reactivity/policy.ts +79 -79
- package/src/reactivity/push-state.ts +310 -310
- package/src/reactivity/recover.ts +153 -153
- package/src/reactivity/replay-buffer.ts +141 -141
- package/src/reactivity/resume.ts +549 -549
- package/src/reactivity/rotation.ts +415 -415
- package/src/reactivity/subscriber.ts +132 -132
- package/src/reactivity/subscription.ts +66 -66
- package/src/reactivity/topic-anchor.ts +71 -71
- package/src/reactivity/verify.ts +73 -73
- package/src/reactivity/wire-validate.ts +13 -13
- package/src/reactivity/wire.ts +224 -224
- package/src/testing/async-wait.ts +65 -65
- package/src/testing/index.ts +2 -2
- package/src/testing/test-transactor.ts +638 -502
- package/src/transaction/errors.ts +91 -91
- package/src/transaction/operations-hash.ts +196 -196
- package/src/transaction/read-dependency-collector.ts +78 -78
- package/src/transaction/transaction.ts +1 -1
- package/src/transactor/change-notifier.ts +80 -80
- package/src/transactor/index.ts +5 -5
- package/src/transactor/network-transactor.ts +49 -14
- package/src/transactor/transactor-source.ts +25 -2
- package/src/transform/atomic-proxy.ts +92 -92
- package/src/transform/helpers.ts +159 -159
- package/src/utility/backoff.ts +95 -95
- package/src/utility/batch-coordinator.ts +191 -191
- package/dist/src/transaction/context.d.ts +0 -60
- package/dist/src/transaction/context.d.ts.map +0 -1
- package/dist/src/transaction/context.js +0 -91
- package/dist/src/transaction/context.js.map +0 -1
|
@@ -1,123 +1,123 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* Matchmaking — provider decision/state (db-core, transport-agnostic).
|
|
3
|
-
*
|
|
4
|
-
* A {@link MatchmakingProvider} owns the live provider state for one topic: its capability tags, its
|
|
5
|
-
* current `capacityBudget`, and a correlation id (registration identity; not bound into the
|
|
6
|
-
* matchmaking signature — see {@link providerSigningPayload} option (b)). It builds the
|
|
7
|
-
* signed {@link ProviderAppPayloadV1} (and the opaque bytes for the cohort-topic
|
|
8
|
-
* `RegisterV1.appPayload` slot) that the db-p2p `provider-manager` registers at cohort-topic tier
|
|
9
|
-
* **T2** (`docs/matchmaking.md` §Provider registration).
|
|
10
|
-
*
|
|
11
|
-
* It is crypto-free: signing is an injected callback (db-p2p supplies the libp2p peer key), matching
|
|
12
|
-
* the cohort-topic {@link import("../cohort-topic/service.js").ParticipantSigner} pattern.
|
|
13
|
-
*
|
|
14
|
-
* Self-throttling (`docs/matchmaking.md` §Provider self-throttling) is expressed here as state:
|
|
15
|
-
* - **Signal full** — {@link MatchmakingProvider.signalFull} sets `capacityBudget = 0`; the provider
|
|
16
|
-
* stays listed as "available but at capacity". The manager re-registers to push the new payload
|
|
17
|
-
* (the cohort-topic `RenewV1` carries no `appPayload`, so a capacity change is a re-register, not a
|
|
18
|
-
* ping — see the implement handoff).
|
|
19
|
-
* - **Withdraw** — {@link MatchmakingProvider.markWithdrawn} records intent; the manager stops
|
|
20
|
-
* renewing so the record ages out by TTL. Withdrawal is an **optimization, not a correctness
|
|
21
|
-
* requirement** (§Provider self-throttling, GROUNDING resolution): a non-withdrawn registration is
|
|
22
|
-
* bounded by TTL eviction.
|
|
23
|
-
*/
|
|
24
|
-
|
|
25
|
-
import { randomBytes } from "@noble/hashes/utils.js";
|
|
26
|
-
import { providerSigningPayload, type ProviderAppPayloadV1, encodeProviderAppPayload } from "./wire.js";
|
|
27
|
-
|
|
28
|
-
/** Construction inputs for a {@link MatchmakingProvider}. */
|
|
29
|
-
export interface MatchmakingProviderOptions {
|
|
30
|
-
/** The matchmaking topic this provider serves (from {@link import("./topic-anchor.js").MatchTopicAnchor}). */
|
|
31
|
-
readonly topicId: Uint8Array;
|
|
32
|
-
/** Application-defined capability tags. */
|
|
33
|
-
readonly capabilities: readonly string[];
|
|
34
|
-
/** Initial concurrent-task budget (integer `>= 0`). */
|
|
35
|
-
readonly capacityBudget: number;
|
|
36
|
-
/** Multiaddr or PeerId-based callback. */
|
|
37
|
-
readonly contactHint: string;
|
|
38
|
-
/** Optional soft expiry hint (unix ms). */
|
|
39
|
-
readonly serviceUntil?: number;
|
|
40
|
-
/** Sign the canonical registration image; resolves the base64url signature. */
|
|
41
|
-
readonly sign: (payload: Uint8Array) => Promise<string>;
|
|
42
|
-
/** 16-byte registration correlation id (not signature-bound); default fresh CSPRNG bytes. */
|
|
43
|
-
readonly correlationId?: Uint8Array;
|
|
44
|
-
/** CSPRNG source (injectable for deterministic tests). Default `@noble/hashes` `randomBytes`. */
|
|
45
|
-
readonly randomBytes?: (n: number) => Uint8Array;
|
|
46
|
-
}
|
|
47
|
-
|
|
48
|
-
/** Live provider state + signed-payload builder for one matchmaking topic. */
|
|
49
|
-
export class MatchmakingProvider {
|
|
50
|
-
readonly topicId: Uint8Array;
|
|
51
|
-
readonly correlationId: Uint8Array;
|
|
52
|
-
private readonly capabilities: readonly string[];
|
|
53
|
-
private readonly contactHint: string;
|
|
54
|
-
private readonly serviceUntil?: number;
|
|
55
|
-
private readonly sign: (payload: Uint8Array) => Promise<string>;
|
|
56
|
-
private capacity: number;
|
|
57
|
-
private withdrawnFlag = false;
|
|
58
|
-
|
|
59
|
-
constructor(options: MatchmakingProviderOptions) {
|
|
60
|
-
this.topicId = options.topicId;
|
|
61
|
-
this.capabilities = [...options.capabilities];
|
|
62
|
-
this.contactHint = options.contactHint;
|
|
63
|
-
this.serviceUntil = options.serviceUntil;
|
|
64
|
-
this.sign = options.sign;
|
|
65
|
-
this.capacity = requireBudget(options.capacityBudget);
|
|
66
|
-
const rand = options.randomBytes ?? randomBytes;
|
|
67
|
-
this.correlationId = options.correlationId ?? rand(16);
|
|
68
|
-
}
|
|
69
|
-
|
|
70
|
-
/** Current concurrent-task budget; `0` means "listed but full". */
|
|
71
|
-
get capacityBudget(): number {
|
|
72
|
-
return this.capacity;
|
|
73
|
-
}
|
|
74
|
-
|
|
75
|
-
/** True once {@link markWithdrawn} has been called (the manager should stop renewing). */
|
|
76
|
-
get withdrawn(): boolean {
|
|
77
|
-
return this.withdrawnFlag;
|
|
78
|
-
}
|
|
79
|
-
|
|
80
|
-
/** Set the live budget (integer `>= 0`). The next built payload reflects it. */
|
|
81
|
-
setCapacity(budget: number): void {
|
|
82
|
-
this.capacity = requireBudget(budget);
|
|
83
|
-
}
|
|
84
|
-
|
|
85
|
-
/** Signal "available but at capacity" by setting `capacityBudget = 0` (§Provider self-throttling). */
|
|
86
|
-
signalFull(): void {
|
|
87
|
-
this.capacity = 0;
|
|
88
|
-
}
|
|
89
|
-
|
|
90
|
-
/** Record withdrawal intent; the manager stops renewing so the record TTL-expires (optimization). */
|
|
91
|
-
markWithdrawn(): void {
|
|
92
|
-
this.withdrawnFlag = true;
|
|
93
|
-
}
|
|
94
|
-
|
|
95
|
-
/** Build the signed {@link ProviderAppPayloadV1} reflecting the current capacity. */
|
|
96
|
-
async buildAppPayload(): Promise<ProviderAppPayloadV1> {
|
|
97
|
-
const signature = await this.sign(providerSigningPayload(this.topicId, this.capabilities, this.capacity));
|
|
98
|
-
const payload: ProviderAppPayloadV1 = {
|
|
99
|
-
kind: "match-provider",
|
|
100
|
-
capabilities: [...this.capabilities],
|
|
101
|
-
capacityBudget: this.capacity,
|
|
102
|
-
contactHint: this.contactHint,
|
|
103
|
-
signature,
|
|
104
|
-
};
|
|
105
|
-
if (this.serviceUntil !== undefined) {
|
|
106
|
-
payload.serviceUntil = this.serviceUntil;
|
|
107
|
-
}
|
|
108
|
-
return payload;
|
|
109
|
-
}
|
|
110
|
-
|
|
111
|
-
/** Build the opaque bytes for the cohort-topic `RegisterV1.appPayload` slot. */
|
|
112
|
-
async appPayloadBytes(): Promise<Uint8Array> {
|
|
113
|
-
return encodeProviderAppPayload(await this.buildAppPayload());
|
|
114
|
-
}
|
|
115
|
-
}
|
|
116
|
-
|
|
117
|
-
/** Validate a capacity budget is an integer `>= 0`. */
|
|
118
|
-
function requireBudget(budget: number): number {
|
|
119
|
-
if (!Number.isInteger(budget) || budget < 0) {
|
|
120
|
-
throw new RangeError(`matchmaking provider: capacityBudget must be an integer >= 0, got ${budget}`);
|
|
121
|
-
}
|
|
122
|
-
return budget;
|
|
123
|
-
}
|
|
1
|
+
/**
|
|
2
|
+
* Matchmaking — provider decision/state (db-core, transport-agnostic).
|
|
3
|
+
*
|
|
4
|
+
* A {@link MatchmakingProvider} owns the live provider state for one topic: its capability tags, its
|
|
5
|
+
* current `capacityBudget`, and a correlation id (registration identity; not bound into the
|
|
6
|
+
* matchmaking signature — see {@link providerSigningPayload} option (b)). It builds the
|
|
7
|
+
* signed {@link ProviderAppPayloadV1} (and the opaque bytes for the cohort-topic
|
|
8
|
+
* `RegisterV1.appPayload` slot) that the db-p2p `provider-manager` registers at cohort-topic tier
|
|
9
|
+
* **T2** (`docs/matchmaking.md` §Provider registration).
|
|
10
|
+
*
|
|
11
|
+
* It is crypto-free: signing is an injected callback (db-p2p supplies the libp2p peer key), matching
|
|
12
|
+
* the cohort-topic {@link import("../cohort-topic/service.js").ParticipantSigner} pattern.
|
|
13
|
+
*
|
|
14
|
+
* Self-throttling (`docs/matchmaking.md` §Provider self-throttling) is expressed here as state:
|
|
15
|
+
* - **Signal full** — {@link MatchmakingProvider.signalFull} sets `capacityBudget = 0`; the provider
|
|
16
|
+
* stays listed as "available but at capacity". The manager re-registers to push the new payload
|
|
17
|
+
* (the cohort-topic `RenewV1` carries no `appPayload`, so a capacity change is a re-register, not a
|
|
18
|
+
* ping — see the implement handoff).
|
|
19
|
+
* - **Withdraw** — {@link MatchmakingProvider.markWithdrawn} records intent; the manager stops
|
|
20
|
+
* renewing so the record ages out by TTL. Withdrawal is an **optimization, not a correctness
|
|
21
|
+
* requirement** (§Provider self-throttling, GROUNDING resolution): a non-withdrawn registration is
|
|
22
|
+
* bounded by TTL eviction.
|
|
23
|
+
*/
|
|
24
|
+
|
|
25
|
+
import { randomBytes } from "@noble/hashes/utils.js";
|
|
26
|
+
import { providerSigningPayload, type ProviderAppPayloadV1, encodeProviderAppPayload } from "./wire.js";
|
|
27
|
+
|
|
28
|
+
/** Construction inputs for a {@link MatchmakingProvider}. */
|
|
29
|
+
export interface MatchmakingProviderOptions {
|
|
30
|
+
/** The matchmaking topic this provider serves (from {@link import("./topic-anchor.js").MatchTopicAnchor}). */
|
|
31
|
+
readonly topicId: Uint8Array;
|
|
32
|
+
/** Application-defined capability tags. */
|
|
33
|
+
readonly capabilities: readonly string[];
|
|
34
|
+
/** Initial concurrent-task budget (integer `>= 0`). */
|
|
35
|
+
readonly capacityBudget: number;
|
|
36
|
+
/** Multiaddr or PeerId-based callback. */
|
|
37
|
+
readonly contactHint: string;
|
|
38
|
+
/** Optional soft expiry hint (unix ms). */
|
|
39
|
+
readonly serviceUntil?: number;
|
|
40
|
+
/** Sign the canonical registration image; resolves the base64url signature. */
|
|
41
|
+
readonly sign: (payload: Uint8Array) => Promise<string>;
|
|
42
|
+
/** 16-byte registration correlation id (not signature-bound); default fresh CSPRNG bytes. */
|
|
43
|
+
readonly correlationId?: Uint8Array;
|
|
44
|
+
/** CSPRNG source (injectable for deterministic tests). Default `@noble/hashes` `randomBytes`. */
|
|
45
|
+
readonly randomBytes?: (n: number) => Uint8Array;
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
/** Live provider state + signed-payload builder for one matchmaking topic. */
|
|
49
|
+
export class MatchmakingProvider {
|
|
50
|
+
readonly topicId: Uint8Array;
|
|
51
|
+
readonly correlationId: Uint8Array;
|
|
52
|
+
private readonly capabilities: readonly string[];
|
|
53
|
+
private readonly contactHint: string;
|
|
54
|
+
private readonly serviceUntil?: number;
|
|
55
|
+
private readonly sign: (payload: Uint8Array) => Promise<string>;
|
|
56
|
+
private capacity: number;
|
|
57
|
+
private withdrawnFlag = false;
|
|
58
|
+
|
|
59
|
+
constructor(options: MatchmakingProviderOptions) {
|
|
60
|
+
this.topicId = options.topicId;
|
|
61
|
+
this.capabilities = [...options.capabilities];
|
|
62
|
+
this.contactHint = options.contactHint;
|
|
63
|
+
this.serviceUntil = options.serviceUntil;
|
|
64
|
+
this.sign = options.sign;
|
|
65
|
+
this.capacity = requireBudget(options.capacityBudget);
|
|
66
|
+
const rand = options.randomBytes ?? randomBytes;
|
|
67
|
+
this.correlationId = options.correlationId ?? rand(16);
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
/** Current concurrent-task budget; `0` means "listed but full". */
|
|
71
|
+
get capacityBudget(): number {
|
|
72
|
+
return this.capacity;
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
/** True once {@link markWithdrawn} has been called (the manager should stop renewing). */
|
|
76
|
+
get withdrawn(): boolean {
|
|
77
|
+
return this.withdrawnFlag;
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
/** Set the live budget (integer `>= 0`). The next built payload reflects it. */
|
|
81
|
+
setCapacity(budget: number): void {
|
|
82
|
+
this.capacity = requireBudget(budget);
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
/** Signal "available but at capacity" by setting `capacityBudget = 0` (§Provider self-throttling). */
|
|
86
|
+
signalFull(): void {
|
|
87
|
+
this.capacity = 0;
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
/** Record withdrawal intent; the manager stops renewing so the record TTL-expires (optimization). */
|
|
91
|
+
markWithdrawn(): void {
|
|
92
|
+
this.withdrawnFlag = true;
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
/** Build the signed {@link ProviderAppPayloadV1} reflecting the current capacity. */
|
|
96
|
+
async buildAppPayload(): Promise<ProviderAppPayloadV1> {
|
|
97
|
+
const signature = await this.sign(providerSigningPayload(this.topicId, this.capabilities, this.capacity));
|
|
98
|
+
const payload: ProviderAppPayloadV1 = {
|
|
99
|
+
kind: "match-provider",
|
|
100
|
+
capabilities: [...this.capabilities],
|
|
101
|
+
capacityBudget: this.capacity,
|
|
102
|
+
contactHint: this.contactHint,
|
|
103
|
+
signature,
|
|
104
|
+
};
|
|
105
|
+
if (this.serviceUntil !== undefined) {
|
|
106
|
+
payload.serviceUntil = this.serviceUntil;
|
|
107
|
+
}
|
|
108
|
+
return payload;
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
/** Build the opaque bytes for the cohort-topic `RegisterV1.appPayload` slot. */
|
|
112
|
+
async appPayloadBytes(): Promise<Uint8Array> {
|
|
113
|
+
return encodeProviderAppPayload(await this.buildAppPayload());
|
|
114
|
+
}
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
/** Validate a capacity budget is an integer `>= 0`. */
|
|
118
|
+
function requireBudget(budget: number): number {
|
|
119
|
+
if (!Number.isInteger(budget) || budget < 0) {
|
|
120
|
+
throw new RangeError(`matchmaking provider: capacityBudget must be an integer >= 0, got ${budget}`);
|
|
121
|
+
}
|
|
122
|
+
return budget;
|
|
123
|
+
}
|
|
@@ -1,105 +1,105 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* Matchmaking — pure `QueryV1` evaluation (db-core, transport-agnostic).
|
|
3
|
-
*
|
|
4
|
-
* The cohort-side query handler (`db-p2p/src/matchmaking/query-handler.ts`) decodes its local
|
|
5
|
-
* registration records into the {@link LocalProviderRegistration} / {@link LocalSeekerRegistration}
|
|
6
|
-
* shapes below and hands them here; this module performs the *advisory* selection that
|
|
7
|
-
* `docs/matchmaking.md` §Seeker query / §Capability filter specify:
|
|
8
|
-
*
|
|
9
|
-
* - Providers are filtered through {@link matchesFilter} (when `includeProviders`).
|
|
10
|
-
* - Each included set is truncated to `query.limit` (`<= query_limit_max`, enforced on decode in
|
|
11
|
-
* `wire.ts`); {@link QueryEvalResult.truncated} is set when *any* included set had more matches than
|
|
12
|
-
* `limit` allowed, so the seeker knows to re-query a sibling cohort.
|
|
13
|
-
* - The forwarded entries carry the provider/seeker's own `registrationSig` verbatim, so the seeker
|
|
14
|
-
* re-validates each one (`verifyProviderEntry`) — the cohort vouches only for the *set it held*.
|
|
15
|
-
*
|
|
16
|
-
* Pure: no I/O, no clock, no crypto. The db-p2p handler attaches `topicTraffic` / `cohortEpoch` and
|
|
17
|
-
* the primary's reply signature.
|
|
18
|
-
*/
|
|
19
|
-
|
|
20
|
-
import { matchesFilter } from "./capability-filter.js";
|
|
21
|
-
import type { ProviderAppPayloadV1, SeekerAppPayloadV1, ProviderEntryV1, SeekerEntryV1, QueryV1 } from "./wire.js";
|
|
22
|
-
|
|
23
|
-
/** A decoded local provider registration held by the cohort, ready for {@link evaluateQuery}. */
|
|
24
|
-
export interface LocalProviderRegistration {
|
|
25
|
-
/** The provider's peer-id string — the entry's `participantId` AND the `registrationSig` signer. */
|
|
26
|
-
readonly participantId: string;
|
|
27
|
-
/** Unix ms the registration first attached (forwarded verbatim for seeker FCFS ordering). */
|
|
28
|
-
readonly attachedAt: number;
|
|
29
|
-
/** The decoded, validated provider app payload (capabilities, budget, contact, signature). */
|
|
30
|
-
readonly payload: ProviderAppPayloadV1;
|
|
31
|
-
}
|
|
32
|
-
|
|
33
|
-
/** A decoded local seeker registration held by the cohort (collective-assembly discovery). */
|
|
34
|
-
export interface LocalSeekerRegistration {
|
|
35
|
-
readonly participantId: string;
|
|
36
|
-
readonly attachedAt: number;
|
|
37
|
-
readonly payload: SeekerAppPayloadV1;
|
|
38
|
-
}
|
|
39
|
-
|
|
40
|
-
/** The selected entries for a {@link QueryReplyV1} body (the db-p2p handler signs + frames it). */
|
|
41
|
-
export interface QueryEvalResult {
|
|
42
|
-
readonly providers?: ProviderEntryV1[];
|
|
43
|
-
readonly seekers?: SeekerEntryV1[];
|
|
44
|
-
/** `true` when an included set had more matches than `query.limit` allowed (re-query hint). */
|
|
45
|
-
readonly truncated: boolean;
|
|
46
|
-
}
|
|
47
|
-
|
|
48
|
-
/** Build a forwarded {@link ProviderEntryV1} from a local registration (signature forwarded verbatim). */
|
|
49
|
-
export function providerEntryOf(reg: LocalProviderRegistration): ProviderEntryV1 {
|
|
50
|
-
return {
|
|
51
|
-
participantId: reg.participantId,
|
|
52
|
-
capabilities: [...reg.payload.capabilities],
|
|
53
|
-
capacityBudget: reg.payload.capacityBudget,
|
|
54
|
-
contactHint: reg.payload.contactHint,
|
|
55
|
-
attachedAt: reg.attachedAt,
|
|
56
|
-
registrationSig: reg.payload.signature,
|
|
57
|
-
};
|
|
58
|
-
}
|
|
59
|
-
|
|
60
|
-
/** Build a forwarded {@link SeekerEntryV1} from a local registration (signature forwarded verbatim). */
|
|
61
|
-
export function seekerEntryOf(reg: LocalSeekerRegistration): SeekerEntryV1 {
|
|
62
|
-
return {
|
|
63
|
-
participantId: reg.participantId,
|
|
64
|
-
wantCount: reg.payload.wantCount,
|
|
65
|
-
contactHint: reg.payload.contactHint,
|
|
66
|
-
attachedAt: reg.attachedAt,
|
|
67
|
-
registrationSig: reg.payload.signature,
|
|
68
|
-
};
|
|
69
|
-
}
|
|
70
|
-
|
|
71
|
-
/**
|
|
72
|
-
* Evaluate `query` against the cohort's locally-held registrations: filter providers, optionally include
|
|
73
|
-
* seekers, truncate each included set to `query.limit`. Inputs are returned in `attachedAt` order
|
|
74
|
-
* (oldest first) so truncation keeps the longest-waiting registrations — the FCFS bias the arrival-push
|
|
75
|
-
* fairness rule also uses (`docs/matchmaking.md` §Fairness).
|
|
76
|
-
*/
|
|
77
|
-
export function evaluateQuery(
|
|
78
|
-
query: QueryV1,
|
|
79
|
-
providers: readonly LocalProviderRegistration[],
|
|
80
|
-
seekers: readonly LocalSeekerRegistration[],
|
|
81
|
-
): QueryEvalResult {
|
|
82
|
-
let truncated = false;
|
|
83
|
-
const result: { providers?: ProviderEntryV1[]; seekers?: SeekerEntryV1[]; truncated: boolean } = { truncated: false };
|
|
84
|
-
|
|
85
|
-
if (query.includeProviders) {
|
|
86
|
-
const matched = providers
|
|
87
|
-
.filter((reg) => matchesFilter(reg.payload, query.filter))
|
|
88
|
-
.sort((a, b) => a.attachedAt - b.attachedAt);
|
|
89
|
-
if (matched.length > query.limit) {
|
|
90
|
-
truncated = true;
|
|
91
|
-
}
|
|
92
|
-
result.providers = matched.slice(0, query.limit).map(providerEntryOf);
|
|
93
|
-
}
|
|
94
|
-
|
|
95
|
-
if (query.includeSeekers) {
|
|
96
|
-
const ordered = [...seekers].sort((a, b) => a.attachedAt - b.attachedAt);
|
|
97
|
-
if (ordered.length > query.limit) {
|
|
98
|
-
truncated = true;
|
|
99
|
-
}
|
|
100
|
-
result.seekers = ordered.slice(0, query.limit).map(seekerEntryOf);
|
|
101
|
-
}
|
|
102
|
-
|
|
103
|
-
result.truncated = truncated;
|
|
104
|
-
return result;
|
|
105
|
-
}
|
|
1
|
+
/**
|
|
2
|
+
* Matchmaking — pure `QueryV1` evaluation (db-core, transport-agnostic).
|
|
3
|
+
*
|
|
4
|
+
* The cohort-side query handler (`db-p2p/src/matchmaking/query-handler.ts`) decodes its local
|
|
5
|
+
* registration records into the {@link LocalProviderRegistration} / {@link LocalSeekerRegistration}
|
|
6
|
+
* shapes below and hands them here; this module performs the *advisory* selection that
|
|
7
|
+
* `docs/matchmaking.md` §Seeker query / §Capability filter specify:
|
|
8
|
+
*
|
|
9
|
+
* - Providers are filtered through {@link matchesFilter} (when `includeProviders`).
|
|
10
|
+
* - Each included set is truncated to `query.limit` (`<= query_limit_max`, enforced on decode in
|
|
11
|
+
* `wire.ts`); {@link QueryEvalResult.truncated} is set when *any* included set had more matches than
|
|
12
|
+
* `limit` allowed, so the seeker knows to re-query a sibling cohort.
|
|
13
|
+
* - The forwarded entries carry the provider/seeker's own `registrationSig` verbatim, so the seeker
|
|
14
|
+
* re-validates each one (`verifyProviderEntry`) — the cohort vouches only for the *set it held*.
|
|
15
|
+
*
|
|
16
|
+
* Pure: no I/O, no clock, no crypto. The db-p2p handler attaches `topicTraffic` / `cohortEpoch` and
|
|
17
|
+
* the primary's reply signature.
|
|
18
|
+
*/
|
|
19
|
+
|
|
20
|
+
import { matchesFilter } from "./capability-filter.js";
|
|
21
|
+
import type { ProviderAppPayloadV1, SeekerAppPayloadV1, ProviderEntryV1, SeekerEntryV1, QueryV1 } from "./wire.js";
|
|
22
|
+
|
|
23
|
+
/** A decoded local provider registration held by the cohort, ready for {@link evaluateQuery}. */
|
|
24
|
+
export interface LocalProviderRegistration {
|
|
25
|
+
/** The provider's peer-id string — the entry's `participantId` AND the `registrationSig` signer. */
|
|
26
|
+
readonly participantId: string;
|
|
27
|
+
/** Unix ms the registration first attached (forwarded verbatim for seeker FCFS ordering). */
|
|
28
|
+
readonly attachedAt: number;
|
|
29
|
+
/** The decoded, validated provider app payload (capabilities, budget, contact, signature). */
|
|
30
|
+
readonly payload: ProviderAppPayloadV1;
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
/** A decoded local seeker registration held by the cohort (collective-assembly discovery). */
|
|
34
|
+
export interface LocalSeekerRegistration {
|
|
35
|
+
readonly participantId: string;
|
|
36
|
+
readonly attachedAt: number;
|
|
37
|
+
readonly payload: SeekerAppPayloadV1;
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
/** The selected entries for a {@link QueryReplyV1} body (the db-p2p handler signs + frames it). */
|
|
41
|
+
export interface QueryEvalResult {
|
|
42
|
+
readonly providers?: ProviderEntryV1[];
|
|
43
|
+
readonly seekers?: SeekerEntryV1[];
|
|
44
|
+
/** `true` when an included set had more matches than `query.limit` allowed (re-query hint). */
|
|
45
|
+
readonly truncated: boolean;
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
/** Build a forwarded {@link ProviderEntryV1} from a local registration (signature forwarded verbatim). */
|
|
49
|
+
export function providerEntryOf(reg: LocalProviderRegistration): ProviderEntryV1 {
|
|
50
|
+
return {
|
|
51
|
+
participantId: reg.participantId,
|
|
52
|
+
capabilities: [...reg.payload.capabilities],
|
|
53
|
+
capacityBudget: reg.payload.capacityBudget,
|
|
54
|
+
contactHint: reg.payload.contactHint,
|
|
55
|
+
attachedAt: reg.attachedAt,
|
|
56
|
+
registrationSig: reg.payload.signature,
|
|
57
|
+
};
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
/** Build a forwarded {@link SeekerEntryV1} from a local registration (signature forwarded verbatim). */
|
|
61
|
+
export function seekerEntryOf(reg: LocalSeekerRegistration): SeekerEntryV1 {
|
|
62
|
+
return {
|
|
63
|
+
participantId: reg.participantId,
|
|
64
|
+
wantCount: reg.payload.wantCount,
|
|
65
|
+
contactHint: reg.payload.contactHint,
|
|
66
|
+
attachedAt: reg.attachedAt,
|
|
67
|
+
registrationSig: reg.payload.signature,
|
|
68
|
+
};
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
/**
|
|
72
|
+
* Evaluate `query` against the cohort's locally-held registrations: filter providers, optionally include
|
|
73
|
+
* seekers, truncate each included set to `query.limit`. Inputs are returned in `attachedAt` order
|
|
74
|
+
* (oldest first) so truncation keeps the longest-waiting registrations — the FCFS bias the arrival-push
|
|
75
|
+
* fairness rule also uses (`docs/matchmaking.md` §Fairness).
|
|
76
|
+
*/
|
|
77
|
+
export function evaluateQuery(
|
|
78
|
+
query: QueryV1,
|
|
79
|
+
providers: readonly LocalProviderRegistration[],
|
|
80
|
+
seekers: readonly LocalSeekerRegistration[],
|
|
81
|
+
): QueryEvalResult {
|
|
82
|
+
let truncated = false;
|
|
83
|
+
const result: { providers?: ProviderEntryV1[]; seekers?: SeekerEntryV1[]; truncated: boolean } = { truncated: false };
|
|
84
|
+
|
|
85
|
+
if (query.includeProviders) {
|
|
86
|
+
const matched = providers
|
|
87
|
+
.filter((reg) => matchesFilter(reg.payload, query.filter))
|
|
88
|
+
.sort((a, b) => a.attachedAt - b.attachedAt);
|
|
89
|
+
if (matched.length > query.limit) {
|
|
90
|
+
truncated = true;
|
|
91
|
+
}
|
|
92
|
+
result.providers = matched.slice(0, query.limit).map(providerEntryOf);
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
if (query.includeSeekers) {
|
|
96
|
+
const ordered = [...seekers].sort((a, b) => a.attachedAt - b.attachedAt);
|
|
97
|
+
if (ordered.length > query.limit) {
|
|
98
|
+
truncated = true;
|
|
99
|
+
}
|
|
100
|
+
result.seekers = ordered.slice(0, query.limit).map(seekerEntryOf);
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
result.truncated = truncated;
|
|
104
|
+
return result;
|
|
105
|
+
}
|