@optimystic/db-core 0.21.0 → 0.24.0

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 (185) hide show
  1. package/README.md +336 -336
  2. package/dist/src/btree/btree.d.ts +2 -1
  3. package/dist/src/btree/btree.d.ts.map +1 -1
  4. package/dist/src/btree/btree.js +1 -1
  5. package/dist/src/btree/btree.js.map +1 -1
  6. package/dist/src/chain/chain.d.ts +1 -1
  7. package/dist/src/chain/chain.d.ts.map +1 -1
  8. package/dist/src/chain/chain.js +1 -1
  9. package/dist/src/chain/chain.js.map +1 -1
  10. package/dist/src/cluster/structs.d.ts +39 -1
  11. package/dist/src/cluster/structs.d.ts.map +1 -1
  12. package/dist/src/cluster/structs.js +24 -0
  13. package/dist/src/cluster/structs.js.map +1 -1
  14. package/dist/src/collection/collection.d.ts +20 -1
  15. package/dist/src/collection/collection.d.ts.map +1 -1
  16. package/dist/src/collection/collection.js +31 -4
  17. package/dist/src/collection/collection.js.map +1 -1
  18. package/dist/src/collections/diary/diary.d.ts.map +1 -1
  19. package/dist/src/collections/diary/diary.js +2 -1
  20. package/dist/src/collections/diary/diary.js.map +1 -1
  21. package/dist/src/collections/diary/struct.js +1 -1
  22. package/dist/src/collections/diary/struct.js.map +1 -1
  23. package/dist/src/collections/tree/collection-trunk.js +1 -1
  24. package/dist/src/collections/tree/collection-trunk.js.map +1 -1
  25. package/dist/src/collections/tree/struct.d.ts +1 -1
  26. package/dist/src/collections/tree/struct.d.ts.map +1 -1
  27. package/dist/src/collections/tree/struct.js +2 -1
  28. package/dist/src/collections/tree/struct.js.map +1 -1
  29. package/dist/src/collections/tree/tree.d.ts +5 -0
  30. package/dist/src/collections/tree/tree.d.ts.map +1 -1
  31. package/dist/src/collections/tree/tree.js +7 -0
  32. package/dist/src/collections/tree/tree.js.map +1 -1
  33. package/dist/src/log/log.d.ts +1 -1
  34. package/dist/src/log/log.d.ts.map +1 -1
  35. package/dist/src/log/log.js +2 -2
  36. package/dist/src/log/log.js.map +1 -1
  37. package/dist/src/network/i-peer-network.d.ts +16 -0
  38. package/dist/src/network/i-peer-network.d.ts.map +1 -1
  39. package/dist/src/network/struct.d.ts +39 -2
  40. package/dist/src/network/struct.d.ts.map +1 -1
  41. package/dist/src/network/struct.js +18 -0
  42. package/dist/src/network/struct.js.map +1 -1
  43. package/dist/src/testing/test-transactor.d.ts +95 -8
  44. package/dist/src/testing/test-transactor.d.ts.map +1 -1
  45. package/dist/src/testing/test-transactor.js +133 -12
  46. package/dist/src/testing/test-transactor.js.map +1 -1
  47. package/dist/src/transaction/coordinator.d.ts.map +1 -1
  48. package/dist/src/transaction/coordinator.js +2 -1
  49. package/dist/src/transaction/coordinator.js.map +1 -1
  50. package/dist/src/transaction/transaction.d.ts +1 -1
  51. package/dist/src/transaction/transaction.js +1 -1
  52. package/dist/src/transactor/network-transactor.d.ts.map +1 -1
  53. package/dist/src/transactor/network-transactor.js +57 -18
  54. package/dist/src/transactor/network-transactor.js.map +1 -1
  55. package/dist/src/transactor/transactor-source.d.ts.map +1 -1
  56. package/dist/src/transactor/transactor-source.js +25 -2
  57. package/dist/src/transactor/transactor-source.js.map +1 -1
  58. package/dist/src/transform/cache-source.js +1 -1
  59. package/dist/src/transform/cache-source.js.map +1 -1
  60. package/dist/src/transform/helpers.d.ts +6 -1
  61. package/dist/src/transform/helpers.d.ts.map +1 -1
  62. package/dist/src/transform/helpers.js +7 -6
  63. package/dist/src/transform/helpers.js.map +1 -1
  64. package/dist/src/transform/tracker.d.ts.map +1 -1
  65. package/dist/src/transform/tracker.js +2 -1
  66. package/dist/src/transform/tracker.js.map +1 -1
  67. package/package.json +1 -1
  68. package/src/btree/btree.ts +2 -1
  69. package/src/chain/chain.ts +2 -1
  70. package/src/cluster/membership.ts +85 -85
  71. package/src/cluster/structs.ts +43 -4
  72. package/src/cohort-topic/addressing.ts +120 -120
  73. package/src/cohort-topic/antidos/bootstrap-evidence-envelope.ts +253 -253
  74. package/src/cohort-topic/antidos/bootstrap-evidence.ts +106 -106
  75. package/src/cohort-topic/antidos/index.ts +5 -5
  76. package/src/cohort-topic/antidos/rate-limiter.ts +210 -210
  77. package/src/cohort-topic/antidos/replay-guard.ts +146 -146
  78. package/src/cohort-topic/antidos/topic-budget.ts +160 -160
  79. package/src/cohort-topic/antiflood/index.ts +2 -2
  80. package/src/cohort-topic/antiflood/invariants.ts +108 -108
  81. package/src/cohort-topic/antiflood/jitter.ts +117 -117
  82. package/src/cohort-topic/coldstart.ts +237 -237
  83. package/src/cohort-topic/dmax.ts +88 -88
  84. package/src/cohort-topic/gossip/bus.ts +254 -254
  85. package/src/cohort-topic/gossip/index.ts +3 -3
  86. package/src/cohort-topic/gossip/records.ts +45 -45
  87. package/src/cohort-topic/gossip/view.ts +91 -91
  88. package/src/cohort-topic/index.ts +20 -20
  89. package/src/cohort-topic/load/barometer.ts +134 -134
  90. package/src/cohort-topic/load/index.ts +1 -1
  91. package/src/cohort-topic/member-engine.ts +430 -430
  92. package/src/cohort-topic/membership/index.ts +3 -3
  93. package/src/cohort-topic/membership/publisher.ts +163 -163
  94. package/src/cohort-topic/membership/source.ts +41 -41
  95. package/src/cohort-topic/membership/verifier.ts +461 -461
  96. package/src/cohort-topic/ports.ts +157 -157
  97. package/src/cohort-topic/promotion.ts +405 -405
  98. package/src/cohort-topic/registration/bytes.ts +37 -37
  99. package/src/cohort-topic/registration/handoff.ts +154 -154
  100. package/src/cohort-topic/registration/index.ts +6 -6
  101. package/src/cohort-topic/registration/renewal.ts +495 -495
  102. package/src/cohort-topic/registration/sharding.ts +61 -61
  103. package/src/cohort-topic/registration/store.ts +81 -81
  104. package/src/cohort-topic/registration/types.ts +91 -91
  105. package/src/cohort-topic/ring-hash.ts +50 -50
  106. package/src/cohort-topic/service.ts +416 -416
  107. package/src/cohort-topic/sig/index.ts +2 -2
  108. package/src/cohort-topic/sig/payloads.ts +59 -59
  109. package/src/cohort-topic/sig/threshold.ts +64 -64
  110. package/src/cohort-topic/tiers.ts +74 -74
  111. package/src/cohort-topic/traffic.ts +233 -233
  112. package/src/cohort-topic/walk.ts +326 -326
  113. package/src/cohort-topic/willingness.ts +237 -237
  114. package/src/cohort-topic/wire/codec.ts +216 -216
  115. package/src/cohort-topic/wire/index.ts +18 -18
  116. package/src/cohort-topic/wire/payloads.ts +126 -126
  117. package/src/cohort-topic/wire/primitives.ts +188 -188
  118. package/src/cohort-topic/wire/types.ts +475 -475
  119. package/src/cohort-topic/wire/validate.ts +512 -512
  120. package/src/collection/collection-type-registry.ts +37 -37
  121. package/src/collection/collection.ts +32 -4
  122. package/src/collections/diary/diary.ts +68 -67
  123. package/src/collections/diary/struct.ts +1 -1
  124. package/src/collections/tree/collection-trunk.ts +1 -1
  125. package/src/collections/tree/readme.md +4 -0
  126. package/src/collections/tree/struct.ts +3 -1
  127. package/src/collections/tree/tree.ts +320 -312
  128. package/src/log/log.ts +2 -2
  129. package/src/matchmaking/capability-filter.ts +45 -45
  130. package/src/matchmaking/config.ts +98 -98
  131. package/src/matchmaking/index.ts +21 -21
  132. package/src/matchmaking/multi-cohort-seeker.ts +234 -234
  133. package/src/matchmaking/provider.ts +123 -123
  134. package/src/matchmaking/query-eval.ts +105 -105
  135. package/src/matchmaking/seeker-walk.ts +127 -127
  136. package/src/matchmaking/seeker.ts +86 -86
  137. package/src/matchmaking/topic-anchor.ts +90 -90
  138. package/src/matchmaking/voting-quorum.ts +394 -394
  139. package/src/matchmaking/wire.ts +603 -603
  140. package/src/network/i-peer-network.ts +17 -0
  141. package/src/network/stale-failure.ts +43 -43
  142. package/src/network/struct.ts +41 -2
  143. package/src/network/types.ts +37 -37
  144. package/src/reactivity/backfill.ts +220 -220
  145. package/src/reactivity/backpressure.ts +191 -191
  146. package/src/reactivity/checkpoint.ts +308 -308
  147. package/src/reactivity/config.ts +172 -172
  148. package/src/reactivity/dedupe.ts +132 -132
  149. package/src/reactivity/forwarder.ts +87 -87
  150. package/src/reactivity/index.ts +34 -34
  151. package/src/reactivity/notification.ts +123 -123
  152. package/src/reactivity/policy.ts +79 -79
  153. package/src/reactivity/push-state.ts +310 -310
  154. package/src/reactivity/recover.ts +153 -153
  155. package/src/reactivity/replay-buffer.ts +141 -141
  156. package/src/reactivity/resume.ts +549 -549
  157. package/src/reactivity/rotation.ts +415 -415
  158. package/src/reactivity/subscriber.ts +132 -132
  159. package/src/reactivity/subscription.ts +66 -66
  160. package/src/reactivity/topic-anchor.ts +71 -71
  161. package/src/reactivity/verify.ts +73 -73
  162. package/src/reactivity/wire-validate.ts +13 -13
  163. package/src/reactivity/wire.ts +224 -224
  164. package/src/testing/async-wait.ts +65 -65
  165. package/src/testing/index.ts +2 -2
  166. package/src/testing/test-transactor.ts +638 -489
  167. package/src/transaction/coordinator.ts +2 -1
  168. package/src/transaction/errors.ts +91 -91
  169. package/src/transaction/operations-hash.ts +196 -196
  170. package/src/transaction/read-dependency-collector.ts +78 -78
  171. package/src/transaction/transaction.ts +1 -1
  172. package/src/transactor/change-notifier.ts +80 -80
  173. package/src/transactor/index.ts +5 -5
  174. package/src/transactor/network-transactor.ts +58 -19
  175. package/src/transactor/transactor-source.ts +25 -2
  176. package/src/transform/atomic-proxy.ts +92 -92
  177. package/src/transform/cache-source.ts +1 -1
  178. package/src/transform/helpers.ts +159 -158
  179. package/src/transform/tracker.ts +2 -1
  180. package/src/utility/backoff.ts +95 -95
  181. package/src/utility/batch-coordinator.ts +191 -191
  182. package/dist/src/transaction/context.d.ts +0 -60
  183. package/dist/src/transaction/context.d.ts.map +0 -1
  184. package/dist/src/transaction/context.js +0 -91
  185. package/dist/src/transaction/context.js.map +0 -1
@@ -1,108 +1,108 @@
1
- /**
2
- * Cohort-topic substrate — anti-flood structural invariants (centralized for the e2e suite).
3
- *
4
- * Transcribed from `docs/cohort-topic.md` §Anti-flood properties. Three of the five named floods are
5
- * defended *by construction* in the walk / promotion machinery already; this module does not
6
- * re-implement that machinery — it provides pure, dependency-free predicates over a recorded walk
7
- * trace so the unit and e2e suites can assert the invariants hold on the real engine's behaviour,
8
- * the same way `packages/substrate-simulator/src/walk-metrics.ts` instruments
9
- * `outwardMovesArePromoted` / `unwillingRetriesRestartAtDMax` on the simulator side.
10
- *
11
- * The caller records one {@link WalkProbe} per probe RPC the {@link import("../walk.js").WalkEngine}
12
- * issues (its `treeTier` and the reply `result`), grouping the probes of one
13
- * {@link import("../walk.js").WalkEngine.register} call into a {@link WalkTrace}. The predicates then
14
- * check the anti-flood discipline:
15
- *
16
- * - **claim 3 — no speculative outward probe:** {@link outwardMovesArePromoted}. The only tier
17
- * *increase* between consecutive probes is one taken immediately after a `promoted` reply.
18
- * - **walk discipline — inward only on `no_state`:** {@link inwardStepsFollowNoState}. Every single
19
- * tier *decrease* is the response to a `no_state` (no inward move on any other reply).
20
- * - **claim 4 — inward retry restarts at `d_max`:** {@link retriesRestartAtDMax}. A fresh walk (the
21
- * restart after an `unwilling_cohort` / cohort-level back-off) begins at `d_max`, never re-hitting
22
- * the declined coord.
23
- * - **claim 5 — sticky promotion:** {@link DEFAULT_T_PROMOTE_STICKY_MS} re-exported as the canonical
24
- * sticky window; the promotion lifecycle (`promotion.ts`) enforces it and `promotion.spec.ts`
25
- * covers the no-flap behaviour. {@link stickyHolds} is the predicate the e2e suite uses to confirm
26
- * a freshly-promoted cohort refuses demotion within the window.
27
- */
28
-
29
- import type { RegisterResult } from "../wire/types.js";
30
- import { DEFAULT_T_PROMOTE_STICKY_MS } from "../promotion.js";
31
-
32
- export { DEFAULT_T_PROMOTE_STICKY_MS };
33
-
34
- /** One probe RPC in a walk: the tier it was issued at and the reply result it drew. */
35
- export interface WalkProbe {
36
- /** Walk position `d` the probe was issued at. */
37
- readonly treeTier: number;
38
- /** The cohort reply classification. */
39
- readonly result: RegisterResult;
40
- }
41
-
42
- /** The ordered probe log of a single {@link import("../walk.js").WalkEngine.register} call. */
43
- export interface WalkTrace {
44
- /** `d_max` the walk started from. */
45
- readonly dMax: number;
46
- /** Probes in issue order. */
47
- readonly probes: readonly WalkProbe[];
48
- }
49
-
50
- /**
51
- * Claim 3 (no speculative outward probe): every tier *increase* between consecutive probes is taken
52
- * only in response to a `promoted` redirect on the preceding probe. Any outward move not preceded by
53
- * `promoted` is a speculative deeper probe — the flood this invariant forbids.
54
- */
55
- export function outwardMovesArePromoted(trace: WalkTrace): boolean {
56
- const { probes } = trace;
57
- for (let i = 1; i < probes.length; i++) {
58
- const moveOutward = probes[i]!.treeTier > probes[i - 1]!.treeTier;
59
- if (moveOutward && probes[i - 1]!.result !== "promoted") {
60
- return false;
61
- }
62
- }
63
- return true;
64
- }
65
-
66
- /**
67
- * Walk discipline: every single inward step (`treeTier` decreased by exactly 1, or reset to 0 at the
68
- * root cold-start) is the response to a `no_state`. The walk never moves toward the root on any other
69
- * reply, so inward traffic is one-hop-per-`no_state`, not a storm.
70
- */
71
- export function inwardStepsFollowNoState(trace: WalkTrace): boolean {
72
- const { probes } = trace;
73
- for (let i = 1; i < probes.length; i++) {
74
- const moveInward = probes[i]!.treeTier < probes[i - 1]!.treeTier;
75
- if (moveInward && probes[i - 1]!.result !== "no_state") {
76
- return false;
77
- }
78
- }
79
- return true;
80
- }
81
-
82
- /**
83
- * Claim 4 (inward retry restarts at `d_max`): each fresh walk in a re-registration sequence starts at
84
- * its `d_max`, so a participant that drew `unwilling_cohort` backs off in *time* and re-walks from the
85
- * top rather than re-hitting the declined coord. `traces` are consecutive `register` calls by one
86
- * participant; the first probe of each must sit at that walk's `d_max`.
87
- */
88
- export function retriesRestartAtDMax(traces: readonly WalkTrace[]): boolean {
89
- for (const trace of traces) {
90
- const first = trace.probes[0];
91
- if (first === undefined) {
92
- continue;
93
- }
94
- if (first.treeTier !== trace.dMax) {
95
- return false;
96
- }
97
- }
98
- return true;
99
- }
100
-
101
- /**
102
- * Claim 5 (sticky promotion): a cohort promoted at `promotedAt` must not be reconsidered for demotion
103
- * before `promotedAt + stickyMs`. Returns whether `now` is still inside the sticky window — the e2e
104
- * suite asserts the promotion lifecycle refuses demotion exactly while this is `true`.
105
- */
106
- export function stickyHolds(promotedAt: number, now: number, stickyMs: number = DEFAULT_T_PROMOTE_STICKY_MS): boolean {
107
- return now - promotedAt < stickyMs;
108
- }
1
+ /**
2
+ * Cohort-topic substrate — anti-flood structural invariants (centralized for the e2e suite).
3
+ *
4
+ * Transcribed from `docs/cohort-topic.md` §Anti-flood properties. Three of the five named floods are
5
+ * defended *by construction* in the walk / promotion machinery already; this module does not
6
+ * re-implement that machinery — it provides pure, dependency-free predicates over a recorded walk
7
+ * trace so the unit and e2e suites can assert the invariants hold on the real engine's behaviour,
8
+ * the same way `packages/substrate-simulator/src/walk-metrics.ts` instruments
9
+ * `outwardMovesArePromoted` / `unwillingRetriesRestartAtDMax` on the simulator side.
10
+ *
11
+ * The caller records one {@link WalkProbe} per probe RPC the {@link import("../walk.js").WalkEngine}
12
+ * issues (its `treeTier` and the reply `result`), grouping the probes of one
13
+ * {@link import("../walk.js").WalkEngine.register} call into a {@link WalkTrace}. The predicates then
14
+ * check the anti-flood discipline:
15
+ *
16
+ * - **claim 3 — no speculative outward probe:** {@link outwardMovesArePromoted}. The only tier
17
+ * *increase* between consecutive probes is one taken immediately after a `promoted` reply.
18
+ * - **walk discipline — inward only on `no_state`:** {@link inwardStepsFollowNoState}. Every single
19
+ * tier *decrease* is the response to a `no_state` (no inward move on any other reply).
20
+ * - **claim 4 — inward retry restarts at `d_max`:** {@link retriesRestartAtDMax}. A fresh walk (the
21
+ * restart after an `unwilling_cohort` / cohort-level back-off) begins at `d_max`, never re-hitting
22
+ * the declined coord.
23
+ * - **claim 5 — sticky promotion:** {@link DEFAULT_T_PROMOTE_STICKY_MS} re-exported as the canonical
24
+ * sticky window; the promotion lifecycle (`promotion.ts`) enforces it and `promotion.spec.ts`
25
+ * covers the no-flap behaviour. {@link stickyHolds} is the predicate the e2e suite uses to confirm
26
+ * a freshly-promoted cohort refuses demotion within the window.
27
+ */
28
+
29
+ import type { RegisterResult } from "../wire/types.js";
30
+ import { DEFAULT_T_PROMOTE_STICKY_MS } from "../promotion.js";
31
+
32
+ export { DEFAULT_T_PROMOTE_STICKY_MS };
33
+
34
+ /** One probe RPC in a walk: the tier it was issued at and the reply result it drew. */
35
+ export interface WalkProbe {
36
+ /** Walk position `d` the probe was issued at. */
37
+ readonly treeTier: number;
38
+ /** The cohort reply classification. */
39
+ readonly result: RegisterResult;
40
+ }
41
+
42
+ /** The ordered probe log of a single {@link import("../walk.js").WalkEngine.register} call. */
43
+ export interface WalkTrace {
44
+ /** `d_max` the walk started from. */
45
+ readonly dMax: number;
46
+ /** Probes in issue order. */
47
+ readonly probes: readonly WalkProbe[];
48
+ }
49
+
50
+ /**
51
+ * Claim 3 (no speculative outward probe): every tier *increase* between consecutive probes is taken
52
+ * only in response to a `promoted` redirect on the preceding probe. Any outward move not preceded by
53
+ * `promoted` is a speculative deeper probe — the flood this invariant forbids.
54
+ */
55
+ export function outwardMovesArePromoted(trace: WalkTrace): boolean {
56
+ const { probes } = trace;
57
+ for (let i = 1; i < probes.length; i++) {
58
+ const moveOutward = probes[i]!.treeTier > probes[i - 1]!.treeTier;
59
+ if (moveOutward && probes[i - 1]!.result !== "promoted") {
60
+ return false;
61
+ }
62
+ }
63
+ return true;
64
+ }
65
+
66
+ /**
67
+ * Walk discipline: every single inward step (`treeTier` decreased by exactly 1, or reset to 0 at the
68
+ * root cold-start) is the response to a `no_state`. The walk never moves toward the root on any other
69
+ * reply, so inward traffic is one-hop-per-`no_state`, not a storm.
70
+ */
71
+ export function inwardStepsFollowNoState(trace: WalkTrace): boolean {
72
+ const { probes } = trace;
73
+ for (let i = 1; i < probes.length; i++) {
74
+ const moveInward = probes[i]!.treeTier < probes[i - 1]!.treeTier;
75
+ if (moveInward && probes[i - 1]!.result !== "no_state") {
76
+ return false;
77
+ }
78
+ }
79
+ return true;
80
+ }
81
+
82
+ /**
83
+ * Claim 4 (inward retry restarts at `d_max`): each fresh walk in a re-registration sequence starts at
84
+ * its `d_max`, so a participant that drew `unwilling_cohort` backs off in *time* and re-walks from the
85
+ * top rather than re-hitting the declined coord. `traces` are consecutive `register` calls by one
86
+ * participant; the first probe of each must sit at that walk's `d_max`.
87
+ */
88
+ export function retriesRestartAtDMax(traces: readonly WalkTrace[]): boolean {
89
+ for (const trace of traces) {
90
+ const first = trace.probes[0];
91
+ if (first === undefined) {
92
+ continue;
93
+ }
94
+ if (first.treeTier !== trace.dMax) {
95
+ return false;
96
+ }
97
+ }
98
+ return true;
99
+ }
100
+
101
+ /**
102
+ * Claim 5 (sticky promotion): a cohort promoted at `promotedAt` must not be reconsidered for demotion
103
+ * before `promotedAt + stickyMs`. Returns whether `now` is still inside the sticky window — the e2e
104
+ * suite asserts the promotion lifecycle refuses demotion exactly while this is `true`.
105
+ */
106
+ export function stickyHolds(promotedAt: number, now: number, stickyMs: number = DEFAULT_T_PROMOTE_STICKY_MS): boolean {
107
+ return now - promotedAt < stickyMs;
108
+ }
@@ -1,117 +1,117 @@
1
- /**
2
- * Cohort-topic substrate — re-registration jitter (anti-flood claim 2).
3
- *
4
- * Transcribed from `docs/cohort-topic.md` §Anti-flood properties (claim 2: "Re-registration storm
5
- * after cohort failure") and folded back from the simulator-validated
6
- * `packages/substrate-simulator/src/walk.ts` (`rejoinStagger` / `rateLimitedStagger`). When a cohort
7
- * fails, every participant it served re-registers at once; without staggering, the recovering or
8
- * replacement cohort takes the whole wave in a single instant and is shoved straight past
9
- * `cap_promote`. Spreading the wave over `T_rejoin_jitter` (default 30 s, widened with the observed
10
- * FRET cohort-failure rate) bounds the recovering cohort's inbound rate to
11
- * `cap_promote / T_rejoin_jitter`.
12
- *
13
- * Two staggering forms, both exposed here:
14
- * - {@link RejoinJitter.scheduleRejoin} — a single participant draws a uniform offset over the
15
- * jitter window. Decorrelates one participant's retry from its peers (the production API: a
16
- * participant that loses its cohort calls this once). Random, so the bound holds *in expectation*.
17
- * - {@link RejoinJitter.scheduleWave} — a whole wave of `count` rejoiners is placed at a fixed
18
- * interval `windowMs / cap_promote`, so **any** `windowMs`-long window contains at most
19
- * `cap_promote` arrivals *by construction*. This is the hard-bound form the e2e suite asserts
20
- * against; it widens the span past one window when `count > cap_promote` so the rate ceiling
21
- * `cap_promote / T_rejoin_jitter` is never exceeded.
22
- *
23
- * This module is FRET-free and side-effect-free apart from the injected RNG: the caller schedules a
24
- * timer at the returned timestamp.
25
- */
26
-
27
- import { createLogger } from "../../logger.js";
28
-
29
- const log = createLogger("cohort-topic:antiflood");
30
-
31
- /** Default jitter window `T_rejoin_jitter` (ms) — `docs/cohort-topic.md` §Configuration. */
32
- export const DEFAULT_T_REJOIN_JITTER_MS = 30_000;
33
- /** Default `cap_promote` — sets the inbound-rate ceiling `cap_promote / T_rejoin_jitter`. */
34
- export const DEFAULT_REJOIN_CAP_PROMOTE = 64;
35
-
36
- export interface RejoinJitterConfig {
37
- /** Base jitter window `T_rejoin_jitter` (ms). Default {@link DEFAULT_T_REJOIN_JITTER_MS}. */
38
- tRejoinJitterMs?: number;
39
- /** Acceptance ceiling per window; the rate bound is `capPromote / T_rejoin_jitter`. Default 64. */
40
- capPromote?: number;
41
- /**
42
- * Multiplier applied to the base window to track the observed FRET cohort-failure rate (claim 2:
43
- * "scaled with cohort failure rate observed from FRET"). A higher failure rate → a wider window so
44
- * the larger expected re-registration wave still respects the rate ceiling. Default 1 (steady FRET).
45
- */
46
- failureRateScale?: number;
47
- /** Uniform RNG in `[0, 1)`. Injected for determinism in tests; defaults to `Math.random`. */
48
- random?: () => number;
49
- }
50
-
51
- /** Staggers post-cohort-failure re-registration so the recovering cohort isn't stormed. */
52
- export interface RejoinJitter {
53
- /**
54
- * Jittered re-registration timestamp for **one** participant: `now + ⌊U[0, windowMs)⌋`. Random,
55
- * so a wave of independent callers spreads roughly uniformly over the window.
56
- */
57
- scheduleRejoin(now: number): number;
58
- /**
59
- * Rate-bounded timestamps for a whole re-registration wave of `count` participants. Arrivals are
60
- * evenly spaced at `windowMs / capPromote`, so any `windowMs`-long sliding window holds at most
61
- * `capPromote` of them — the `cap_promote / T_rejoin_jitter` ceiling, exactly rather than in
62
- * expectation. Returns one timestamp per participant, ascending.
63
- */
64
- scheduleWave(count: number, now: number): number[];
65
- /** Effective jitter window (ms) = base `T_rejoin_jitter` × `failureRateScale`. */
66
- readonly windowMs: number;
67
- /** Per-window acceptance ceiling (`capPromote`); the inbound-rate bound's numerator. */
68
- readonly capPromote: number;
69
- }
70
-
71
- class StaggeredRejoinJitter implements RejoinJitter {
72
- readonly windowMs: number;
73
- readonly capPromote: number;
74
- private readonly random: () => number;
75
-
76
- constructor(config: RejoinJitterConfig = {}) {
77
- const base = config.tRejoinJitterMs ?? DEFAULT_T_REJOIN_JITTER_MS;
78
- if (!(base > 0)) {
79
- throw new RangeError(`tRejoinJitterMs must be > 0, got ${base}`);
80
- }
81
- const scale = config.failureRateScale ?? 1;
82
- if (!(scale >= 1)) {
83
- throw new RangeError(`failureRateScale must be >= 1, got ${scale}`);
84
- }
85
- this.capPromote = config.capPromote ?? DEFAULT_REJOIN_CAP_PROMOTE;
86
- if (!Number.isInteger(this.capPromote) || this.capPromote <= 0) {
87
- throw new RangeError(`capPromote must be a positive integer, got ${this.capPromote}`);
88
- }
89
- this.windowMs = base * scale;
90
- this.random = config.random ?? ((): number => Math.random());
91
- }
92
-
93
- scheduleRejoin(now: number): number {
94
- const offset = Math.floor(this.random() * this.windowMs);
95
- return now + offset;
96
- }
97
-
98
- scheduleWave(count: number, now: number): number[] {
99
- if (!Number.isInteger(count) || count < 0) {
100
- throw new RangeError(`count must be a non-negative integer, got ${count}`);
101
- }
102
- // Even spacing at windowMs/capPromote guarantees ≤ capPromote arrivals in any windowMs window;
103
- // for count > capPromote the wave necessarily spans more than one window, holding the rate.
104
- const interval = this.windowMs / this.capPromote;
105
- const out = new Array<number>(count);
106
- for (let i = 0; i < count; i++) {
107
- out[i] = now + Math.floor(i * interval);
108
- }
109
- log("scheduleWave count=%d window=%d cap=%d span=%d", count, this.windowMs, this.capPromote, count > 0 ? out[count - 1]! - now : 0);
110
- return out;
111
- }
112
- }
113
-
114
- /** Build a {@link RejoinJitter} over the configured window, rate ceiling, and (test-injectable) RNG. */
115
- export function createRejoinJitter(config: RejoinJitterConfig = {}): RejoinJitter {
116
- return new StaggeredRejoinJitter(config);
117
- }
1
+ /**
2
+ * Cohort-topic substrate — re-registration jitter (anti-flood claim 2).
3
+ *
4
+ * Transcribed from `docs/cohort-topic.md` §Anti-flood properties (claim 2: "Re-registration storm
5
+ * after cohort failure") and folded back from the simulator-validated
6
+ * `packages/substrate-simulator/src/walk.ts` (`rejoinStagger` / `rateLimitedStagger`). When a cohort
7
+ * fails, every participant it served re-registers at once; without staggering, the recovering or
8
+ * replacement cohort takes the whole wave in a single instant and is shoved straight past
9
+ * `cap_promote`. Spreading the wave over `T_rejoin_jitter` (default 30 s, widened with the observed
10
+ * FRET cohort-failure rate) bounds the recovering cohort's inbound rate to
11
+ * `cap_promote / T_rejoin_jitter`.
12
+ *
13
+ * Two staggering forms, both exposed here:
14
+ * - {@link RejoinJitter.scheduleRejoin} — a single participant draws a uniform offset over the
15
+ * jitter window. Decorrelates one participant's retry from its peers (the production API: a
16
+ * participant that loses its cohort calls this once). Random, so the bound holds *in expectation*.
17
+ * - {@link RejoinJitter.scheduleWave} — a whole wave of `count` rejoiners is placed at a fixed
18
+ * interval `windowMs / cap_promote`, so **any** `windowMs`-long window contains at most
19
+ * `cap_promote` arrivals *by construction*. This is the hard-bound form the e2e suite asserts
20
+ * against; it widens the span past one window when `count > cap_promote` so the rate ceiling
21
+ * `cap_promote / T_rejoin_jitter` is never exceeded.
22
+ *
23
+ * This module is FRET-free and side-effect-free apart from the injected RNG: the caller schedules a
24
+ * timer at the returned timestamp.
25
+ */
26
+
27
+ import { createLogger } from "../../logger.js";
28
+
29
+ const log = createLogger("cohort-topic:antiflood");
30
+
31
+ /** Default jitter window `T_rejoin_jitter` (ms) — `docs/cohort-topic.md` §Configuration. */
32
+ export const DEFAULT_T_REJOIN_JITTER_MS = 30_000;
33
+ /** Default `cap_promote` — sets the inbound-rate ceiling `cap_promote / T_rejoin_jitter`. */
34
+ export const DEFAULT_REJOIN_CAP_PROMOTE = 64;
35
+
36
+ export interface RejoinJitterConfig {
37
+ /** Base jitter window `T_rejoin_jitter` (ms). Default {@link DEFAULT_T_REJOIN_JITTER_MS}. */
38
+ tRejoinJitterMs?: number;
39
+ /** Acceptance ceiling per window; the rate bound is `capPromote / T_rejoin_jitter`. Default 64. */
40
+ capPromote?: number;
41
+ /**
42
+ * Multiplier applied to the base window to track the observed FRET cohort-failure rate (claim 2:
43
+ * "scaled with cohort failure rate observed from FRET"). A higher failure rate → a wider window so
44
+ * the larger expected re-registration wave still respects the rate ceiling. Default 1 (steady FRET).
45
+ */
46
+ failureRateScale?: number;
47
+ /** Uniform RNG in `[0, 1)`. Injected for determinism in tests; defaults to `Math.random`. */
48
+ random?: () => number;
49
+ }
50
+
51
+ /** Staggers post-cohort-failure re-registration so the recovering cohort isn't stormed. */
52
+ export interface RejoinJitter {
53
+ /**
54
+ * Jittered re-registration timestamp for **one** participant: `now + ⌊U[0, windowMs)⌋`. Random,
55
+ * so a wave of independent callers spreads roughly uniformly over the window.
56
+ */
57
+ scheduleRejoin(now: number): number;
58
+ /**
59
+ * Rate-bounded timestamps for a whole re-registration wave of `count` participants. Arrivals are
60
+ * evenly spaced at `windowMs / capPromote`, so any `windowMs`-long sliding window holds at most
61
+ * `capPromote` of them — the `cap_promote / T_rejoin_jitter` ceiling, exactly rather than in
62
+ * expectation. Returns one timestamp per participant, ascending.
63
+ */
64
+ scheduleWave(count: number, now: number): number[];
65
+ /** Effective jitter window (ms) = base `T_rejoin_jitter` × `failureRateScale`. */
66
+ readonly windowMs: number;
67
+ /** Per-window acceptance ceiling (`capPromote`); the inbound-rate bound's numerator. */
68
+ readonly capPromote: number;
69
+ }
70
+
71
+ class StaggeredRejoinJitter implements RejoinJitter {
72
+ readonly windowMs: number;
73
+ readonly capPromote: number;
74
+ private readonly random: () => number;
75
+
76
+ constructor(config: RejoinJitterConfig = {}) {
77
+ const base = config.tRejoinJitterMs ?? DEFAULT_T_REJOIN_JITTER_MS;
78
+ if (!(base > 0)) {
79
+ throw new RangeError(`tRejoinJitterMs must be > 0, got ${base}`);
80
+ }
81
+ const scale = config.failureRateScale ?? 1;
82
+ if (!(scale >= 1)) {
83
+ throw new RangeError(`failureRateScale must be >= 1, got ${scale}`);
84
+ }
85
+ this.capPromote = config.capPromote ?? DEFAULT_REJOIN_CAP_PROMOTE;
86
+ if (!Number.isInteger(this.capPromote) || this.capPromote <= 0) {
87
+ throw new RangeError(`capPromote must be a positive integer, got ${this.capPromote}`);
88
+ }
89
+ this.windowMs = base * scale;
90
+ this.random = config.random ?? ((): number => Math.random());
91
+ }
92
+
93
+ scheduleRejoin(now: number): number {
94
+ const offset = Math.floor(this.random() * this.windowMs);
95
+ return now + offset;
96
+ }
97
+
98
+ scheduleWave(count: number, now: number): number[] {
99
+ if (!Number.isInteger(count) || count < 0) {
100
+ throw new RangeError(`count must be a non-negative integer, got ${count}`);
101
+ }
102
+ // Even spacing at windowMs/capPromote guarantees ≤ capPromote arrivals in any windowMs window;
103
+ // for count > capPromote the wave necessarily spans more than one window, holding the rate.
104
+ const interval = this.windowMs / this.capPromote;
105
+ const out = new Array<number>(count);
106
+ for (let i = 0; i < count; i++) {
107
+ out[i] = now + Math.floor(i * interval);
108
+ }
109
+ log("scheduleWave count=%d window=%d cap=%d span=%d", count, this.windowMs, this.capPromote, count > 0 ? out[count - 1]! - now : 0);
110
+ return out;
111
+ }
112
+ }
113
+
114
+ /** Build a {@link RejoinJitter} over the configured window, rate ceiling, and (test-injectable) RNG. */
115
+ export function createRejoinJitter(config: RejoinJitterConfig = {}): RejoinJitter {
116
+ return new StaggeredRejoinJitter(config);
117
+ }