@optimystic/db-core 0.22.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.
- package/README.md +336 -336
- package/dist/src/cluster/structs.d.ts +39 -1
- package/dist/src/cluster/structs.d.ts.map +1 -1
- package/dist/src/cluster/structs.js +24 -0
- package/dist/src/cluster/structs.js.map +1 -1
- package/dist/src/collection/collection.d.ts +17 -0
- package/dist/src/collection/collection.d.ts.map +1 -1
- package/dist/src/collection/collection.js +24 -2
- package/dist/src/collection/collection.js.map +1 -1
- package/dist/src/collections/tree/tree.d.ts +5 -0
- package/dist/src/collections/tree/tree.d.ts.map +1 -1
- package/dist/src/collections/tree/tree.js +7 -0
- package/dist/src/collections/tree/tree.js.map +1 -1
- package/dist/src/network/i-peer-network.d.ts +16 -0
- package/dist/src/network/i-peer-network.d.ts.map +1 -1
- package/dist/src/network/struct.d.ts +39 -2
- package/dist/src/network/struct.d.ts.map +1 -1
- package/dist/src/network/struct.js +18 -0
- package/dist/src/network/struct.js.map +1 -1
- package/dist/src/testing/test-transactor.d.ts +95 -8
- package/dist/src/testing/test-transactor.d.ts.map +1 -1
- package/dist/src/testing/test-transactor.js +121 -8
- package/dist/src/testing/test-transactor.js.map +1 -1
- package/dist/src/transaction/transaction.d.ts +1 -1
- package/dist/src/transaction/transaction.js +1 -1
- package/dist/src/transactor/network-transactor.d.ts.map +1 -1
- package/dist/src/transactor/network-transactor.js +48 -13
- package/dist/src/transactor/network-transactor.js.map +1 -1
- package/dist/src/transactor/transactor-source.d.ts.map +1 -1
- package/dist/src/transactor/transactor-source.js +25 -2
- package/dist/src/transactor/transactor-source.js.map +1 -1
- package/package.json +1 -1
- package/src/cluster/membership.ts +85 -85
- package/src/cluster/structs.ts +43 -4
- package/src/cohort-topic/addressing.ts +120 -120
- package/src/cohort-topic/antidos/bootstrap-evidence-envelope.ts +253 -253
- package/src/cohort-topic/antidos/bootstrap-evidence.ts +106 -106
- package/src/cohort-topic/antidos/index.ts +5 -5
- package/src/cohort-topic/antidos/rate-limiter.ts +210 -210
- package/src/cohort-topic/antidos/replay-guard.ts +146 -146
- package/src/cohort-topic/antidos/topic-budget.ts +160 -160
- package/src/cohort-topic/antiflood/index.ts +2 -2
- package/src/cohort-topic/antiflood/invariants.ts +108 -108
- package/src/cohort-topic/antiflood/jitter.ts +117 -117
- package/src/cohort-topic/coldstart.ts +237 -237
- package/src/cohort-topic/dmax.ts +88 -88
- package/src/cohort-topic/gossip/bus.ts +254 -254
- package/src/cohort-topic/gossip/index.ts +3 -3
- package/src/cohort-topic/gossip/records.ts +45 -45
- package/src/cohort-topic/gossip/view.ts +91 -91
- package/src/cohort-topic/index.ts +20 -20
- package/src/cohort-topic/load/barometer.ts +134 -134
- package/src/cohort-topic/load/index.ts +1 -1
- package/src/cohort-topic/member-engine.ts +430 -430
- package/src/cohort-topic/membership/index.ts +3 -3
- package/src/cohort-topic/membership/publisher.ts +163 -163
- package/src/cohort-topic/membership/source.ts +41 -41
- package/src/cohort-topic/membership/verifier.ts +461 -461
- package/src/cohort-topic/ports.ts +157 -157
- package/src/cohort-topic/promotion.ts +405 -405
- package/src/cohort-topic/registration/bytes.ts +37 -37
- package/src/cohort-topic/registration/handoff.ts +154 -154
- package/src/cohort-topic/registration/index.ts +6 -6
- package/src/cohort-topic/registration/renewal.ts +495 -495
- package/src/cohort-topic/registration/sharding.ts +61 -61
- package/src/cohort-topic/registration/store.ts +81 -81
- package/src/cohort-topic/registration/types.ts +91 -91
- package/src/cohort-topic/ring-hash.ts +50 -50
- package/src/cohort-topic/service.ts +416 -416
- package/src/cohort-topic/sig/index.ts +2 -2
- package/src/cohort-topic/sig/payloads.ts +59 -59
- package/src/cohort-topic/sig/threshold.ts +64 -64
- package/src/cohort-topic/tiers.ts +74 -74
- package/src/cohort-topic/traffic.ts +233 -233
- package/src/cohort-topic/walk.ts +326 -326
- package/src/cohort-topic/willingness.ts +237 -237
- package/src/cohort-topic/wire/codec.ts +216 -216
- package/src/cohort-topic/wire/index.ts +18 -18
- package/src/cohort-topic/wire/payloads.ts +126 -126
- package/src/cohort-topic/wire/primitives.ts +188 -188
- package/src/cohort-topic/wire/types.ts +475 -475
- package/src/cohort-topic/wire/validate.ts +512 -512
- package/src/collection/collection-type-registry.ts +37 -37
- package/src/collection/collection.ts +25 -2
- package/src/collections/diary/diary.ts +68 -68
- package/src/collections/tree/readme.md +4 -0
- package/src/collections/tree/tree.ts +320 -312
- package/src/matchmaking/capability-filter.ts +45 -45
- package/src/matchmaking/config.ts +98 -98
- package/src/matchmaking/index.ts +21 -21
- package/src/matchmaking/multi-cohort-seeker.ts +234 -234
- package/src/matchmaking/provider.ts +123 -123
- package/src/matchmaking/query-eval.ts +105 -105
- package/src/matchmaking/seeker-walk.ts +127 -127
- package/src/matchmaking/seeker.ts +86 -86
- package/src/matchmaking/topic-anchor.ts +90 -90
- package/src/matchmaking/voting-quorum.ts +394 -394
- package/src/matchmaking/wire.ts +603 -603
- package/src/network/i-peer-network.ts +17 -0
- package/src/network/stale-failure.ts +43 -43
- package/src/network/struct.ts +41 -2
- package/src/network/types.ts +37 -37
- package/src/reactivity/backfill.ts +220 -220
- package/src/reactivity/backpressure.ts +191 -191
- package/src/reactivity/checkpoint.ts +308 -308
- package/src/reactivity/config.ts +172 -172
- package/src/reactivity/dedupe.ts +132 -132
- package/src/reactivity/forwarder.ts +87 -87
- package/src/reactivity/index.ts +34 -34
- package/src/reactivity/notification.ts +123 -123
- package/src/reactivity/policy.ts +79 -79
- package/src/reactivity/push-state.ts +310 -310
- package/src/reactivity/recover.ts +153 -153
- package/src/reactivity/replay-buffer.ts +141 -141
- package/src/reactivity/resume.ts +549 -549
- package/src/reactivity/rotation.ts +415 -415
- package/src/reactivity/subscriber.ts +132 -132
- package/src/reactivity/subscription.ts +66 -66
- package/src/reactivity/topic-anchor.ts +71 -71
- package/src/reactivity/verify.ts +73 -73
- package/src/reactivity/wire-validate.ts +13 -13
- package/src/reactivity/wire.ts +224 -224
- package/src/testing/async-wait.ts +65 -65
- package/src/testing/index.ts +2 -2
- package/src/testing/test-transactor.ts +638 -502
- package/src/transaction/errors.ts +91 -91
- package/src/transaction/operations-hash.ts +196 -196
- package/src/transaction/read-dependency-collector.ts +78 -78
- package/src/transaction/transaction.ts +1 -1
- package/src/transactor/change-notifier.ts +80 -80
- package/src/transactor/index.ts +5 -5
- package/src/transactor/network-transactor.ts +49 -14
- package/src/transactor/transactor-source.ts +25 -2
- package/src/transform/atomic-proxy.ts +92 -92
- package/src/transform/helpers.ts +159 -159
- package/src/utility/backoff.ts +95 -95
- package/src/utility/batch-coordinator.ts +191 -191
- package/dist/src/transaction/context.d.ts +0 -60
- package/dist/src/transaction/context.d.ts.map +0 -1
- package/dist/src/transaction/context.js +0 -91
- 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";
|