@optimystic/db-p2p 0.22.0 → 0.24.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (194) hide show
  1. package/dist/src/cluster/client.d.ts +10 -0
  2. package/dist/src/cluster/client.d.ts.map +1 -1
  3. package/dist/src/cluster/client.js +30 -1
  4. package/dist/src/cluster/client.js.map +1 -1
  5. package/dist/src/cluster/cluster-repo.d.ts +39 -14
  6. package/dist/src/cluster/cluster-repo.d.ts.map +1 -1
  7. package/dist/src/cluster/cluster-repo.js +226 -119
  8. package/dist/src/cluster/cluster-repo.js.map +1 -1
  9. package/dist/src/cluster/service.d.ts +32 -1
  10. package/dist/src/cluster/service.d.ts.map +1 -1
  11. package/dist/src/cluster/service.js +43 -2
  12. package/dist/src/cluster/service.js.map +1 -1
  13. package/dist/src/cohort-topic/host.js +34 -11
  14. package/dist/src/cohort-topic/host.js.map +1 -1
  15. package/dist/src/cohort-topic/stream-util.d.ts +37 -7
  16. package/dist/src/cohort-topic/stream-util.d.ts.map +1 -1
  17. package/dist/src/cohort-topic/stream-util.js +77 -19
  18. package/dist/src/cohort-topic/stream-util.js.map +1 -1
  19. package/dist/src/dispute/dispute-service.d.ts.map +1 -1
  20. package/dist/src/dispute/dispute-service.js +9 -3
  21. package/dist/src/dispute/dispute-service.js.map +1 -1
  22. package/dist/src/index.d.ts +3 -0
  23. package/dist/src/index.d.ts.map +1 -1
  24. package/dist/src/index.js +3 -0
  25. package/dist/src/index.js.map +1 -1
  26. package/dist/src/libp2p-key-network.d.ts +88 -2
  27. package/dist/src/libp2p-key-network.d.ts.map +1 -1
  28. package/dist/src/libp2p-key-network.js +134 -28
  29. package/dist/src/libp2p-key-network.js.map +1 -1
  30. package/dist/src/libp2p-node-base.d.ts.map +1 -1
  31. package/dist/src/libp2p-node-base.js +25 -1
  32. package/dist/src/libp2p-node-base.js.map +1 -1
  33. package/dist/src/logger.d.ts +17 -1
  34. package/dist/src/logger.d.ts.map +1 -1
  35. package/dist/src/logger.js +19 -2
  36. package/dist/src/logger.js.map +1 -1
  37. package/dist/src/matchmaking/query-transport.js +3 -3
  38. package/dist/src/matchmaking/query-transport.js.map +1 -1
  39. package/dist/src/owned-block-seed.d.ts +6 -3
  40. package/dist/src/owned-block-seed.d.ts.map +1 -1
  41. package/dist/src/owned-block-seed.js +16 -3
  42. package/dist/src/owned-block-seed.js.map +1 -1
  43. package/dist/src/peer-address-book.d.ts +72 -0
  44. package/dist/src/peer-address-book.d.ts.map +1 -0
  45. package/dist/src/peer-address-book.js +123 -0
  46. package/dist/src/peer-address-book.js.map +1 -0
  47. package/dist/src/reactivity/notify-transport.d.ts +4 -4
  48. package/dist/src/reactivity/notify-transport.js +6 -6
  49. package/dist/src/reactivity/notify-transport.js.map +1 -1
  50. package/dist/src/reactivity/push-state-gossip.js +2 -2
  51. package/dist/src/reactivity/push-state-gossip.js.map +1 -1
  52. package/dist/src/reactivity/recover-transport.d.ts +6 -2
  53. package/dist/src/reactivity/recover-transport.d.ts.map +1 -1
  54. package/dist/src/reactivity/recover-transport.js +7 -3
  55. package/dist/src/reactivity/recover-transport.js.map +1 -1
  56. package/dist/src/repo/client.d.ts.map +1 -1
  57. package/dist/src/repo/client.js +11 -2
  58. package/dist/src/repo/client.js.map +1 -1
  59. package/dist/src/repo/cluster-coordinator.d.ts +30 -0
  60. package/dist/src/repo/cluster-coordinator.d.ts.map +1 -1
  61. package/dist/src/repo/cluster-coordinator.js +95 -3
  62. package/dist/src/repo/cluster-coordinator.js.map +1 -1
  63. package/dist/src/repo/coordinator-repo.d.ts +62 -9
  64. package/dist/src/repo/coordinator-repo.d.ts.map +1 -1
  65. package/dist/src/repo/coordinator-repo.js +242 -73
  66. package/dist/src/repo/coordinator-repo.js.map +1 -1
  67. package/dist/src/rn.d.ts +3 -0
  68. package/dist/src/rn.d.ts.map +1 -1
  69. package/dist/src/rn.js +3 -0
  70. package/dist/src/rn.js.map +1 -1
  71. package/dist/src/storage/cached-raw-storage.d.ts +83 -0
  72. package/dist/src/storage/cached-raw-storage.d.ts.map +1 -0
  73. package/dist/src/storage/cached-raw-storage.js +152 -0
  74. package/dist/src/storage/cached-raw-storage.js.map +1 -0
  75. package/dist/src/storage/cached-store-driver.d.ts +186 -0
  76. package/dist/src/storage/cached-store-driver.d.ts.map +1 -0
  77. package/dist/src/storage/cached-store-driver.js +775 -0
  78. package/dist/src/storage/cached-store-driver.js.map +1 -0
  79. package/dist/src/storage/i-raw-storage.d.ts +12 -5
  80. package/dist/src/storage/i-raw-storage.d.ts.map +1 -1
  81. package/dist/src/storage/shared-cache-pool.d.ts +234 -0
  82. package/dist/src/storage/shared-cache-pool.d.ts.map +1 -0
  83. package/dist/src/storage/shared-cache-pool.js +354 -0
  84. package/dist/src/storage/shared-cache-pool.js.map +1 -0
  85. package/dist/src/testing/cohort-topic-mesh-harness.d.ts +13 -6
  86. package/dist/src/testing/cohort-topic-mesh-harness.d.ts.map +1 -1
  87. package/dist/src/testing/cohort-topic-mesh-harness.js +15 -6
  88. package/dist/src/testing/cohort-topic-mesh-harness.js.map +1 -1
  89. package/dist/src/testing/raw-storage-conformance.d.ts +2 -1
  90. package/dist/src/testing/raw-storage-conformance.d.ts.map +1 -1
  91. package/dist/src/testing/raw-storage-conformance.js +35 -2
  92. package/dist/src/testing/raw-storage-conformance.js.map +1 -1
  93. package/package.json +3 -3
  94. package/readme.md +668 -668
  95. package/src/cluster/block-transfer.ts +424 -424
  96. package/src/cluster/client.ts +119 -88
  97. package/src/cluster/cluster-error.ts +64 -64
  98. package/src/cluster/cluster-policy.ts +203 -203
  99. package/src/cluster/cluster-repo.ts +242 -122
  100. package/src/cluster/cluster-size-coupling.ts +45 -45
  101. package/src/cluster/commit-cert.ts +139 -139
  102. package/src/cluster/i-transaction-state-store.ts +43 -43
  103. package/src/cluster/memory-transaction-state-store.ts +56 -56
  104. package/src/cluster/peer-key-binding.ts +37 -37
  105. package/src/cluster/persistent-transaction-state-store.ts +92 -92
  106. package/src/cluster/quorum-restore.ts +223 -223
  107. package/src/cluster/reconcile-block.ts +203 -203
  108. package/src/cluster/service.ts +293 -241
  109. package/src/cluster/supermajority-coupling.ts +37 -37
  110. package/src/cohort-topic/bootstrap-evidence-builder.ts +122 -122
  111. package/src/cohort-topic/bootstrap-evidence-verifiers.ts +132 -132
  112. package/src/cohort-topic/bootstrap-parent-reference.ts +159 -159
  113. package/src/cohort-topic/change-bridge.ts +109 -109
  114. package/src/cohort-topic/cohort-gossip-driver.ts +231 -231
  115. package/src/cohort-topic/cohort-gossip-transport.ts +84 -84
  116. package/src/cohort-topic/fret-trust-anchor.ts +153 -153
  117. package/src/cohort-topic/host.ts +42 -11
  118. package/src/cohort-topic/index.ts +13 -13
  119. package/src/cohort-topic/membership-publish-sink.ts +20 -20
  120. package/src/cohort-topic/membership-source.ts +68 -68
  121. package/src/cohort-topic/peer-codec.ts +31 -31
  122. package/src/cohort-topic/peer-sig.ts +86 -86
  123. package/src/cohort-topic/protocols.ts +71 -71
  124. package/src/cohort-topic/reactivity-membership-gate.ts +77 -77
  125. package/src/cohort-topic/size-estimator.ts +16 -16
  126. package/src/cohort-topic/stream-util.ts +79 -19
  127. package/src/cohort-topic/threshold-crypto.ts +239 -239
  128. package/src/cohort-topic/topic-router.ts +77 -77
  129. package/src/dispute/arbitrator-selection.ts +138 -138
  130. package/src/dispute/cascade.ts +524 -524
  131. package/src/dispute/dispute-service.ts +11 -5
  132. package/src/dispute/invalidation.ts +625 -625
  133. package/src/inbound-authorization.ts +190 -190
  134. package/src/index.ts +52 -49
  135. package/src/libp2p-key-network.ts +1120 -990
  136. package/src/libp2p-node-base.ts +1675 -1651
  137. package/src/libp2p-node-rn.ts +30 -30
  138. package/src/libp2p-node.ts +36 -36
  139. package/src/logger.ts +19 -2
  140. package/src/matchmaking/aggregate-counts.ts +104 -104
  141. package/src/matchmaking/index.ts +20 -20
  142. package/src/matchmaking/module.ts +363 -363
  143. package/src/matchmaking/protocols.ts +51 -51
  144. package/src/matchmaking/provider-manager.ts +95 -95
  145. package/src/matchmaking/query-handler.ts +88 -88
  146. package/src/matchmaking/query-transport.ts +3 -3
  147. package/src/matchmaking/seeker-manager.ts +64 -64
  148. package/src/matchmaking/seeker-walk-client.ts +293 -293
  149. package/src/matchmaking/traffic-validation.ts +195 -195
  150. package/src/optimystic-node.ts +36 -36
  151. package/src/owned-block-seed.ts +53 -40
  152. package/src/peer-address-book.ts +149 -0
  153. package/src/protocol-limits.ts +33 -33
  154. package/src/reactivity/forwarder-host.ts +438 -438
  155. package/src/reactivity/index.ts +19 -19
  156. package/src/reactivity/notify-transport.ts +144 -144
  157. package/src/reactivity/origination-manager.ts +192 -192
  158. package/src/reactivity/protocols.ts +61 -61
  159. package/src/reactivity/push-state-gossip.ts +291 -291
  160. package/src/reactivity/recover-transport.ts +7 -3
  161. package/src/reactivity/rotation-rereg-scheduler.ts +256 -256
  162. package/src/reactivity/subscriber-registry.ts +96 -96
  163. package/src/reactivity/subscription-manager.ts +450 -450
  164. package/src/reactivity/topic-bytes.ts +37 -37
  165. package/src/repo/client.ts +12 -2
  166. package/src/repo/cluster-coordinator.ts +99 -3
  167. package/src/repo/coordinator-repo.ts +281 -74
  168. package/src/repo/types.ts +7 -7
  169. package/src/rn.ts +39 -36
  170. package/src/rpc-deadline.ts +45 -45
  171. package/src/storage/arachnode-partition.ts +74 -74
  172. package/src/storage/cached-raw-storage.ts +180 -0
  173. package/src/storage/cached-store-driver.ts +859 -0
  174. package/src/storage/i-kv-store.ts +8 -8
  175. package/src/storage/i-raw-storage.ts +12 -5
  176. package/src/storage/kv-raw-storage.ts +135 -135
  177. package/src/storage/memory-kv-store.ts +28 -28
  178. package/src/storage/memory-storage.ts +25 -25
  179. package/src/storage/memory-store-driver.ts +157 -157
  180. package/src/storage/raw-store-codec.ts +42 -42
  181. package/src/storage/raw-store-driver.ts +80 -80
  182. package/src/storage/ring-selector.ts +317 -317
  183. package/src/storage/ring-shift-coordinator.ts +271 -271
  184. package/src/storage/shared-cache-pool.ts +452 -0
  185. package/src/storage/storage-repo.ts +1014 -1014
  186. package/src/testing/cohort-topic-mesh-harness.ts +673 -663
  187. package/src/testing/index.ts +8 -8
  188. package/src/testing/matchmaking-mesh-harness.ts +475 -475
  189. package/src/testing/raw-storage-conformance.ts +453 -417
  190. package/src/testing/reactivity-mesh-harness.ts +922 -922
  191. package/dist/src/storage/restoration-coordinator-v2.d.ts +0 -67
  192. package/dist/src/storage/restoration-coordinator-v2.d.ts.map +0 -1
  193. package/dist/src/storage/restoration-coordinator-v2.js +0 -172
  194. package/dist/src/storage/restoration-coordinator-v2.js.map +0 -1
@@ -1,51 +1,51 @@
1
- /**
2
- * Matchmaking libp2p protocol IDs (`docs/matchmaking.md` §Seeker query).
3
- *
4
- * Matchmaking is an application layered **above** the cohort-topic substrate, so it owns its own protocol
5
- * family rather than riding the cohort-topic protocols (which carry only substrate concerns — register,
6
- * gossip, promote, membership, sign). Exactly one protocol lives here today:
7
- *
8
- * - `query` — `QueryV1` → `QueryReplyV1`, the seeker's request-reply RPC against a cohort for the
9
- * providers/seekers it locally holds (the serve side is `query-transport.ts`; the seeker
10
- * walk client is the follow-on `matchmaking-query-rpc-seeker-walk`).
11
- *
12
- * The default (network-agnostic) ID omits the network segment; {@link makeMatchmakingProtocols} mirrors
13
- * FRET's `makeProtocols(networkName)` so a named network namespaces its matchmaking protocols the same way
14
- * FRET namespaces its routing protocols (and the cohort-topic / reactivity families namespace via their own
15
- * `make*Protocols`). Production wires the network-agnostic default, matching the cohort-topic + reactivity
16
- * families' production default.
17
- */
18
-
19
- /** Base path for the matchmaking protocol family. */
20
- export const MATCHMAKING_BASE = "/optimystic/matchmaking/1.0.0" as const;
21
-
22
- /** `QueryV1` / `QueryReplyV1` — a seeker's query for a cohort's locally-held provider/seeker registrations. */
23
- export const PROTOCOL_MATCHMAKING_QUERY = `${MATCHMAKING_BASE}/query` as const;
24
-
25
- /** The matchmaking protocol IDs in registration order. */
26
- export interface MatchmakingProtocols {
27
- readonly query: string;
28
- }
29
-
30
- /** Default (network-agnostic) protocol IDs, matching `docs/matchmaking.md`. */
31
- export const DEFAULT_MATCHMAKING_PROTOCOLS: MatchmakingProtocols = {
32
- query: PROTOCOL_MATCHMAKING_QUERY,
33
- };
34
-
35
- /**
36
- * Namespaced matchmaking protocol IDs for `networkName` (mirrors FRET's `makeProtocols`, which inserts the
37
- * network segment even for `"default"` → `/optimystic/default/...`). Note this does NOT equal
38
- * {@link DEFAULT_MATCHMAKING_PROTOCOLS}: the canonical, network-agnostic IDs omit the segment entirely
39
- * (`/optimystic/matchmaking/1.0.0/...`); use those unless you need per-network namespacing.
40
- */
41
- export function makeMatchmakingProtocols(networkName = "default"): MatchmakingProtocols {
42
- const base = `/optimystic/${networkName}/matchmaking/1.0.0`;
43
- return {
44
- query: `${base}/query`,
45
- };
46
- }
47
-
48
- /** All matchmaking protocol IDs as an array (for `node.handle` / `unhandle` over the set). */
49
- export function matchmakingProtocolList(p: MatchmakingProtocols): string[] {
50
- return [p.query];
51
- }
1
+ /**
2
+ * Matchmaking libp2p protocol IDs (`docs/matchmaking.md` §Seeker query).
3
+ *
4
+ * Matchmaking is an application layered **above** the cohort-topic substrate, so it owns its own protocol
5
+ * family rather than riding the cohort-topic protocols (which carry only substrate concerns — register,
6
+ * gossip, promote, membership, sign). Exactly one protocol lives here today:
7
+ *
8
+ * - `query` — `QueryV1` → `QueryReplyV1`, the seeker's request-reply RPC against a cohort for the
9
+ * providers/seekers it locally holds (the serve side is `query-transport.ts`; the seeker
10
+ * walk client is the follow-on `matchmaking-query-rpc-seeker-walk`).
11
+ *
12
+ * The default (network-agnostic) ID omits the network segment; {@link makeMatchmakingProtocols} mirrors
13
+ * FRET's `makeProtocols(networkName)` so a named network namespaces its matchmaking protocols the same way
14
+ * FRET namespaces its routing protocols (and the cohort-topic / reactivity families namespace via their own
15
+ * `make*Protocols`). Production wires the network-agnostic default, matching the cohort-topic + reactivity
16
+ * families' production default.
17
+ */
18
+
19
+ /** Base path for the matchmaking protocol family. */
20
+ export const MATCHMAKING_BASE = "/optimystic/matchmaking/1.0.0" as const;
21
+
22
+ /** `QueryV1` / `QueryReplyV1` — a seeker's query for a cohort's locally-held provider/seeker registrations. */
23
+ export const PROTOCOL_MATCHMAKING_QUERY = `${MATCHMAKING_BASE}/query` as const;
24
+
25
+ /** The matchmaking protocol IDs in registration order. */
26
+ export interface MatchmakingProtocols {
27
+ readonly query: string;
28
+ }
29
+
30
+ /** Default (network-agnostic) protocol IDs, matching `docs/matchmaking.md`. */
31
+ export const DEFAULT_MATCHMAKING_PROTOCOLS: MatchmakingProtocols = {
32
+ query: PROTOCOL_MATCHMAKING_QUERY,
33
+ };
34
+
35
+ /**
36
+ * Namespaced matchmaking protocol IDs for `networkName` (mirrors FRET's `makeProtocols`, which inserts the
37
+ * network segment even for `"default"` → `/optimystic/default/...`). Note this does NOT equal
38
+ * {@link DEFAULT_MATCHMAKING_PROTOCOLS}: the canonical, network-agnostic IDs omit the segment entirely
39
+ * (`/optimystic/matchmaking/1.0.0/...`); use those unless you need per-network namespacing.
40
+ */
41
+ export function makeMatchmakingProtocols(networkName = "default"): MatchmakingProtocols {
42
+ const base = `/optimystic/${networkName}/matchmaking/1.0.0`;
43
+ return {
44
+ query: `${base}/query`,
45
+ };
46
+ }
47
+
48
+ /** All matchmaking protocol IDs as an array (for `node.handle` / `unhandle` over the set). */
49
+ export function matchmakingProtocolList(p: MatchmakingProtocols): string[] {
50
+ return [p.query];
51
+ }
@@ -1,95 +1,95 @@
1
- /**
2
- * Matchmaking — provider manager (db-p2p, wires to the cohort-topic substrate).
3
- *
4
- * Drives a db-core {@link MatchmakingProvider}'s lifecycle against the participant-facing
5
- * {@link CohortTopicService}: register at cohort-topic tier **T2 (functional)** with a profile-based
6
- * TTL, renew to keep the record alive, and withdraw by ceasing renewal (`docs/matchmaking.md`
7
- * §Provider registration).
8
- *
9
- * Self-throttling maps onto the substrate as follows (an honest gap surfaced for the reviewer): the
10
- * cohort-topic `RenewV1` carries **no** `appPayload` and **no** `ttl` field — a renewal is a pure
11
- * keep-alive touch. So a capacity change ("signal full", `capacityBudget = 0`) is realized by
12
- * **re-registering** with the new signed payload (which updates the cohort record's `appState`), not
13
- * by a renewal. Likewise immediate withdrawal (`RenewV1` TTL = 0 in the matchmaking doc) has no wire
14
- * realization yet — {@link withdraw} stops renewing and lets the record age out by TTL. Per the
15
- * §Provider self-throttling GROUNDING resolution, withdrawal is an **optimization, not correctness**,
16
- * so passive TTL expiry is acceptable; an immediate-tombstone renew is a documented cohort-topic
17
- * follow-on.
18
- */
19
-
20
- import { Tier, providerTtlForProfile, type CohortTopicService, type MatchmakingProvider, type NodeProfile, type RegistrationHandle } from "@optimystic/db-core";
21
-
22
- /** Construction inputs for a {@link MatchmakingProviderManager}. */
23
- export interface MatchmakingProviderManagerOptions {
24
- /** Participant-facing cohort-topic substrate API. */
25
- readonly service: CohortTopicService;
26
- /** The provider state/decision object this manager drives. */
27
- readonly provider: MatchmakingProvider;
28
- /** Provider TTL (ms). Default: derived from {@link profile} via `providerTtlForProfile`. */
29
- readonly ttlMs?: number;
30
- /** Node profile used to derive the TTL when {@link ttlMs} is absent (Core 90 s / Edge 60 s). */
31
- readonly profile?: NodeProfile;
32
- }
33
-
34
- /** Wires one matchmaking provider to the cohort-topic substrate at tier T2. */
35
- export class MatchmakingProviderManager {
36
- private readonly service: CohortTopicService;
37
- private readonly provider: MatchmakingProvider;
38
- private readonly ttlMs: number;
39
- private handle?: RegistrationHandle;
40
-
41
- constructor(options: MatchmakingProviderManagerOptions) {
42
- this.service = options.service;
43
- this.provider = options.provider;
44
- this.ttlMs = options.ttlMs ?? (options.profile !== undefined ? providerTtlForProfile(options.profile) : undefined) ?? DEFAULT_PROVIDER_TTL_MS;
45
- }
46
-
47
- /** The live registration handle, or `undefined` before the first {@link register}. */
48
- get registration(): RegistrationHandle | undefined {
49
- return this.handle;
50
- }
51
-
52
- /** Register (or re-register) the provider at tier T2 with the current signed payload. */
53
- async register(): Promise<RegistrationHandle> {
54
- const appPayload = await this.provider.appPayloadBytes();
55
- this.handle = await this.service.register({
56
- topicId: this.provider.topicId,
57
- tier: Tier.T2,
58
- appPayload,
59
- ttl: this.ttlMs,
60
- });
61
- return this.handle;
62
- }
63
-
64
- /** Run one renewal cycle (keep-alive touch). No-op before the first {@link register}. */
65
- async renew(): Promise<void> {
66
- if (this.handle === undefined) {
67
- return;
68
- }
69
- await this.service.renew(this.handle);
70
- }
71
-
72
- /** Set the live capacity budget and push it by re-registering (`RenewV1` cannot carry payload). */
73
- async setCapacity(budget: number): Promise<RegistrationHandle> {
74
- this.provider.setCapacity(budget);
75
- return this.register();
76
- }
77
-
78
- /** Signal "available but at capacity" (`capacityBudget = 0`) and push it by re-registering. */
79
- async signalFull(): Promise<RegistrationHandle> {
80
- this.provider.signalFull();
81
- return this.register();
82
- }
83
-
84
- /** Withdraw: stop renewing and send a best-effort signed tombstone so the cohort frees the record
85
- * immediately; TTL expiry is the fallback (the immediate tombstone is an optimization, not correctness). */
86
- async withdraw(): Promise<void> {
87
- this.provider.markWithdrawn();
88
- if (this.handle !== undefined) {
89
- await this.service.withdraw(this.handle);
90
- }
91
- }
92
- }
93
-
94
- /** Fallback provider TTL when neither an explicit `ttlMs` nor a `profile` is supplied (Core default). */
95
- const DEFAULT_PROVIDER_TTL_MS = 90_000;
1
+ /**
2
+ * Matchmaking — provider manager (db-p2p, wires to the cohort-topic substrate).
3
+ *
4
+ * Drives a db-core {@link MatchmakingProvider}'s lifecycle against the participant-facing
5
+ * {@link CohortTopicService}: register at cohort-topic tier **T2 (functional)** with a profile-based
6
+ * TTL, renew to keep the record alive, and withdraw by ceasing renewal (`docs/matchmaking.md`
7
+ * §Provider registration).
8
+ *
9
+ * Self-throttling maps onto the substrate as follows (an honest gap surfaced for the reviewer): the
10
+ * cohort-topic `RenewV1` carries **no** `appPayload` and **no** `ttl` field — a renewal is a pure
11
+ * keep-alive touch. So a capacity change ("signal full", `capacityBudget = 0`) is realized by
12
+ * **re-registering** with the new signed payload (which updates the cohort record's `appState`), not
13
+ * by a renewal. Likewise immediate withdrawal (`RenewV1` TTL = 0 in the matchmaking doc) has no wire
14
+ * realization yet — {@link withdraw} stops renewing and lets the record age out by TTL. Per the
15
+ * §Provider self-throttling GROUNDING resolution, withdrawal is an **optimization, not correctness**,
16
+ * so passive TTL expiry is acceptable; an immediate-tombstone renew is a documented cohort-topic
17
+ * follow-on.
18
+ */
19
+
20
+ import { Tier, providerTtlForProfile, type CohortTopicService, type MatchmakingProvider, type NodeProfile, type RegistrationHandle } from "@optimystic/db-core";
21
+
22
+ /** Construction inputs for a {@link MatchmakingProviderManager}. */
23
+ export interface MatchmakingProviderManagerOptions {
24
+ /** Participant-facing cohort-topic substrate API. */
25
+ readonly service: CohortTopicService;
26
+ /** The provider state/decision object this manager drives. */
27
+ readonly provider: MatchmakingProvider;
28
+ /** Provider TTL (ms). Default: derived from {@link profile} via `providerTtlForProfile`. */
29
+ readonly ttlMs?: number;
30
+ /** Node profile used to derive the TTL when {@link ttlMs} is absent (Core 90 s / Edge 60 s). */
31
+ readonly profile?: NodeProfile;
32
+ }
33
+
34
+ /** Wires one matchmaking provider to the cohort-topic substrate at tier T2. */
35
+ export class MatchmakingProviderManager {
36
+ private readonly service: CohortTopicService;
37
+ private readonly provider: MatchmakingProvider;
38
+ private readonly ttlMs: number;
39
+ private handle?: RegistrationHandle;
40
+
41
+ constructor(options: MatchmakingProviderManagerOptions) {
42
+ this.service = options.service;
43
+ this.provider = options.provider;
44
+ this.ttlMs = options.ttlMs ?? (options.profile !== undefined ? providerTtlForProfile(options.profile) : undefined) ?? DEFAULT_PROVIDER_TTL_MS;
45
+ }
46
+
47
+ /** The live registration handle, or `undefined` before the first {@link register}. */
48
+ get registration(): RegistrationHandle | undefined {
49
+ return this.handle;
50
+ }
51
+
52
+ /** Register (or re-register) the provider at tier T2 with the current signed payload. */
53
+ async register(): Promise<RegistrationHandle> {
54
+ const appPayload = await this.provider.appPayloadBytes();
55
+ this.handle = await this.service.register({
56
+ topicId: this.provider.topicId,
57
+ tier: Tier.T2,
58
+ appPayload,
59
+ ttl: this.ttlMs,
60
+ });
61
+ return this.handle;
62
+ }
63
+
64
+ /** Run one renewal cycle (keep-alive touch). No-op before the first {@link register}. */
65
+ async renew(): Promise<void> {
66
+ if (this.handle === undefined) {
67
+ return;
68
+ }
69
+ await this.service.renew(this.handle);
70
+ }
71
+
72
+ /** Set the live capacity budget and push it by re-registering (`RenewV1` cannot carry payload). */
73
+ async setCapacity(budget: number): Promise<RegistrationHandle> {
74
+ this.provider.setCapacity(budget);
75
+ return this.register();
76
+ }
77
+
78
+ /** Signal "available but at capacity" (`capacityBudget = 0`) and push it by re-registering. */
79
+ async signalFull(): Promise<RegistrationHandle> {
80
+ this.provider.signalFull();
81
+ return this.register();
82
+ }
83
+
84
+ /** Withdraw: stop renewing and send a best-effort signed tombstone so the cohort frees the record
85
+ * immediately; TTL expiry is the fallback (the immediate tombstone is an optimization, not correctness). */
86
+ async withdraw(): Promise<void> {
87
+ this.provider.markWithdrawn();
88
+ if (this.handle !== undefined) {
89
+ await this.service.withdraw(this.handle);
90
+ }
91
+ }
92
+ }
93
+
94
+ /** Fallback provider TTL when neither an explicit `ttlMs` nor a `profile` is supplied (Core default). */
95
+ const DEFAULT_PROVIDER_TTL_MS = 90_000;
@@ -1,88 +1,88 @@
1
- /**
2
- * Matchmaking — cohort-side `QueryV1` handler (db-p2p, wires the substrate store to the query reply).
3
- *
4
- * `docs/matchmaking.md` §Seeker query / §Capability filter. When a seeker dials a cohort with a
5
- * {@link QueryV1}, the cohort returns its **locally-known direct registrations** for the topic, with the
6
- * capability filter applied. This handler is the thin db-p2p binding: it reads the cohort's local
7
- * {@link RegistrationRecord}s, decodes each one's `appState` into a provider/seeker payload, and hands
8
- * them to the pure db-core {@link evaluateQuery} (filter + truncation + entry building). It then
9
- * attaches the substrate's `topicTraffic` snapshot, the `cohortEpoch`, and the cohort **primary's**
10
- * single-member reply signature (NOT a threshold signature — the reply is advisory; the seeker
11
- * re-validates each entry's `registrationSig` via `verifyProviderEntry`).
12
- *
13
- * The handler is transport-light on purpose: the records, traffic, epoch, and signer are all injected,
14
- * so it unit-tests without a live libp2p stack (mock-tier e2e is a documented follow-on). The FRET host
15
- * supplies `records = store.listByTopic(topicId)`, `topicTraffic` from the cohort `TrafficCounters`,
16
- * the current `cohortEpoch`, and a `sign` bound to this node's peer key.
17
- */
18
-
19
- import {
20
- bytesToB64url,
21
- decodeMatchAppPayload,
22
- evaluateQuery,
23
- queryReplySigningPayload,
24
- type LocalProviderRegistration,
25
- type LocalSeekerRegistration,
26
- type QueryReplyV1,
27
- type QueryV1,
28
- type RegistrationRecord,
29
- type TopicTrafficV1,
30
- } from "@optimystic/db-core";
31
- import { bytesToPeerIdString } from "../cohort-topic/peer-codec.js";
32
-
33
- /** Everything the {@link handleMatchmakingQuery} needs from the cohort substrate, all injected. */
34
- export interface CohortQueryContext {
35
- /** The cohort's local registration records for the queried topic (e.g. `store.listByTopic(topicId)`). */
36
- readonly records: readonly RegistrationRecord[];
37
- /** The substrate's current traffic barometer for the topic (from the cohort `TrafficCounters`). */
38
- readonly topicTraffic: TopicTrafficV1;
39
- /** The current cohort epoch (32 bytes). */
40
- readonly cohortEpoch: Uint8Array;
41
- /** Sign the canonical reply image with the cohort primary's peer key; resolves the base64url signature. */
42
- readonly sign: (payload: Uint8Array) => Promise<string>;
43
- /** Optional logger for records whose `appState` fails to decode (skipped, not fatal). */
44
- readonly log?: (formatter: string, ...args: unknown[]) => void;
45
- }
46
-
47
- /** Build the advisory {@link QueryReplyV1} for `query` from the cohort's local registrations. */
48
- export async function handleMatchmakingQuery(query: QueryV1, ctx: CohortQueryContext): Promise<QueryReplyV1> {
49
- const providers: LocalProviderRegistration[] = [];
50
- const seekers: LocalSeekerRegistration[] = [];
51
-
52
- for (const rec of ctx.records) {
53
- if (rec.appState === undefined) {
54
- continue;
55
- }
56
- const participantId = bytesToPeerIdString(rec.participantId);
57
- let payload;
58
- try {
59
- payload = decodeMatchAppPayload(rec.appState);
60
- } catch (err) {
61
- // A record whose appState isn't a matchmaking payload (or is malformed) is not ours to serve;
62
- // skip it rather than fail the whole reply. Logged so it is never silently swallowed.
63
- ctx.log?.("matchmaking query handler: skipping undecodable record for %s: %o", participantId, err);
64
- continue;
65
- }
66
- if (payload.kind === "match-provider") {
67
- providers.push({ participantId, attachedAt: rec.attachedAt, payload });
68
- } else {
69
- seekers.push({ participantId, attachedAt: rec.attachedAt, payload });
70
- }
71
- }
72
-
73
- const evaluated = evaluateQuery(query, providers, seekers);
74
- const unsigned: Omit<QueryReplyV1, "signature"> = {
75
- v: 1,
76
- truncated: evaluated.truncated,
77
- cohortEpoch: bytesToB64url(ctx.cohortEpoch),
78
- topicTraffic: ctx.topicTraffic,
79
- };
80
- if (evaluated.providers !== undefined) {
81
- unsigned.providers = evaluated.providers;
82
- }
83
- if (evaluated.seekers !== undefined) {
84
- unsigned.seekers = evaluated.seekers;
85
- }
86
- const signature = await ctx.sign(queryReplySigningPayload(unsigned));
87
- return { ...unsigned, signature };
88
- }
1
+ /**
2
+ * Matchmaking — cohort-side `QueryV1` handler (db-p2p, wires the substrate store to the query reply).
3
+ *
4
+ * `docs/matchmaking.md` §Seeker query / §Capability filter. When a seeker dials a cohort with a
5
+ * {@link QueryV1}, the cohort returns its **locally-known direct registrations** for the topic, with the
6
+ * capability filter applied. This handler is the thin db-p2p binding: it reads the cohort's local
7
+ * {@link RegistrationRecord}s, decodes each one's `appState` into a provider/seeker payload, and hands
8
+ * them to the pure db-core {@link evaluateQuery} (filter + truncation + entry building). It then
9
+ * attaches the substrate's `topicTraffic` snapshot, the `cohortEpoch`, and the cohort **primary's**
10
+ * single-member reply signature (NOT a threshold signature — the reply is advisory; the seeker
11
+ * re-validates each entry's `registrationSig` via `verifyProviderEntry`).
12
+ *
13
+ * The handler is transport-light on purpose: the records, traffic, epoch, and signer are all injected,
14
+ * so it unit-tests without a live libp2p stack (mock-tier e2e is a documented follow-on). The FRET host
15
+ * supplies `records = store.listByTopic(topicId)`, `topicTraffic` from the cohort `TrafficCounters`,
16
+ * the current `cohortEpoch`, and a `sign` bound to this node's peer key.
17
+ */
18
+
19
+ import {
20
+ bytesToB64url,
21
+ decodeMatchAppPayload,
22
+ evaluateQuery,
23
+ queryReplySigningPayload,
24
+ type LocalProviderRegistration,
25
+ type LocalSeekerRegistration,
26
+ type QueryReplyV1,
27
+ type QueryV1,
28
+ type RegistrationRecord,
29
+ type TopicTrafficV1,
30
+ } from "@optimystic/db-core";
31
+ import { bytesToPeerIdString } from "../cohort-topic/peer-codec.js";
32
+
33
+ /** Everything the {@link handleMatchmakingQuery} needs from the cohort substrate, all injected. */
34
+ export interface CohortQueryContext {
35
+ /** The cohort's local registration records for the queried topic (e.g. `store.listByTopic(topicId)`). */
36
+ readonly records: readonly RegistrationRecord[];
37
+ /** The substrate's current traffic barometer for the topic (from the cohort `TrafficCounters`). */
38
+ readonly topicTraffic: TopicTrafficV1;
39
+ /** The current cohort epoch (32 bytes). */
40
+ readonly cohortEpoch: Uint8Array;
41
+ /** Sign the canonical reply image with the cohort primary's peer key; resolves the base64url signature. */
42
+ readonly sign: (payload: Uint8Array) => Promise<string>;
43
+ /** Optional logger for records whose `appState` fails to decode (skipped, not fatal). */
44
+ readonly log?: (formatter: string, ...args: unknown[]) => void;
45
+ }
46
+
47
+ /** Build the advisory {@link QueryReplyV1} for `query` from the cohort's local registrations. */
48
+ export async function handleMatchmakingQuery(query: QueryV1, ctx: CohortQueryContext): Promise<QueryReplyV1> {
49
+ const providers: LocalProviderRegistration[] = [];
50
+ const seekers: LocalSeekerRegistration[] = [];
51
+
52
+ for (const rec of ctx.records) {
53
+ if (rec.appState === undefined) {
54
+ continue;
55
+ }
56
+ const participantId = bytesToPeerIdString(rec.participantId);
57
+ let payload;
58
+ try {
59
+ payload = decodeMatchAppPayload(rec.appState);
60
+ } catch (err) {
61
+ // A record whose appState isn't a matchmaking payload (or is malformed) is not ours to serve;
62
+ // skip it rather than fail the whole reply. Logged so it is never silently swallowed.
63
+ ctx.log?.("matchmaking query handler: skipping undecodable record for %s: %o", participantId, err);
64
+ continue;
65
+ }
66
+ if (payload.kind === "match-provider") {
67
+ providers.push({ participantId, attachedAt: rec.attachedAt, payload });
68
+ } else {
69
+ seekers.push({ participantId, attachedAt: rec.attachedAt, payload });
70
+ }
71
+ }
72
+
73
+ const evaluated = evaluateQuery(query, providers, seekers);
74
+ const unsigned: Omit<QueryReplyV1, "signature"> = {
75
+ v: 1,
76
+ truncated: evaluated.truncated,
77
+ cohortEpoch: bytesToB64url(ctx.cohortEpoch),
78
+ topicTraffic: ctx.topicTraffic,
79
+ };
80
+ if (evaluated.providers !== undefined) {
81
+ unsigned.providers = evaluated.providers;
82
+ }
83
+ if (evaluated.seekers !== undefined) {
84
+ unsigned.seekers = evaluated.seekers;
85
+ }
86
+ const signature = await ctx.sign(queryReplySigningPayload(unsigned));
87
+ return { ...unsigned, signature };
88
+ }
@@ -174,9 +174,9 @@ export function createMatchmakingQueryHandler(
174
174
  return encodeQueryReplyV1(reply, maxBytes);
175
175
  } catch (err) {
176
176
  // Any failure — a malformed/foreign query (decode), an oversize reply (encode), or a transient
177
- // `sign` rejection — must never throw out of the stream handler: log + no reply. The outer
178
- // `handleRequestResponse` would otherwise abort the stream; a clean no-reply lets the seeker treat
179
- // it as a benign empty advisory result. Mirrors the reactivity recover serve handler exactly.
177
+ // `sign` rejection — must never throw out of the stream handler: log + no reply. Throwing would
178
+ // make `handleRequestResponse` abort the stream; returning `undefined` makes it reply with an
179
+ // explicit zero-length frame, which the seeker maps to a benign empty advisory result.
180
180
  log("matchmaking query serve: dropping query (no reply): %o", err);
181
181
  return undefined;
182
182
  }
@@ -1,64 +1,64 @@
1
- /**
2
- * Matchmaking — seeker manager (db-p2p, wires to the cohort-topic substrate).
3
- *
4
- * Registers a db-core {@link MatchmakingSeeker} at cohort-topic tier **T2 (functional)** with a short
5
- * TTL (`seeker_ttl`, default 10 s — `docs/matchmaking.md` §Seeker query). The seeker registers
6
- * **briefly** so other seekers can find it (collective assembly) and the cohort sees active demand.
7
- *
8
- * By design this manager does **not** renew by default: the cohort-topic service does not auto-ping
9
- * (renewal is caller-driven), so simply not calling renew lets the registration age out by TTL. This
10
- * makes seeker TTL eviction directly observable — the property this ticket proves. The `QueryV1`
11
- * issuance, filter evaluation, and hang-out decision (which would keep a hanging-out seeker's
12
- * registration alive via renewals) land in `matchmaking-query-filter-hangout`.
13
- */
14
-
15
- import { Tier, SEEKER_TTL_MS, type CohortTopicService, type MatchmakingSeeker, type RegistrationHandle } from "@optimystic/db-core";
16
-
17
- /** Construction inputs for a {@link MatchmakingSeekerManager}. */
18
- export interface MatchmakingSeekerManagerOptions {
19
- /** Participant-facing cohort-topic substrate API. */
20
- readonly service: CohortTopicService;
21
- /** The seeker state/decision object this manager registers. */
22
- readonly seeker: MatchmakingSeeker;
23
- /** Seeker TTL (ms). Default {@link SEEKER_TTL_MS} (10 s). */
24
- readonly ttlMs?: number;
25
- }
26
-
27
- /** Wires one matchmaking seeker's brief registration to the cohort-topic substrate at tier T2. */
28
- export class MatchmakingSeekerManager {
29
- private readonly service: CohortTopicService;
30
- private readonly seeker: MatchmakingSeeker;
31
- private readonly ttlMs: number;
32
- private handle?: RegistrationHandle;
33
-
34
- constructor(options: MatchmakingSeekerManagerOptions) {
35
- this.service = options.service;
36
- this.seeker = options.seeker;
37
- this.ttlMs = options.ttlMs ?? SEEKER_TTL_MS;
38
- }
39
-
40
- /** The live registration handle, or `undefined` before {@link register}. */
41
- get registration(): RegistrationHandle | undefined {
42
- return this.handle;
43
- }
44
-
45
- /** Register the seeker briefly at tier T2 with the short seeker TTL; no renewal is started. */
46
- async register(): Promise<RegistrationHandle> {
47
- const appPayload = await this.seeker.appPayloadBytes();
48
- this.handle = await this.service.register({
49
- topicId: this.seeker.topicId,
50
- tier: Tier.T2,
51
- appPayload,
52
- ttl: this.ttlMs,
53
- });
54
- return this.handle;
55
- }
56
-
57
- /** Drop the seeker registration: stop renewing and send a best-effort signed tombstone so the
58
- * cohort frees the record immediately (TTL expiry is the fallback if the primary is unreachable). */
59
- async withdraw(): Promise<void> {
60
- if (this.handle !== undefined) {
61
- await this.service.withdraw(this.handle);
62
- }
63
- }
64
- }
1
+ /**
2
+ * Matchmaking — seeker manager (db-p2p, wires to the cohort-topic substrate).
3
+ *
4
+ * Registers a db-core {@link MatchmakingSeeker} at cohort-topic tier **T2 (functional)** with a short
5
+ * TTL (`seeker_ttl`, default 10 s — `docs/matchmaking.md` §Seeker query). The seeker registers
6
+ * **briefly** so other seekers can find it (collective assembly) and the cohort sees active demand.
7
+ *
8
+ * By design this manager does **not** renew by default: the cohort-topic service does not auto-ping
9
+ * (renewal is caller-driven), so simply not calling renew lets the registration age out by TTL. This
10
+ * makes seeker TTL eviction directly observable — the property this ticket proves. The `QueryV1`
11
+ * issuance, filter evaluation, and hang-out decision (which would keep a hanging-out seeker's
12
+ * registration alive via renewals) land in `matchmaking-query-filter-hangout`.
13
+ */
14
+
15
+ import { Tier, SEEKER_TTL_MS, type CohortTopicService, type MatchmakingSeeker, type RegistrationHandle } from "@optimystic/db-core";
16
+
17
+ /** Construction inputs for a {@link MatchmakingSeekerManager}. */
18
+ export interface MatchmakingSeekerManagerOptions {
19
+ /** Participant-facing cohort-topic substrate API. */
20
+ readonly service: CohortTopicService;
21
+ /** The seeker state/decision object this manager registers. */
22
+ readonly seeker: MatchmakingSeeker;
23
+ /** Seeker TTL (ms). Default {@link SEEKER_TTL_MS} (10 s). */
24
+ readonly ttlMs?: number;
25
+ }
26
+
27
+ /** Wires one matchmaking seeker's brief registration to the cohort-topic substrate at tier T2. */
28
+ export class MatchmakingSeekerManager {
29
+ private readonly service: CohortTopicService;
30
+ private readonly seeker: MatchmakingSeeker;
31
+ private readonly ttlMs: number;
32
+ private handle?: RegistrationHandle;
33
+
34
+ constructor(options: MatchmakingSeekerManagerOptions) {
35
+ this.service = options.service;
36
+ this.seeker = options.seeker;
37
+ this.ttlMs = options.ttlMs ?? SEEKER_TTL_MS;
38
+ }
39
+
40
+ /** The live registration handle, or `undefined` before {@link register}. */
41
+ get registration(): RegistrationHandle | undefined {
42
+ return this.handle;
43
+ }
44
+
45
+ /** Register the seeker briefly at tier T2 with the short seeker TTL; no renewal is started. */
46
+ async register(): Promise<RegistrationHandle> {
47
+ const appPayload = await this.seeker.appPayloadBytes();
48
+ this.handle = await this.service.register({
49
+ topicId: this.seeker.topicId,
50
+ tier: Tier.T2,
51
+ appPayload,
52
+ ttl: this.ttlMs,
53
+ });
54
+ return this.handle;
55
+ }
56
+
57
+ /** Drop the seeker registration: stop renewing and send a best-effort signed tombstone so the
58
+ * cohort frees the record immediately (TTL expiry is the fallback if the primary is unreachable). */
59
+ async withdraw(): Promise<void> {
60
+ if (this.handle !== undefined) {
61
+ await this.service.withdraw(this.handle);
62
+ }
63
+ }
64
+ }