@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,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";