@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.
Files changed (141) hide show
  1. package/README.md +336 -336
  2. package/dist/src/cluster/structs.d.ts +39 -1
  3. package/dist/src/cluster/structs.d.ts.map +1 -1
  4. package/dist/src/cluster/structs.js +24 -0
  5. package/dist/src/cluster/structs.js.map +1 -1
  6. package/dist/src/collection/collection.d.ts +17 -0
  7. package/dist/src/collection/collection.d.ts.map +1 -1
  8. package/dist/src/collection/collection.js +24 -2
  9. package/dist/src/collection/collection.js.map +1 -1
  10. package/dist/src/collections/tree/tree.d.ts +5 -0
  11. package/dist/src/collections/tree/tree.d.ts.map +1 -1
  12. package/dist/src/collections/tree/tree.js +7 -0
  13. package/dist/src/collections/tree/tree.js.map +1 -1
  14. package/dist/src/network/i-peer-network.d.ts +16 -0
  15. package/dist/src/network/i-peer-network.d.ts.map +1 -1
  16. package/dist/src/network/struct.d.ts +39 -2
  17. package/dist/src/network/struct.d.ts.map +1 -1
  18. package/dist/src/network/struct.js +18 -0
  19. package/dist/src/network/struct.js.map +1 -1
  20. package/dist/src/testing/test-transactor.d.ts +95 -8
  21. package/dist/src/testing/test-transactor.d.ts.map +1 -1
  22. package/dist/src/testing/test-transactor.js +121 -8
  23. package/dist/src/testing/test-transactor.js.map +1 -1
  24. package/dist/src/transaction/transaction.d.ts +1 -1
  25. package/dist/src/transaction/transaction.js +1 -1
  26. package/dist/src/transactor/network-transactor.d.ts.map +1 -1
  27. package/dist/src/transactor/network-transactor.js +48 -13
  28. package/dist/src/transactor/network-transactor.js.map +1 -1
  29. package/dist/src/transactor/transactor-source.d.ts.map +1 -1
  30. package/dist/src/transactor/transactor-source.js +25 -2
  31. package/dist/src/transactor/transactor-source.js.map +1 -1
  32. package/package.json +1 -1
  33. package/src/cluster/membership.ts +85 -85
  34. package/src/cluster/structs.ts +43 -4
  35. package/src/cohort-topic/addressing.ts +120 -120
  36. package/src/cohort-topic/antidos/bootstrap-evidence-envelope.ts +253 -253
  37. package/src/cohort-topic/antidos/bootstrap-evidence.ts +106 -106
  38. package/src/cohort-topic/antidos/index.ts +5 -5
  39. package/src/cohort-topic/antidos/rate-limiter.ts +210 -210
  40. package/src/cohort-topic/antidos/replay-guard.ts +146 -146
  41. package/src/cohort-topic/antidos/topic-budget.ts +160 -160
  42. package/src/cohort-topic/antiflood/index.ts +2 -2
  43. package/src/cohort-topic/antiflood/invariants.ts +108 -108
  44. package/src/cohort-topic/antiflood/jitter.ts +117 -117
  45. package/src/cohort-topic/coldstart.ts +237 -237
  46. package/src/cohort-topic/dmax.ts +88 -88
  47. package/src/cohort-topic/gossip/bus.ts +254 -254
  48. package/src/cohort-topic/gossip/index.ts +3 -3
  49. package/src/cohort-topic/gossip/records.ts +45 -45
  50. package/src/cohort-topic/gossip/view.ts +91 -91
  51. package/src/cohort-topic/index.ts +20 -20
  52. package/src/cohort-topic/load/barometer.ts +134 -134
  53. package/src/cohort-topic/load/index.ts +1 -1
  54. package/src/cohort-topic/member-engine.ts +430 -430
  55. package/src/cohort-topic/membership/index.ts +3 -3
  56. package/src/cohort-topic/membership/publisher.ts +163 -163
  57. package/src/cohort-topic/membership/source.ts +41 -41
  58. package/src/cohort-topic/membership/verifier.ts +461 -461
  59. package/src/cohort-topic/ports.ts +157 -157
  60. package/src/cohort-topic/promotion.ts +405 -405
  61. package/src/cohort-topic/registration/bytes.ts +37 -37
  62. package/src/cohort-topic/registration/handoff.ts +154 -154
  63. package/src/cohort-topic/registration/index.ts +6 -6
  64. package/src/cohort-topic/registration/renewal.ts +495 -495
  65. package/src/cohort-topic/registration/sharding.ts +61 -61
  66. package/src/cohort-topic/registration/store.ts +81 -81
  67. package/src/cohort-topic/registration/types.ts +91 -91
  68. package/src/cohort-topic/ring-hash.ts +50 -50
  69. package/src/cohort-topic/service.ts +416 -416
  70. package/src/cohort-topic/sig/index.ts +2 -2
  71. package/src/cohort-topic/sig/payloads.ts +59 -59
  72. package/src/cohort-topic/sig/threshold.ts +64 -64
  73. package/src/cohort-topic/tiers.ts +74 -74
  74. package/src/cohort-topic/traffic.ts +233 -233
  75. package/src/cohort-topic/walk.ts +326 -326
  76. package/src/cohort-topic/willingness.ts +237 -237
  77. package/src/cohort-topic/wire/codec.ts +216 -216
  78. package/src/cohort-topic/wire/index.ts +18 -18
  79. package/src/cohort-topic/wire/payloads.ts +126 -126
  80. package/src/cohort-topic/wire/primitives.ts +188 -188
  81. package/src/cohort-topic/wire/types.ts +475 -475
  82. package/src/cohort-topic/wire/validate.ts +512 -512
  83. package/src/collection/collection-type-registry.ts +37 -37
  84. package/src/collection/collection.ts +25 -2
  85. package/src/collections/diary/diary.ts +68 -68
  86. package/src/collections/tree/readme.md +4 -0
  87. package/src/collections/tree/tree.ts +320 -312
  88. package/src/matchmaking/capability-filter.ts +45 -45
  89. package/src/matchmaking/config.ts +98 -98
  90. package/src/matchmaking/index.ts +21 -21
  91. package/src/matchmaking/multi-cohort-seeker.ts +234 -234
  92. package/src/matchmaking/provider.ts +123 -123
  93. package/src/matchmaking/query-eval.ts +105 -105
  94. package/src/matchmaking/seeker-walk.ts +127 -127
  95. package/src/matchmaking/seeker.ts +86 -86
  96. package/src/matchmaking/topic-anchor.ts +90 -90
  97. package/src/matchmaking/voting-quorum.ts +394 -394
  98. package/src/matchmaking/wire.ts +603 -603
  99. package/src/network/i-peer-network.ts +17 -0
  100. package/src/network/stale-failure.ts +43 -43
  101. package/src/network/struct.ts +41 -2
  102. package/src/network/types.ts +37 -37
  103. package/src/reactivity/backfill.ts +220 -220
  104. package/src/reactivity/backpressure.ts +191 -191
  105. package/src/reactivity/checkpoint.ts +308 -308
  106. package/src/reactivity/config.ts +172 -172
  107. package/src/reactivity/dedupe.ts +132 -132
  108. package/src/reactivity/forwarder.ts +87 -87
  109. package/src/reactivity/index.ts +34 -34
  110. package/src/reactivity/notification.ts +123 -123
  111. package/src/reactivity/policy.ts +79 -79
  112. package/src/reactivity/push-state.ts +310 -310
  113. package/src/reactivity/recover.ts +153 -153
  114. package/src/reactivity/replay-buffer.ts +141 -141
  115. package/src/reactivity/resume.ts +549 -549
  116. package/src/reactivity/rotation.ts +415 -415
  117. package/src/reactivity/subscriber.ts +132 -132
  118. package/src/reactivity/subscription.ts +66 -66
  119. package/src/reactivity/topic-anchor.ts +71 -71
  120. package/src/reactivity/verify.ts +73 -73
  121. package/src/reactivity/wire-validate.ts +13 -13
  122. package/src/reactivity/wire.ts +224 -224
  123. package/src/testing/async-wait.ts +65 -65
  124. package/src/testing/index.ts +2 -2
  125. package/src/testing/test-transactor.ts +638 -502
  126. package/src/transaction/errors.ts +91 -91
  127. package/src/transaction/operations-hash.ts +196 -196
  128. package/src/transaction/read-dependency-collector.ts +78 -78
  129. package/src/transaction/transaction.ts +1 -1
  130. package/src/transactor/change-notifier.ts +80 -80
  131. package/src/transactor/index.ts +5 -5
  132. package/src/transactor/network-transactor.ts +49 -14
  133. package/src/transactor/transactor-source.ts +25 -2
  134. package/src/transform/atomic-proxy.ts +92 -92
  135. package/src/transform/helpers.ts +159 -159
  136. package/src/utility/backoff.ts +95 -95
  137. package/src/utility/batch-coordinator.ts +191 -191
  138. package/dist/src/transaction/context.d.ts +0 -60
  139. package/dist/src/transaction/context.d.ts.map +0 -1
  140. package/dist/src/transaction/context.js +0 -91
  141. 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
+ }