@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,127 +1,127 @@
1
- /**
2
- * Matchmaking — seeker hang-out-vs-continue decision engine (db-core, pure).
3
- *
4
- * Per `docs/matchmaking.md` §Hang-out vs. continue. After a seeker registers at a cohort and receives
5
- * `Accepted` with `topicTraffic` at tree tier `d`, it must decide: is *this* tier the right place to
6
- * wait, or should it walk one tier toward the root? This module is the pure arithmetic of that choice
7
- * — no I/O, no clock — so it is unit-testable directly against the doc's worked examples. The db-p2p
8
- * `seeker-walk-client` drives the walk (register / query / renew / withdraw) and calls {@link decide}.
9
- *
10
- * Decision rule (`docs/matchmaking.md` §Decision rule):
11
- *
12
- * 1. **Immediate-match.** If `currentMatches >= wantCount` → `done`. (The caller has already issued the
13
- * `QueryV1`; `currentMatches` is its filter-matched, re-validated yield. This also covers edge case 2
14
- * — a stale `arrivalsPerMin = 0` after an epoch rotation still gets a real query first, so a quiet
15
- * cohort that actually holds enough providers resolves to `done` rather than a spurious escalate.)
16
- * 2. **Hang-out feasibility.**
17
- * ```
18
- * expectedNewMatches ≈ arrivalsPerMin × filterAcceptRatio × (patienceMsRemaining / 60000)
19
- * contentionFactor ≈ min(1 + (queriesPerMin × meanWantCount) / max(arrivalsPerMin, 1), cap)
20
- * hang out iff currentMatches + expectedNewMatches ≥ wantCount × contentionFactor
21
- * ```
22
- * 3. **Otherwise escalate** (walk one tier toward the root).
23
- *
24
- * Edge cases the engine encodes (the rest live in the walk client, which owns the walk topology):
25
- * - **Filter matches almost nothing (case 4):** a `filterAcceptRatio` decayed toward 0 collapses
26
- * `expectedNewMatches`, so the feasibility test fails at every tier and the seeker walks to the root.
27
- * - **Many seekers competing (case 5):** a high `queriesPerMin` inflates `contentionFactor` up to
28
- * `cap`, raising the threshold so more seekers escalate toward aggregation — self-balancing, bounded.
29
- */
30
-
31
- import type { HangOutConfig } from "./config.js";
32
- import { FILTER_ACCEPT_RATIO_INITIAL, MEAN_WANT_COUNT_DEFAULT } from "./config.js";
33
-
34
- /** Inputs to {@link decide}: the cohort's `topicTraffic` plus the seeker's running state. */
35
- export interface SeekerDecisionInputs {
36
- /** Filter-matched, re-validated providers from the immediate `QueryV1` at this tier. */
37
- readonly currentMatches: number;
38
- /** `topicTraffic.directParticipants` — informational (providers known here right now). */
39
- readonly directParticipants: number;
40
- /** `topicTraffic.arrivalsPerMin` — provider registration + renewal rate. */
41
- readonly arrivalsPerMin: number;
42
- /** `topicTraffic.queriesPerMin` — competing seeker activity over the same pool. */
43
- readonly queriesPerMin: number;
44
- /** `topicTraffic.childCohortCount` — `> 0` means this tier has promoted (descend territory). */
45
- readonly childCohortCount: number;
46
- /** Providers the seeker needs. */
47
- readonly wantCount: number;
48
- /** Patience budget left at this tier (drains across walked tiers — the client tracks it). */
49
- readonly patienceMsRemaining: number;
50
- /** Running estimate of the fraction of returned providers that pass the seeker's filter (decays from 1.0). */
51
- readonly filterAcceptRatio: number;
52
- /** Assumed mean `wantCount` of competing seekers (small constant or learned). */
53
- readonly meanWantCount: number;
54
- }
55
-
56
- /** The outcome of {@link decide}. `hangOut` carries the poll cadence for the requery loop. */
57
- export type SeekerDecision =
58
- | { readonly action: "done" }
59
- | { readonly action: "hangOut"; readonly requeryIntervalMs: number }
60
- | { readonly action: "escalate" };
61
-
62
- /** `expectedNewMatches ≈ arrivalsPerMin × filterAcceptRatio × (patienceMsRemaining / 60000)`. */
63
- export function expectedNewMatches(inputs: SeekerDecisionInputs): number {
64
- return inputs.arrivalsPerMin * inputs.filterAcceptRatio * (inputs.patienceMsRemaining / 60_000);
65
- }
66
-
67
- /** `contentionFactor ≈ min(1 + (queriesPerMin × meanWantCount) / max(arrivalsPerMin, 1), cap)`. */
68
- export function contentionFactor(inputs: SeekerDecisionInputs, cfg: HangOutConfig): number {
69
- const raw = 1 + (inputs.queriesPerMin * inputs.meanWantCount) / Math.max(inputs.arrivalsPerMin, 1);
70
- return Math.min(raw, cfg.contentionFactorCap);
71
- }
72
-
73
- /** The hang-out feasibility threshold `wantCount × contentionFactor`. */
74
- export function hangOutThreshold(inputs: SeekerDecisionInputs, cfg: HangOutConfig): number {
75
- return inputs.wantCount * contentionFactor(inputs, cfg);
76
- }
77
-
78
- /**
79
- * Decide whether the seeker is `done` (immediate match), should `hangOut` (feasible to wait here), or
80
- * should `escalate` (walk one tier toward the root). Pure — see the module header for the rule.
81
- */
82
- export function decide(inputs: SeekerDecisionInputs, cfg: HangOutConfig): SeekerDecision {
83
- if (inputs.currentMatches >= inputs.wantCount) {
84
- return { action: "done" };
85
- }
86
- const projected = inputs.currentMatches + expectedNewMatches(inputs);
87
- if (projected >= hangOutThreshold(inputs, cfg)) {
88
- return { action: "hangOut", requeryIntervalMs: cfg.requeryIntervalMs };
89
- }
90
- return { action: "escalate" };
91
- }
92
-
93
- // --- filterAcceptRatio running refinement (`docs/matchmaking.md` §Decision rule / §Edge cases 4) ---
94
-
95
- /**
96
- * Running yield accumulator for `filterAcceptRatio`. The ratio starts at `filter_accept_ratio_initial`
97
- * (1.0, used before any observation) and is refined to the cumulative `matched / returned` over the
98
- * walk: after two cohorts that each return ~10% matchable providers it settles near 0.1, collapsing
99
- * `expectedNewMatches` for a pathological filter (edge case 4).
100
- */
101
- export interface FilterAcceptRatioState {
102
- /** Total filter-matched providers observed across queries so far. */
103
- readonly matched: number;
104
- /** Total providers returned across queries so far. */
105
- readonly returned: number;
106
- }
107
-
108
- /** A fresh (zero-observation) {@link FilterAcceptRatioState}. */
109
- export function newFilterAcceptRatioState(): FilterAcceptRatioState {
110
- return { matched: 0, returned: 0 };
111
- }
112
-
113
- /** Fold one query's yield (`matched` of `returned`) into the running state. */
114
- export function observeYield(state: FilterAcceptRatioState, matched: number, returned: number): FilterAcceptRatioState {
115
- return { matched: state.matched + matched, returned: state.returned + returned };
116
- }
117
-
118
- /**
119
- * The current `filterAcceptRatio`: cumulative `matched / returned`, or `initial` (1.0) before any
120
- * providers have been returned (so the first hang-out estimate is optimistic, then refines).
121
- */
122
- export function filterAcceptRatio(state: FilterAcceptRatioState, initial: number = FILTER_ACCEPT_RATIO_INITIAL): number {
123
- return state.returned > 0 ? state.matched / state.returned : initial;
124
- }
125
-
126
- /** The default assumed competing-seeker `meanWantCount` (re-exported for the walk client). */
127
- export const DEFAULT_MEAN_WANT_COUNT = MEAN_WANT_COUNT_DEFAULT;
1
+ /**
2
+ * Matchmaking — seeker hang-out-vs-continue decision engine (db-core, pure).
3
+ *
4
+ * Per `docs/matchmaking.md` §Hang-out vs. continue. After a seeker registers at a cohort and receives
5
+ * `Accepted` with `topicTraffic` at tree tier `d`, it must decide: is *this* tier the right place to
6
+ * wait, or should it walk one tier toward the root? This module is the pure arithmetic of that choice
7
+ * — no I/O, no clock — so it is unit-testable directly against the doc's worked examples. The db-p2p
8
+ * `seeker-walk-client` drives the walk (register / query / renew / withdraw) and calls {@link decide}.
9
+ *
10
+ * Decision rule (`docs/matchmaking.md` §Decision rule):
11
+ *
12
+ * 1. **Immediate-match.** If `currentMatches >= wantCount` → `done`. (The caller has already issued the
13
+ * `QueryV1`; `currentMatches` is its filter-matched, re-validated yield. This also covers edge case 2
14
+ * — a stale `arrivalsPerMin = 0` after an epoch rotation still gets a real query first, so a quiet
15
+ * cohort that actually holds enough providers resolves to `done` rather than a spurious escalate.)
16
+ * 2. **Hang-out feasibility.**
17
+ * ```
18
+ * expectedNewMatches ≈ arrivalsPerMin × filterAcceptRatio × (patienceMsRemaining / 60000)
19
+ * contentionFactor ≈ min(1 + (queriesPerMin × meanWantCount) / max(arrivalsPerMin, 1), cap)
20
+ * hang out iff currentMatches + expectedNewMatches ≥ wantCount × contentionFactor
21
+ * ```
22
+ * 3. **Otherwise escalate** (walk one tier toward the root).
23
+ *
24
+ * Edge cases the engine encodes (the rest live in the walk client, which owns the walk topology):
25
+ * - **Filter matches almost nothing (case 4):** a `filterAcceptRatio` decayed toward 0 collapses
26
+ * `expectedNewMatches`, so the feasibility test fails at every tier and the seeker walks to the root.
27
+ * - **Many seekers competing (case 5):** a high `queriesPerMin` inflates `contentionFactor` up to
28
+ * `cap`, raising the threshold so more seekers escalate toward aggregation — self-balancing, bounded.
29
+ */
30
+
31
+ import type { HangOutConfig } from "./config.js";
32
+ import { FILTER_ACCEPT_RATIO_INITIAL, MEAN_WANT_COUNT_DEFAULT } from "./config.js";
33
+
34
+ /** Inputs to {@link decide}: the cohort's `topicTraffic` plus the seeker's running state. */
35
+ export interface SeekerDecisionInputs {
36
+ /** Filter-matched, re-validated providers from the immediate `QueryV1` at this tier. */
37
+ readonly currentMatches: number;
38
+ /** `topicTraffic.directParticipants` — informational (providers known here right now). */
39
+ readonly directParticipants: number;
40
+ /** `topicTraffic.arrivalsPerMin` — provider registration + renewal rate. */
41
+ readonly arrivalsPerMin: number;
42
+ /** `topicTraffic.queriesPerMin` — competing seeker activity over the same pool. */
43
+ readonly queriesPerMin: number;
44
+ /** `topicTraffic.childCohortCount` — `> 0` means this tier has promoted (descend territory). */
45
+ readonly childCohortCount: number;
46
+ /** Providers the seeker needs. */
47
+ readonly wantCount: number;
48
+ /** Patience budget left at this tier (drains across walked tiers — the client tracks it). */
49
+ readonly patienceMsRemaining: number;
50
+ /** Running estimate of the fraction of returned providers that pass the seeker's filter (decays from 1.0). */
51
+ readonly filterAcceptRatio: number;
52
+ /** Assumed mean `wantCount` of competing seekers (small constant or learned). */
53
+ readonly meanWantCount: number;
54
+ }
55
+
56
+ /** The outcome of {@link decide}. `hangOut` carries the poll cadence for the requery loop. */
57
+ export type SeekerDecision =
58
+ | { readonly action: "done" }
59
+ | { readonly action: "hangOut"; readonly requeryIntervalMs: number }
60
+ | { readonly action: "escalate" };
61
+
62
+ /** `expectedNewMatches ≈ arrivalsPerMin × filterAcceptRatio × (patienceMsRemaining / 60000)`. */
63
+ export function expectedNewMatches(inputs: SeekerDecisionInputs): number {
64
+ return inputs.arrivalsPerMin * inputs.filterAcceptRatio * (inputs.patienceMsRemaining / 60_000);
65
+ }
66
+
67
+ /** `contentionFactor ≈ min(1 + (queriesPerMin × meanWantCount) / max(arrivalsPerMin, 1), cap)`. */
68
+ export function contentionFactor(inputs: SeekerDecisionInputs, cfg: HangOutConfig): number {
69
+ const raw = 1 + (inputs.queriesPerMin * inputs.meanWantCount) / Math.max(inputs.arrivalsPerMin, 1);
70
+ return Math.min(raw, cfg.contentionFactorCap);
71
+ }
72
+
73
+ /** The hang-out feasibility threshold `wantCount × contentionFactor`. */
74
+ export function hangOutThreshold(inputs: SeekerDecisionInputs, cfg: HangOutConfig): number {
75
+ return inputs.wantCount * contentionFactor(inputs, cfg);
76
+ }
77
+
78
+ /**
79
+ * Decide whether the seeker is `done` (immediate match), should `hangOut` (feasible to wait here), or
80
+ * should `escalate` (walk one tier toward the root). Pure — see the module header for the rule.
81
+ */
82
+ export function decide(inputs: SeekerDecisionInputs, cfg: HangOutConfig): SeekerDecision {
83
+ if (inputs.currentMatches >= inputs.wantCount) {
84
+ return { action: "done" };
85
+ }
86
+ const projected = inputs.currentMatches + expectedNewMatches(inputs);
87
+ if (projected >= hangOutThreshold(inputs, cfg)) {
88
+ return { action: "hangOut", requeryIntervalMs: cfg.requeryIntervalMs };
89
+ }
90
+ return { action: "escalate" };
91
+ }
92
+
93
+ // --- filterAcceptRatio running refinement (`docs/matchmaking.md` §Decision rule / §Edge cases 4) ---
94
+
95
+ /**
96
+ * Running yield accumulator for `filterAcceptRatio`. The ratio starts at `filter_accept_ratio_initial`
97
+ * (1.0, used before any observation) and is refined to the cumulative `matched / returned` over the
98
+ * walk: after two cohorts that each return ~10% matchable providers it settles near 0.1, collapsing
99
+ * `expectedNewMatches` for a pathological filter (edge case 4).
100
+ */
101
+ export interface FilterAcceptRatioState {
102
+ /** Total filter-matched providers observed across queries so far. */
103
+ readonly matched: number;
104
+ /** Total providers returned across queries so far. */
105
+ readonly returned: number;
106
+ }
107
+
108
+ /** A fresh (zero-observation) {@link FilterAcceptRatioState}. */
109
+ export function newFilterAcceptRatioState(): FilterAcceptRatioState {
110
+ return { matched: 0, returned: 0 };
111
+ }
112
+
113
+ /** Fold one query's yield (`matched` of `returned`) into the running state. */
114
+ export function observeYield(state: FilterAcceptRatioState, matched: number, returned: number): FilterAcceptRatioState {
115
+ return { matched: state.matched + matched, returned: state.returned + returned };
116
+ }
117
+
118
+ /**
119
+ * The current `filterAcceptRatio`: cumulative `matched / returned`, or `initial` (1.0) before any
120
+ * providers have been returned (so the first hang-out estimate is optimistic, then refines).
121
+ */
122
+ export function filterAcceptRatio(state: FilterAcceptRatioState, initial: number = FILTER_ACCEPT_RATIO_INITIAL): number {
123
+ return state.returned > 0 ? state.matched / state.returned : initial;
124
+ }
125
+
126
+ /** The default assumed competing-seeker `meanWantCount` (re-exported for the walk client). */
127
+ export const DEFAULT_MEAN_WANT_COUNT = MEAN_WANT_COUNT_DEFAULT;
@@ -1,86 +1,86 @@
1
- /**
2
- * Matchmaking — seeker registration state (db-core, transport-agnostic).
3
- *
4
- * A {@link MatchmakingSeeker} owns the short-lived seeker state for one topic and builds the signed
5
- * {@link SeekerAppPayloadV1} that the db-p2p `seeker-manager` registers at cohort-topic tier **T2**
6
- * with a short TTL (`seeker_ttl`, default 10 s — `docs/matchmaking.md` §Seeker query). The seeker
7
- * registers briefly so other seekers can find it (collective assembly) and the cohort sees active
8
- * demand.
9
- *
10
- * This module holds *registration* state only. The `QueryV1` issuance and the hang-out-vs-continue
11
- * decision live in the pure {@link import("./seeker-walk.js").decide} engine and the db-p2p
12
- * `seeker-walk-client` that drives the walk; the capability filter is {@link import("./capability-filter.js").matchesFilter}.
13
- *
14
- * Crypto-free: signing is an injected callback, matching {@link MatchmakingProvider}.
15
- */
16
-
17
- import { randomBytes } from "@noble/hashes/utils.js";
18
- import { seekerSigningPayload, type CapabilityFilter, type SeekerAppPayloadV1, encodeSeekerAppPayload } from "./wire.js";
19
-
20
- /** Construction inputs for a {@link MatchmakingSeeker}. */
21
- export interface MatchmakingSeekerOptions {
22
- /** The matchmaking topic this seeker is querying. */
23
- readonly topicId: Uint8Array;
24
- /** Number of providers desired (integer `>= 1`). */
25
- readonly wantCount: number;
26
- /** Multiaddr or PeerId-based callback (collective-assembly use). */
27
- readonly contactHint: string;
28
- /** Optional capability filter (evaluated cohort-side in the next ticket; carried here). */
29
- readonly filter?: CapabilityFilter;
30
- /** Opt into arrival pushes; default false (poll path). Consumed by the next ticket. */
31
- readonly pushOnArrival?: boolean;
32
- /** Sign the canonical registration image; resolves the base64url signature. */
33
- readonly sign: (payload: Uint8Array) => Promise<string>;
34
- /** 16-byte registration correlation id (not signature-bound); default fresh CSPRNG bytes. */
35
- readonly correlationId?: Uint8Array;
36
- /** CSPRNG source (injectable for deterministic tests). Default `@noble/hashes` `randomBytes`. */
37
- readonly randomBytes?: (n: number) => Uint8Array;
38
- }
39
-
40
- /** Live seeker state + signed-payload builder for one matchmaking topic (registration only). */
41
- export class MatchmakingSeeker {
42
- readonly topicId: Uint8Array;
43
- readonly correlationId: Uint8Array;
44
- private readonly wantCount: number;
45
- private readonly contactHint: string;
46
- private readonly filter?: CapabilityFilter;
47
- private readonly pushOnArrival?: boolean;
48
- private readonly sign: (payload: Uint8Array) => Promise<string>;
49
-
50
- constructor(options: MatchmakingSeekerOptions) {
51
- if (!Number.isInteger(options.wantCount) || options.wantCount < 1) {
52
- throw new RangeError(`matchmaking seeker: wantCount must be an integer >= 1, got ${options.wantCount}`);
53
- }
54
- this.topicId = options.topicId;
55
- this.wantCount = options.wantCount;
56
- this.contactHint = options.contactHint;
57
- this.filter = options.filter;
58
- this.pushOnArrival = options.pushOnArrival;
59
- this.sign = options.sign;
60
- const rand = options.randomBytes ?? randomBytes;
61
- this.correlationId = options.correlationId ?? rand(16);
62
- }
63
-
64
- /** Build the signed {@link SeekerAppPayloadV1} for this seeker's registration. */
65
- async buildAppPayload(): Promise<SeekerAppPayloadV1> {
66
- const signature = await this.sign(seekerSigningPayload(this.topicId, this.wantCount));
67
- const payload: SeekerAppPayloadV1 = {
68
- kind: "match-seeker",
69
- wantCount: this.wantCount,
70
- contactHint: this.contactHint,
71
- signature,
72
- };
73
- if (this.filter !== undefined) {
74
- payload.filter = this.filter;
75
- }
76
- if (this.pushOnArrival !== undefined) {
77
- payload.pushOnArrival = this.pushOnArrival;
78
- }
79
- return payload;
80
- }
81
-
82
- /** Build the opaque bytes for the cohort-topic `RegisterV1.appPayload` slot. */
83
- async appPayloadBytes(): Promise<Uint8Array> {
84
- return encodeSeekerAppPayload(await this.buildAppPayload());
85
- }
86
- }
1
+ /**
2
+ * Matchmaking — seeker registration state (db-core, transport-agnostic).
3
+ *
4
+ * A {@link MatchmakingSeeker} owns the short-lived seeker state for one topic and builds the signed
5
+ * {@link SeekerAppPayloadV1} that the db-p2p `seeker-manager` registers at cohort-topic tier **T2**
6
+ * with a short TTL (`seeker_ttl`, default 10 s — `docs/matchmaking.md` §Seeker query). The seeker
7
+ * registers briefly so other seekers can find it (collective assembly) and the cohort sees active
8
+ * demand.
9
+ *
10
+ * This module holds *registration* state only. The `QueryV1` issuance and the hang-out-vs-continue
11
+ * decision live in the pure {@link import("./seeker-walk.js").decide} engine and the db-p2p
12
+ * `seeker-walk-client` that drives the walk; the capability filter is {@link import("./capability-filter.js").matchesFilter}.
13
+ *
14
+ * Crypto-free: signing is an injected callback, matching {@link MatchmakingProvider}.
15
+ */
16
+
17
+ import { randomBytes } from "@noble/hashes/utils.js";
18
+ import { seekerSigningPayload, type CapabilityFilter, type SeekerAppPayloadV1, encodeSeekerAppPayload } from "./wire.js";
19
+
20
+ /** Construction inputs for a {@link MatchmakingSeeker}. */
21
+ export interface MatchmakingSeekerOptions {
22
+ /** The matchmaking topic this seeker is querying. */
23
+ readonly topicId: Uint8Array;
24
+ /** Number of providers desired (integer `>= 1`). */
25
+ readonly wantCount: number;
26
+ /** Multiaddr or PeerId-based callback (collective-assembly use). */
27
+ readonly contactHint: string;
28
+ /** Optional capability filter (evaluated cohort-side in the next ticket; carried here). */
29
+ readonly filter?: CapabilityFilter;
30
+ /** Opt into arrival pushes; default false (poll path). Consumed by the next ticket. */
31
+ readonly pushOnArrival?: boolean;
32
+ /** Sign the canonical registration image; resolves the base64url signature. */
33
+ readonly sign: (payload: Uint8Array) => Promise<string>;
34
+ /** 16-byte registration correlation id (not signature-bound); default fresh CSPRNG bytes. */
35
+ readonly correlationId?: Uint8Array;
36
+ /** CSPRNG source (injectable for deterministic tests). Default `@noble/hashes` `randomBytes`. */
37
+ readonly randomBytes?: (n: number) => Uint8Array;
38
+ }
39
+
40
+ /** Live seeker state + signed-payload builder for one matchmaking topic (registration only). */
41
+ export class MatchmakingSeeker {
42
+ readonly topicId: Uint8Array;
43
+ readonly correlationId: Uint8Array;
44
+ private readonly wantCount: number;
45
+ private readonly contactHint: string;
46
+ private readonly filter?: CapabilityFilter;
47
+ private readonly pushOnArrival?: boolean;
48
+ private readonly sign: (payload: Uint8Array) => Promise<string>;
49
+
50
+ constructor(options: MatchmakingSeekerOptions) {
51
+ if (!Number.isInteger(options.wantCount) || options.wantCount < 1) {
52
+ throw new RangeError(`matchmaking seeker: wantCount must be an integer >= 1, got ${options.wantCount}`);
53
+ }
54
+ this.topicId = options.topicId;
55
+ this.wantCount = options.wantCount;
56
+ this.contactHint = options.contactHint;
57
+ this.filter = options.filter;
58
+ this.pushOnArrival = options.pushOnArrival;
59
+ this.sign = options.sign;
60
+ const rand = options.randomBytes ?? randomBytes;
61
+ this.correlationId = options.correlationId ?? rand(16);
62
+ }
63
+
64
+ /** Build the signed {@link SeekerAppPayloadV1} for this seeker's registration. */
65
+ async buildAppPayload(): Promise<SeekerAppPayloadV1> {
66
+ const signature = await this.sign(seekerSigningPayload(this.topicId, this.wantCount));
67
+ const payload: SeekerAppPayloadV1 = {
68
+ kind: "match-seeker",
69
+ wantCount: this.wantCount,
70
+ contactHint: this.contactHint,
71
+ signature,
72
+ };
73
+ if (this.filter !== undefined) {
74
+ payload.filter = this.filter;
75
+ }
76
+ if (this.pushOnArrival !== undefined) {
77
+ payload.pushOnArrival = this.pushOnArrival;
78
+ }
79
+ return payload;
80
+ }
81
+
82
+ /** Build the opaque bytes for the cohort-topic `RegisterV1.appPayload` slot. */
83
+ async appPayloadBytes(): Promise<Uint8Array> {
84
+ return encodeSeekerAppPayload(await this.buildAppPayload());
85
+ }
86
+ }
@@ -1,90 +1,90 @@
1
- /**
2
- * Matchmaking — stable topic anchor.
3
- *
4
- * Transcribed from `docs/matchmaking.md` §Anchor:
5
- *
6
- * ```
7
- * topicId(kind, label) = H(kind ‖ label ‖ "match")
8
- * ```
9
- *
10
- * Unlike reactivity, matchmaking topics do **not** rotate — a task or capability label has a stable
11
- * identity over its useful lifetime, so the anchor is a pure function of `(kind, label)`.
12
- *
13
- * `kind` namespaces the topic so unrelated label spaces never collide; `label` is application-defined
14
- * (capability name, proposal hash, task-type id). The resulting `topicId` is fed verbatim into
15
- * cohort-topic tier addressing (`coord_d(self, topicId)`), so it is derived with the **same** hash
16
- * primitive cohort-topic uses for `coord_d` input — db-core's own {@link IRingHash} (SHA-256 truncated
17
- * to the ring width), **not** a FRET import. At the default `ringBits = 256` the anchor is a 32-byte
18
- * value, matching the cohort-topic `topicId` width.
19
- *
20
- * Concatenation is delimiter-free, exactly as the spec writes it. This is unambiguous because `kind`
21
- * is drawn from a closed set ({@link MATCH_TOPIC_KINDS}) in which no member is a prefix of another, so
22
- * `kind ‖ label` can never alias a different `(kind, label)` pair. The trailing `"match"` literal
23
- * domain-separates matchmaking anchors from any other application that might hash the same label.
24
- */
25
-
26
- import { createRingHash } from "../cohort-topic/ring-hash.js";
27
- import type { IRingHash } from "../cohort-topic/ports.js";
28
-
29
- /**
30
- * The category of a matchmaking topic. Namespaces the `label` space so unrelated topics never
31
- * collide on the ring (`docs/matchmaking.md` §Anchor).
32
- *
33
- * - `task` — a work-task type id; long-lived, matures a deep tree.
34
- * - `capability` — a capability name (storage class, region, hardware feature); long-lived.
35
- * - `quorum` — a voting-proposal hash; short-lived, forms a shallow tree that demotes once closed.
36
- * - `capacity-class` — a capacity bucket for capacity gossip.
37
- */
38
- export type MatchTopicKind = "task" | "capability" | "quorum" | "capacity-class";
39
-
40
- /** Every matchmaking topic kind. Validated against on the seeker/provider build path. */
41
- export const MATCH_TOPIC_KINDS: readonly MatchTopicKind[] = ["task", "capability", "quorum", "capacity-class"];
42
-
43
- /** Domain-separation suffix mixed into every matchmaking anchor. */
44
- const MATCH_SUFFIX = "match";
45
-
46
- const utf8 = new TextEncoder();
47
-
48
- /** Derives the stable `topicId` for a matchmaking `(kind, label)` pair. */
49
- export interface MatchTopicAnchor {
50
- /** `H(kind ‖ label ‖ "match")` — the cohort-topic `topicId` for this matchmaking topic. */
51
- topicId(kind: MatchTopicKind, label: string): Uint8Array;
52
- }
53
-
54
- /** True iff `kind` is one of the closed {@link MATCH_TOPIC_KINDS}. */
55
- export function isMatchTopicKind(kind: string): kind is MatchTopicKind {
56
- return (MATCH_TOPIC_KINDS as readonly string[]).includes(kind);
57
- }
58
-
59
- class HashMatchTopicAnchor implements MatchTopicAnchor {
60
- constructor(private readonly hash: IRingHash) {}
61
-
62
- topicId(kind: MatchTopicKind, label: string): Uint8Array {
63
- if (!isMatchTopicKind(kind)) {
64
- throw new RangeError(`matchmaking topicId: unknown kind ${JSON.stringify(kind)}`);
65
- }
66
- const kindBytes = utf8.encode(kind);
67
- const labelBytes = utf8.encode(label);
68
- const suffixBytes = utf8.encode(MATCH_SUFFIX);
69
- const input = new Uint8Array(kindBytes.length + labelBytes.length + suffixBytes.length);
70
- input.set(kindBytes, 0);
71
- input.set(labelBytes, kindBytes.length);
72
- input.set(suffixBytes, kindBytes.length + labelBytes.length);
73
- return this.hash.H(input);
74
- }
75
- }
76
-
77
- /**
78
- * Build a {@link MatchTopicAnchor} over the injected hash. db-p2p passes the same {@link IRingHash}
79
- * instance it binds to FRET's `RING_BITS`, so the anchor and cohort-topic routing keys line up; the
80
- * default constructs db-core's own {@link createRingHash} (256-bit SHA-256), which is byte-identical
81
- * to what the cohort-topic host uses.
82
- */
83
- export function createMatchTopicAnchor(hash: IRingHash = createRingHash()): MatchTopicAnchor {
84
- return new HashMatchTopicAnchor(hash);
85
- }
86
-
87
- /** One-shot convenience: `H(kind ‖ label ‖ "match")` over the default db-core ring hash. */
88
- export function matchTopicId(kind: MatchTopicKind, label: string, hash: IRingHash = createRingHash()): Uint8Array {
89
- return createMatchTopicAnchor(hash).topicId(kind, label);
90
- }
1
+ /**
2
+ * Matchmaking — stable topic anchor.
3
+ *
4
+ * Transcribed from `docs/matchmaking.md` §Anchor:
5
+ *
6
+ * ```
7
+ * topicId(kind, label) = H(kind ‖ label ‖ "match")
8
+ * ```
9
+ *
10
+ * Unlike reactivity, matchmaking topics do **not** rotate — a task or capability label has a stable
11
+ * identity over its useful lifetime, so the anchor is a pure function of `(kind, label)`.
12
+ *
13
+ * `kind` namespaces the topic so unrelated label spaces never collide; `label` is application-defined
14
+ * (capability name, proposal hash, task-type id). The resulting `topicId` is fed verbatim into
15
+ * cohort-topic tier addressing (`coord_d(self, topicId)`), so it is derived with the **same** hash
16
+ * primitive cohort-topic uses for `coord_d` input — db-core's own {@link IRingHash} (SHA-256 truncated
17
+ * to the ring width), **not** a FRET import. At the default `ringBits = 256` the anchor is a 32-byte
18
+ * value, matching the cohort-topic `topicId` width.
19
+ *
20
+ * Concatenation is delimiter-free, exactly as the spec writes it. This is unambiguous because `kind`
21
+ * is drawn from a closed set ({@link MATCH_TOPIC_KINDS}) in which no member is a prefix of another, so
22
+ * `kind ‖ label` can never alias a different `(kind, label)` pair. The trailing `"match"` literal
23
+ * domain-separates matchmaking anchors from any other application that might hash the same label.
24
+ */
25
+
26
+ import { createRingHash } from "../cohort-topic/ring-hash.js";
27
+ import type { IRingHash } from "../cohort-topic/ports.js";
28
+
29
+ /**
30
+ * The category of a matchmaking topic. Namespaces the `label` space so unrelated topics never
31
+ * collide on the ring (`docs/matchmaking.md` §Anchor).
32
+ *
33
+ * - `task` — a work-task type id; long-lived, matures a deep tree.
34
+ * - `capability` — a capability name (storage class, region, hardware feature); long-lived.
35
+ * - `quorum` — a voting-proposal hash; short-lived, forms a shallow tree that demotes once closed.
36
+ * - `capacity-class` — a capacity bucket for capacity gossip.
37
+ */
38
+ export type MatchTopicKind = "task" | "capability" | "quorum" | "capacity-class";
39
+
40
+ /** Every matchmaking topic kind. Validated against on the seeker/provider build path. */
41
+ export const MATCH_TOPIC_KINDS: readonly MatchTopicKind[] = ["task", "capability", "quorum", "capacity-class"];
42
+
43
+ /** Domain-separation suffix mixed into every matchmaking anchor. */
44
+ const MATCH_SUFFIX = "match";
45
+
46
+ const utf8 = new TextEncoder();
47
+
48
+ /** Derives the stable `topicId` for a matchmaking `(kind, label)` pair. */
49
+ export interface MatchTopicAnchor {
50
+ /** `H(kind ‖ label ‖ "match")` — the cohort-topic `topicId` for this matchmaking topic. */
51
+ topicId(kind: MatchTopicKind, label: string): Uint8Array;
52
+ }
53
+
54
+ /** True iff `kind` is one of the closed {@link MATCH_TOPIC_KINDS}. */
55
+ export function isMatchTopicKind(kind: string): kind is MatchTopicKind {
56
+ return (MATCH_TOPIC_KINDS as readonly string[]).includes(kind);
57
+ }
58
+
59
+ class HashMatchTopicAnchor implements MatchTopicAnchor {
60
+ constructor(private readonly hash: IRingHash) {}
61
+
62
+ topicId(kind: MatchTopicKind, label: string): Uint8Array {
63
+ if (!isMatchTopicKind(kind)) {
64
+ throw new RangeError(`matchmaking topicId: unknown kind ${JSON.stringify(kind)}`);
65
+ }
66
+ const kindBytes = utf8.encode(kind);
67
+ const labelBytes = utf8.encode(label);
68
+ const suffixBytes = utf8.encode(MATCH_SUFFIX);
69
+ const input = new Uint8Array(kindBytes.length + labelBytes.length + suffixBytes.length);
70
+ input.set(kindBytes, 0);
71
+ input.set(labelBytes, kindBytes.length);
72
+ input.set(suffixBytes, kindBytes.length + labelBytes.length);
73
+ return this.hash.H(input);
74
+ }
75
+ }
76
+
77
+ /**
78
+ * Build a {@link MatchTopicAnchor} over the injected hash. db-p2p passes the same {@link IRingHash}
79
+ * instance it binds to FRET's `RING_BITS`, so the anchor and cohort-topic routing keys line up; the
80
+ * default constructs db-core's own {@link createRingHash} (256-bit SHA-256), which is byte-identical
81
+ * to what the cohort-topic host uses.
82
+ */
83
+ export function createMatchTopicAnchor(hash: IRingHash = createRingHash()): MatchTopicAnchor {
84
+ return new HashMatchTopicAnchor(hash);
85
+ }
86
+
87
+ /** One-shot convenience: `H(kind ‖ label ‖ "match")` over the default db-core ring hash. */
88
+ export function matchTopicId(kind: MatchTopicKind, label: string, hash: IRingHash = createRingHash()): Uint8Array {
89
+ return createMatchTopicAnchor(hash).topicId(kind, label);
90
+ }