@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,91 +1,91 @@
1
- /**
2
- * Cohort-topic substrate — the merged per-member gossip view.
3
- *
4
- * Each cohort member periodically gossips its willingness vector, load barometer buckets, and exact
5
- * per-topic summaries (`CohortGossipV1`). The bus folds the latest such contribution per member into
6
- * this view (last-writer-wins by gossip `timestamp`), which the willingness, barometer, and traffic
7
- * tickets read. Records replicate into the registration store separately (see {@link createCohortGossipBus}).
8
- */
9
-
10
- import type { CohortTopicSummary } from "../wire/types.js";
11
-
12
- /** Latest gossip contribution from a single cohort member. */
13
- export interface MemberContribution {
14
- /** Cohort epoch this member gossiped under (drift signal). */
15
- readonly cohortEpoch: Uint8Array;
16
- /** Willingness vector, 4 bits T0..T3 (0..15) decoded from the hex nibble. */
17
- readonly willingness: number;
18
- /** Load barometer buckets, 4 entries 0..7 per tier. */
19
- readonly loadBuckets: readonly number[];
20
- /** Observation window for the rate fields in `topicSummaries`. */
21
- readonly windowSeconds: number;
22
- /** Exact per-topic summaries this member last reported. */
23
- readonly topicSummaries: readonly CohortTopicSummary[];
24
- /**
25
- * `topicId` (base64url) → summary, derived from {@link topicSummaries} at {@link MutableCohortView.merge}
26
- * so a per-topic reader (traffic `snapshot`) does an O(1) lookup instead of an O(summaries) `.find` per
27
- * member per reply. First occurrence wins on a duplicate `topicId` (matching `Array.prototype.find`).
28
- * Populated by `merge`; a contribution built without going through `merge` may omit it, in which case
29
- * consumers fall back to scanning {@link topicSummaries}.
30
- */
31
- readonly topicIndex?: ReadonlyMap<string, CohortTopicSummary>;
32
- /** Gossip timestamp (unix ms) — the last-writer-wins key. */
33
- readonly timestamp: number;
34
- }
35
-
36
- /** Read view over the merged per-member contributions, keyed by `fromMember`. */
37
- export interface CohortView {
38
- /** Latest contribution from `member` (the `fromMember` string), or `undefined`. */
39
- get(member: string): MemberContribution | undefined;
40
- /** All current contributions, keyed by `fromMember`. */
41
- all(): ReadonlyMap<string, MemberContribution>;
42
- }
43
-
44
- /** Mutable view the bus writes into; exposes the read {@link CohortView} surface. */
45
- export interface MutableCohortView extends CohortView {
46
- /** Merge `c` for `member` iff it is at least as recent as the held contribution. Returns true if applied. */
47
- merge(member: string, c: MemberContribution): boolean;
48
- }
49
-
50
- class MapCohortView implements MutableCohortView {
51
- private readonly byMember = new Map<string, MemberContribution>();
52
-
53
- get(member: string): MemberContribution | undefined {
54
- return this.byMember.get(member);
55
- }
56
-
57
- all(): ReadonlyMap<string, MemberContribution> {
58
- return this.byMember;
59
- }
60
-
61
- merge(member: string, c: MemberContribution): boolean {
62
- const held = this.byMember.get(member);
63
- if (held !== undefined && c.timestamp < held.timestamp) {
64
- return false; // stale; keep the newer contribution
65
- }
66
- // Derive the per-topic index once, at write time, so per-topic readers never rescan the flat
67
- // `topicSummaries` array per reply. A writer pays O(summaries) once per merge (gossip round).
68
- this.byMember.set(member, { ...c, topicIndex: indexTopics(c.topicSummaries) });
69
- return true;
70
- }
71
- }
72
-
73
- /**
74
- * Index a member's flat topic summaries by `topicId` (base64url). First occurrence wins on a duplicate
75
- * `topicId`, exactly matching the `Array.prototype.find` scan this replaces — so a snapshot over the index
76
- * returns identical numbers to one over the array.
77
- */
78
- function indexTopics(summaries: readonly CohortTopicSummary[]): ReadonlyMap<string, CohortTopicSummary> {
79
- const index = new Map<string, CohortTopicSummary>();
80
- for (const s of summaries) {
81
- if (!index.has(s.topicId)) {
82
- index.set(s.topicId, s);
83
- }
84
- }
85
- return index;
86
- }
87
-
88
- /** Construct an empty {@link MutableCohortView}. */
89
- export function createCohortView(): MutableCohortView {
90
- return new MapCohortView();
91
- }
1
+ /**
2
+ * Cohort-topic substrate — the merged per-member gossip view.
3
+ *
4
+ * Each cohort member periodically gossips its willingness vector, load barometer buckets, and exact
5
+ * per-topic summaries (`CohortGossipV1`). The bus folds the latest such contribution per member into
6
+ * this view (last-writer-wins by gossip `timestamp`), which the willingness, barometer, and traffic
7
+ * tickets read. Records replicate into the registration store separately (see {@link createCohortGossipBus}).
8
+ */
9
+
10
+ import type { CohortTopicSummary } from "../wire/types.js";
11
+
12
+ /** Latest gossip contribution from a single cohort member. */
13
+ export interface MemberContribution {
14
+ /** Cohort epoch this member gossiped under (drift signal). */
15
+ readonly cohortEpoch: Uint8Array;
16
+ /** Willingness vector, 4 bits T0..T3 (0..15) decoded from the hex nibble. */
17
+ readonly willingness: number;
18
+ /** Load barometer buckets, 4 entries 0..7 per tier. */
19
+ readonly loadBuckets: readonly number[];
20
+ /** Observation window for the rate fields in `topicSummaries`. */
21
+ readonly windowSeconds: number;
22
+ /** Exact per-topic summaries this member last reported. */
23
+ readonly topicSummaries: readonly CohortTopicSummary[];
24
+ /**
25
+ * `topicId` (base64url) → summary, derived from {@link topicSummaries} at {@link MutableCohortView.merge}
26
+ * so a per-topic reader (traffic `snapshot`) does an O(1) lookup instead of an O(summaries) `.find` per
27
+ * member per reply. First occurrence wins on a duplicate `topicId` (matching `Array.prototype.find`).
28
+ * Populated by `merge`; a contribution built without going through `merge` may omit it, in which case
29
+ * consumers fall back to scanning {@link topicSummaries}.
30
+ */
31
+ readonly topicIndex?: ReadonlyMap<string, CohortTopicSummary>;
32
+ /** Gossip timestamp (unix ms) — the last-writer-wins key. */
33
+ readonly timestamp: number;
34
+ }
35
+
36
+ /** Read view over the merged per-member contributions, keyed by `fromMember`. */
37
+ export interface CohortView {
38
+ /** Latest contribution from `member` (the `fromMember` string), or `undefined`. */
39
+ get(member: string): MemberContribution | undefined;
40
+ /** All current contributions, keyed by `fromMember`. */
41
+ all(): ReadonlyMap<string, MemberContribution>;
42
+ }
43
+
44
+ /** Mutable view the bus writes into; exposes the read {@link CohortView} surface. */
45
+ export interface MutableCohortView extends CohortView {
46
+ /** Merge `c` for `member` iff it is at least as recent as the held contribution. Returns true if applied. */
47
+ merge(member: string, c: MemberContribution): boolean;
48
+ }
49
+
50
+ class MapCohortView implements MutableCohortView {
51
+ private readonly byMember = new Map<string, MemberContribution>();
52
+
53
+ get(member: string): MemberContribution | undefined {
54
+ return this.byMember.get(member);
55
+ }
56
+
57
+ all(): ReadonlyMap<string, MemberContribution> {
58
+ return this.byMember;
59
+ }
60
+
61
+ merge(member: string, c: MemberContribution): boolean {
62
+ const held = this.byMember.get(member);
63
+ if (held !== undefined && c.timestamp < held.timestamp) {
64
+ return false; // stale; keep the newer contribution
65
+ }
66
+ // Derive the per-topic index once, at write time, so per-topic readers never rescan the flat
67
+ // `topicSummaries` array per reply. A writer pays O(summaries) once per merge (gossip round).
68
+ this.byMember.set(member, { ...c, topicIndex: indexTopics(c.topicSummaries) });
69
+ return true;
70
+ }
71
+ }
72
+
73
+ /**
74
+ * Index a member's flat topic summaries by `topicId` (base64url). First occurrence wins on a duplicate
75
+ * `topicId`, exactly matching the `Array.prototype.find` scan this replaces — so a snapshot over the index
76
+ * returns identical numbers to one over the array.
77
+ */
78
+ function indexTopics(summaries: readonly CohortTopicSummary[]): ReadonlyMap<string, CohortTopicSummary> {
79
+ const index = new Map<string, CohortTopicSummary>();
80
+ for (const s of summaries) {
81
+ if (!index.has(s.topicId)) {
82
+ index.set(s.topicId, s);
83
+ }
84
+ }
85
+ return index;
86
+ }
87
+
88
+ /** Construct an empty {@link MutableCohortView}. */
89
+ export function createCohortView(): MutableCohortView {
90
+ return new MapCohortView();
91
+ }
@@ -1,20 +1,20 @@
1
- export * from "./ports.js";
2
- export * from "./ring-hash.js";
3
- export * from "./addressing.js";
4
- export * from "./tiers.js";
5
- export * from "./dmax.js";
6
- export * from "./wire/index.js";
7
- export * from "./registration/index.js";
8
- export * from "./gossip/index.js";
9
- export * from "./sig/index.js";
10
- export * from "./membership/index.js";
11
- export * from "./load/index.js";
12
- export * from "./willingness.js";
13
- export * from "./traffic.js";
14
- export * from "./walk.js";
15
- export * from "./promotion.js";
16
- export * from "./coldstart.js";
17
- export * from "./antiflood/index.js";
18
- export * from "./antidos/index.js";
19
- export * from "./member-engine.js";
20
- export * from "./service.js";
1
+ export * from "./ports.js";
2
+ export * from "./ring-hash.js";
3
+ export * from "./addressing.js";
4
+ export * from "./tiers.js";
5
+ export * from "./dmax.js";
6
+ export * from "./wire/index.js";
7
+ export * from "./registration/index.js";
8
+ export * from "./gossip/index.js";
9
+ export * from "./sig/index.js";
10
+ export * from "./membership/index.js";
11
+ export * from "./load/index.js";
12
+ export * from "./willingness.js";
13
+ export * from "./traffic.js";
14
+ export * from "./walk.js";
15
+ export * from "./promotion.js";
16
+ export * from "./coldstart.js";
17
+ export * from "./antiflood/index.js";
18
+ export * from "./antidos/index.js";
19
+ export * from "./member-engine.js";
20
+ export * from "./service.js";
@@ -1,134 +1,134 @@
1
- /**
2
- * Cohort-topic substrate — capacity barometer.
3
- *
4
- * Transcribed from `docs/cohort-topic.md` §Capacity barometer (and folded back from the
5
- * simulator-validated `packages/substrate-simulator/src/willingness.ts`). A cohort member tracks a
6
- * coarse **3-bit (0..7) log-bucketed utilization** per tier (T0..T3). The barometer feeds two
7
- * decisions:
8
- *
9
- * 1. **Willingness-bit refresh.** When a tier's bucket reaches `overloadBucket` the member sheds
10
- * that tier — its load-driven willingness bit flips off until utilization recedes. Siblings see
11
- * the flip within one gossip round (the bit rides `CohortGossipV1.willingnessBits`).
12
- * 2. **Early-promote signal.** `bucket ≥ overloadBucket` (default 6, the `bucket_overload` of
13
- * §Configuration) is the "cohort is hot at this tier" signal the promotion ticket consumes to
14
- * fire promotion earlier than the strict `cap_promote`. This module only *exposes* the signal;
15
- * it does not itself promote.
16
- *
17
- * The barometer is **not** aggregated across the tree — children promote independently, so a member
18
- * only ever observes its own per-tier utilization here. The 3-bit buckets (× 4 tiers) plus the
19
- * willingness bit fit in 16 bits of cohort gossip; the cost is negligible.
20
- */
21
-
22
- import { ALL_TIERS, type Tier } from "../tiers.js";
23
-
24
- /** A single tier's coarse load reading, as carried in cohort gossip. */
25
- export interface LoadBarometer {
26
- readonly tier: Tier;
27
- /** 0..7, log-bucketed utilization (see {@link utilizationBucket}). */
28
- readonly bucket: number;
29
- }
30
-
31
- /**
32
- * Load-barometer bucket at/above which a member sheds a tier (willingness flips off) and the cohort
33
- * is considered hot enough to promote early. `bucket_overload` in `docs/cohort-topic.md`
34
- * §Configuration; simulator-confirmed at 6.
35
- */
36
- export const DEFAULT_OVERLOAD_BUCKET = 6;
37
-
38
- /**
39
- * Map a utilization ratio (`load / capacity`, where `1.0` = at capacity) to a 0..7 log bucket.
40
- *
41
- * Each bucket spans a doubling of utilization, anchored so the top of the range is at-capacity:
42
- *
43
- * | bucket | utilization range |
44
- * |--------|-------------------|
45
- * | 7 | `u ≥ 1.0` |
46
- * | 6 | `[0.5, 1.0)` |
47
- * | 5 | `[0.25, 0.5)` |
48
- * | … | … |
49
- * | 1 | `[1/64, 1/32)` |
50
- * | 0 | `u < 1/64` (incl. 0, negative clamped) |
51
- *
52
- * Monotonic non-decreasing in `u`; `bucket = clamp(7 + ⌊log₂ u⌋, 0, 7)`. With the default
53
- * `overloadBucket = 6`, a member sheds a tier once utilization reaches half capacity.
54
- */
55
- export function utilizationBucket(utilization: number): number {
56
- if (!(utilization > 0)) {
57
- return 0; // zero, negative, or NaN → idle
58
- }
59
- const b = 7 + Math.floor(Math.log2(utilization));
60
- if (b < 0) return 0;
61
- if (b > 7) return 7;
62
- return b;
63
- }
64
-
65
- /** Mutable per-tier capacity barometer for one cohort member. */
66
- export interface LoadBarometerState {
67
- /** Record this member's current utilization (`load / capacity`) for `tier`. */
68
- observe(tier: Tier, utilization: number): void;
69
- /** Current 0..7 bucket for `tier`. */
70
- bucket(tier: Tier): number;
71
- /** `{ tier, bucket }` reading for `tier`. */
72
- reading(tier: Tier): LoadBarometer;
73
- /** All four buckets (T0..T3), the array gossiped in `CohortGossipV1.loadBuckets`. */
74
- loadBuckets(): number[];
75
- /**
76
- * Early-promote / shed signal: `bucket(tier) ≥ overloadBucket`. True means the member is hot at
77
- * `tier` — its load-driven willingness bit is off and the promotion layer may promote early.
78
- */
79
- isOverloaded(tier: Tier): boolean;
80
- /** Load-driven willingness bit for `tier`: `!isOverloaded(tier)`. (Profile/budget gates live in the willingness check.) */
81
- loadWilling(tier: Tier): boolean;
82
- }
83
-
84
- export interface LoadBarometerConfig {
85
- /** Bucket at/above which a tier is shed and flagged hot. Default {@link DEFAULT_OVERLOAD_BUCKET}. */
86
- overloadBucket?: number;
87
- }
88
-
89
- class ArrayLoadBarometer implements LoadBarometerState {
90
- private readonly buckets: number[] = [0, 0, 0, 0];
91
- private readonly overloadBucket: number;
92
-
93
- constructor(config?: LoadBarometerConfig) {
94
- const ob = config?.overloadBucket ?? DEFAULT_OVERLOAD_BUCKET;
95
- if (!Number.isInteger(ob) || ob < 1 || ob > 7) {
96
- throw new RangeError(`overloadBucket must be an integer in [1, 7], got ${ob}`);
97
- }
98
- this.overloadBucket = ob;
99
- }
100
-
101
- observe(tier: Tier, utilization: number): void {
102
- this.buckets[tier] = utilizationBucket(utilization);
103
- }
104
-
105
- bucket(tier: Tier): number {
106
- return this.buckets[tier]!;
107
- }
108
-
109
- reading(tier: Tier): LoadBarometer {
110
- return { tier, bucket: this.buckets[tier]! };
111
- }
112
-
113
- loadBuckets(): number[] {
114
- return [...this.buckets];
115
- }
116
-
117
- isOverloaded(tier: Tier): boolean {
118
- return this.buckets[tier]! >= this.overloadBucket;
119
- }
120
-
121
- loadWilling(tier: Tier): boolean {
122
- return !this.isOverloaded(tier);
123
- }
124
- }
125
-
126
- /** Construct an idle (all-bucket-0) {@link LoadBarometerState}. */
127
- export function createLoadBarometer(config?: LoadBarometerConfig): LoadBarometerState {
128
- return new ArrayLoadBarometer(config);
129
- }
130
-
131
- /** Iterate the four tiers — convenience for callers folding the barometer into a gossip frame. */
132
- export function eachTier(): readonly Tier[] {
133
- return ALL_TIERS;
134
- }
1
+ /**
2
+ * Cohort-topic substrate — capacity barometer.
3
+ *
4
+ * Transcribed from `docs/cohort-topic.md` §Capacity barometer (and folded back from the
5
+ * simulator-validated `packages/substrate-simulator/src/willingness.ts`). A cohort member tracks a
6
+ * coarse **3-bit (0..7) log-bucketed utilization** per tier (T0..T3). The barometer feeds two
7
+ * decisions:
8
+ *
9
+ * 1. **Willingness-bit refresh.** When a tier's bucket reaches `overloadBucket` the member sheds
10
+ * that tier — its load-driven willingness bit flips off until utilization recedes. Siblings see
11
+ * the flip within one gossip round (the bit rides `CohortGossipV1.willingnessBits`).
12
+ * 2. **Early-promote signal.** `bucket ≥ overloadBucket` (default 6, the `bucket_overload` of
13
+ * §Configuration) is the "cohort is hot at this tier" signal the promotion ticket consumes to
14
+ * fire promotion earlier than the strict `cap_promote`. This module only *exposes* the signal;
15
+ * it does not itself promote.
16
+ *
17
+ * The barometer is **not** aggregated across the tree — children promote independently, so a member
18
+ * only ever observes its own per-tier utilization here. The 3-bit buckets (× 4 tiers) plus the
19
+ * willingness bit fit in 16 bits of cohort gossip; the cost is negligible.
20
+ */
21
+
22
+ import { ALL_TIERS, type Tier } from "../tiers.js";
23
+
24
+ /** A single tier's coarse load reading, as carried in cohort gossip. */
25
+ export interface LoadBarometer {
26
+ readonly tier: Tier;
27
+ /** 0..7, log-bucketed utilization (see {@link utilizationBucket}). */
28
+ readonly bucket: number;
29
+ }
30
+
31
+ /**
32
+ * Load-barometer bucket at/above which a member sheds a tier (willingness flips off) and the cohort
33
+ * is considered hot enough to promote early. `bucket_overload` in `docs/cohort-topic.md`
34
+ * §Configuration; simulator-confirmed at 6.
35
+ */
36
+ export const DEFAULT_OVERLOAD_BUCKET = 6;
37
+
38
+ /**
39
+ * Map a utilization ratio (`load / capacity`, where `1.0` = at capacity) to a 0..7 log bucket.
40
+ *
41
+ * Each bucket spans a doubling of utilization, anchored so the top of the range is at-capacity:
42
+ *
43
+ * | bucket | utilization range |
44
+ * |--------|-------------------|
45
+ * | 7 | `u ≥ 1.0` |
46
+ * | 6 | `[0.5, 1.0)` |
47
+ * | 5 | `[0.25, 0.5)` |
48
+ * | … | … |
49
+ * | 1 | `[1/64, 1/32)` |
50
+ * | 0 | `u < 1/64` (incl. 0, negative clamped) |
51
+ *
52
+ * Monotonic non-decreasing in `u`; `bucket = clamp(7 + ⌊log₂ u⌋, 0, 7)`. With the default
53
+ * `overloadBucket = 6`, a member sheds a tier once utilization reaches half capacity.
54
+ */
55
+ export function utilizationBucket(utilization: number): number {
56
+ if (!(utilization > 0)) {
57
+ return 0; // zero, negative, or NaN → idle
58
+ }
59
+ const b = 7 + Math.floor(Math.log2(utilization));
60
+ if (b < 0) return 0;
61
+ if (b > 7) return 7;
62
+ return b;
63
+ }
64
+
65
+ /** Mutable per-tier capacity barometer for one cohort member. */
66
+ export interface LoadBarometerState {
67
+ /** Record this member's current utilization (`load / capacity`) for `tier`. */
68
+ observe(tier: Tier, utilization: number): void;
69
+ /** Current 0..7 bucket for `tier`. */
70
+ bucket(tier: Tier): number;
71
+ /** `{ tier, bucket }` reading for `tier`. */
72
+ reading(tier: Tier): LoadBarometer;
73
+ /** All four buckets (T0..T3), the array gossiped in `CohortGossipV1.loadBuckets`. */
74
+ loadBuckets(): number[];
75
+ /**
76
+ * Early-promote / shed signal: `bucket(tier) ≥ overloadBucket`. True means the member is hot at
77
+ * `tier` — its load-driven willingness bit is off and the promotion layer may promote early.
78
+ */
79
+ isOverloaded(tier: Tier): boolean;
80
+ /** Load-driven willingness bit for `tier`: `!isOverloaded(tier)`. (Profile/budget gates live in the willingness check.) */
81
+ loadWilling(tier: Tier): boolean;
82
+ }
83
+
84
+ export interface LoadBarometerConfig {
85
+ /** Bucket at/above which a tier is shed and flagged hot. Default {@link DEFAULT_OVERLOAD_BUCKET}. */
86
+ overloadBucket?: number;
87
+ }
88
+
89
+ class ArrayLoadBarometer implements LoadBarometerState {
90
+ private readonly buckets: number[] = [0, 0, 0, 0];
91
+ private readonly overloadBucket: number;
92
+
93
+ constructor(config?: LoadBarometerConfig) {
94
+ const ob = config?.overloadBucket ?? DEFAULT_OVERLOAD_BUCKET;
95
+ if (!Number.isInteger(ob) || ob < 1 || ob > 7) {
96
+ throw new RangeError(`overloadBucket must be an integer in [1, 7], got ${ob}`);
97
+ }
98
+ this.overloadBucket = ob;
99
+ }
100
+
101
+ observe(tier: Tier, utilization: number): void {
102
+ this.buckets[tier] = utilizationBucket(utilization);
103
+ }
104
+
105
+ bucket(tier: Tier): number {
106
+ return this.buckets[tier]!;
107
+ }
108
+
109
+ reading(tier: Tier): LoadBarometer {
110
+ return { tier, bucket: this.buckets[tier]! };
111
+ }
112
+
113
+ loadBuckets(): number[] {
114
+ return [...this.buckets];
115
+ }
116
+
117
+ isOverloaded(tier: Tier): boolean {
118
+ return this.buckets[tier]! >= this.overloadBucket;
119
+ }
120
+
121
+ loadWilling(tier: Tier): boolean {
122
+ return !this.isOverloaded(tier);
123
+ }
124
+ }
125
+
126
+ /** Construct an idle (all-bucket-0) {@link LoadBarometerState}. */
127
+ export function createLoadBarometer(config?: LoadBarometerConfig): LoadBarometerState {
128
+ return new ArrayLoadBarometer(config);
129
+ }
130
+
131
+ /** Iterate the four tiers — convenience for callers folding the barometer into a gossip frame. */
132
+ export function eachTier(): readonly Tier[] {
133
+ return ALL_TIERS;
134
+ }
@@ -1 +1 @@
1
- export * from "./barometer.js";
1
+ export * from "./barometer.js";