@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.
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,61 +1,61 @@
1
- /**
2
- * Cohort-topic substrate — deterministic primary/backup sharding.
3
- *
4
- * Transcribed from `docs/cohort-topic.md` §Primary and backup sharding:
5
- *
6
- * ```
7
- * order(cohortMembers) = sort(cohortMembers, by PeerId ascending)
8
- * slot(participantId, cohortEpoch) = H(participantId ‖ cohortEpoch) mod k
9
- * primary = order[slot]
10
- * backups = order[slot+1 .. slot+2 (mod k)]
11
- * ```
12
- *
13
- * Deterministic given `(participantId, cohortEpoch, members)`; shards delivery load roughly evenly
14
- * across the `~k` members. `H` is the injected {@link IRingHash} (db-core's own SHA-256 — **not** a
15
- * FRET import), reused so coords stay byte-compatible with the rest of the substrate.
16
- */
17
-
18
- import type { IRingHash } from "../ports.js";
19
- import { compareBytes, concatBytes } from "./bytes.js";
20
-
21
- /** Deterministic slot assignment over a fixed cohort snapshot. */
22
- export interface SlotAssigner {
23
- /**
24
- * `(primary, backups)` for `participantId` under `cohortEpoch` and `cohortMembers`.
25
- * Backups are the 1..2 members following the primary in ascending order, wrapping mod `k`.
26
- */
27
- assignSlots(participantId: Uint8Array, cohortEpoch: Uint8Array, cohortMembers: readonly Uint8Array[]): { primary: Uint8Array; backups: Uint8Array[] };
28
- }
29
-
30
- /** Number of warm-failover backups (capped by available members). */
31
- const MAX_BACKUPS = 2;
32
-
33
- /** `coord mod k` over the full digest, MSB-first, without bigint allocation. */
34
- function modK(coord: Uint8Array, k: number): number {
35
- let acc = 0;
36
- for (let i = 0; i < coord.length; i++) {
37
- acc = (acc * 256 + coord[i]!) % k;
38
- }
39
- return acc;
40
- }
41
-
42
- /** Build a {@link SlotAssigner} bound to a hash. */
43
- export function createSlotAssigner(hash: IRingHash): SlotAssigner {
44
- return {
45
- assignSlots(participantId: Uint8Array, cohortEpoch: Uint8Array, cohortMembers: readonly Uint8Array[]): { primary: Uint8Array; backups: Uint8Array[] } {
46
- const k = cohortMembers.length;
47
- if (k === 0) {
48
- throw new RangeError("assignSlots requires a non-empty cohort");
49
- }
50
- const order = [...cohortMembers].sort(compareBytes);
51
- const slot = modK(hash.H(concatBytes(participantId, cohortEpoch)), k);
52
- const primary = order[slot]!;
53
- const backups: Uint8Array[] = [];
54
- const nBackups = Math.min(MAX_BACKUPS, k - 1);
55
- for (let i = 1; i <= nBackups; i++) {
56
- backups.push(order[(slot + i) % k]!);
57
- }
58
- return { primary, backups };
59
- },
60
- };
61
- }
1
+ /**
2
+ * Cohort-topic substrate — deterministic primary/backup sharding.
3
+ *
4
+ * Transcribed from `docs/cohort-topic.md` §Primary and backup sharding:
5
+ *
6
+ * ```
7
+ * order(cohortMembers) = sort(cohortMembers, by PeerId ascending)
8
+ * slot(participantId, cohortEpoch) = H(participantId ‖ cohortEpoch) mod k
9
+ * primary = order[slot]
10
+ * backups = order[slot+1 .. slot+2 (mod k)]
11
+ * ```
12
+ *
13
+ * Deterministic given `(participantId, cohortEpoch, members)`; shards delivery load roughly evenly
14
+ * across the `~k` members. `H` is the injected {@link IRingHash} (db-core's own SHA-256 — **not** a
15
+ * FRET import), reused so coords stay byte-compatible with the rest of the substrate.
16
+ */
17
+
18
+ import type { IRingHash } from "../ports.js";
19
+ import { compareBytes, concatBytes } from "./bytes.js";
20
+
21
+ /** Deterministic slot assignment over a fixed cohort snapshot. */
22
+ export interface SlotAssigner {
23
+ /**
24
+ * `(primary, backups)` for `participantId` under `cohortEpoch` and `cohortMembers`.
25
+ * Backups are the 1..2 members following the primary in ascending order, wrapping mod `k`.
26
+ */
27
+ assignSlots(participantId: Uint8Array, cohortEpoch: Uint8Array, cohortMembers: readonly Uint8Array[]): { primary: Uint8Array; backups: Uint8Array[] };
28
+ }
29
+
30
+ /** Number of warm-failover backups (capped by available members). */
31
+ const MAX_BACKUPS = 2;
32
+
33
+ /** `coord mod k` over the full digest, MSB-first, without bigint allocation. */
34
+ function modK(coord: Uint8Array, k: number): number {
35
+ let acc = 0;
36
+ for (let i = 0; i < coord.length; i++) {
37
+ acc = (acc * 256 + coord[i]!) % k;
38
+ }
39
+ return acc;
40
+ }
41
+
42
+ /** Build a {@link SlotAssigner} bound to a hash. */
43
+ export function createSlotAssigner(hash: IRingHash): SlotAssigner {
44
+ return {
45
+ assignSlots(participantId: Uint8Array, cohortEpoch: Uint8Array, cohortMembers: readonly Uint8Array[]): { primary: Uint8Array; backups: Uint8Array[] } {
46
+ const k = cohortMembers.length;
47
+ if (k === 0) {
48
+ throw new RangeError("assignSlots requires a non-empty cohort");
49
+ }
50
+ const order = [...cohortMembers].sort(compareBytes);
51
+ const slot = modK(hash.H(concatBytes(participantId, cohortEpoch)), k);
52
+ const primary = order[slot]!;
53
+ const backups: Uint8Array[] = [];
54
+ const nBackups = Math.min(MAX_BACKUPS, k - 1);
55
+ for (let i = 1; i <= nBackups; i++) {
56
+ backups.push(order[(slot + i) % k]!);
57
+ }
58
+ return { primary, backups };
59
+ },
60
+ };
61
+ }
@@ -1,81 +1,81 @@
1
- /**
2
- * Cohort-topic substrate — in-memory registration store.
3
- *
4
- * Per `docs/cohort-topic.md` §Registration mechanics. Records are doubly indexed: an outer map
5
- * keyed by topic, each holding an inner map keyed by participant. This gives O(1)
6
- * {@link RegistrationStore.getByParticipant} / {@link RegistrationStore.delete} and O(participants)
7
- * {@link RegistrationStore.listByTopic} without a secondary index to keep in sync. The store is
8
- * local soft state; cross-member replication runs over cohort gossip in a later ticket.
9
- */
10
-
11
- import { bytesKey } from "./bytes.js";
12
- import type { RegistrationRecord, RegistrationStore } from "./types.js";
13
-
14
- class InMemoryRegistrationStore implements RegistrationStore {
15
- /** topicKey → (participantKey → record). Inner maps are pruned when they empty. */
16
- private readonly byTopic = new Map<string, Map<string, RegistrationRecord>>();
17
-
18
- put(rec: RegistrationRecord): void {
19
- const tk = bytesKey(rec.topicId);
20
- let inner = this.byTopic.get(tk);
21
- if (inner === undefined) {
22
- inner = new Map<string, RegistrationRecord>();
23
- this.byTopic.set(tk, inner);
24
- }
25
- inner.set(bytesKey(rec.participantId), rec);
26
- }
27
-
28
- getByParticipant(topicId: Uint8Array, participantId: Uint8Array): RegistrationRecord | undefined {
29
- return this.byTopic.get(bytesKey(topicId))?.get(bytesKey(participantId));
30
- }
31
-
32
- listByTopic(topicId: Uint8Array): readonly RegistrationRecord[] {
33
- const inner = this.byTopic.get(bytesKey(topicId));
34
- return inner === undefined ? [] : [...inner.values()];
35
- }
36
-
37
- listAll(): readonly RegistrationRecord[] {
38
- const out: RegistrationRecord[] = [];
39
- for (const inner of this.byTopic.values()) {
40
- for (const rec of inner.values()) {
41
- out.push(rec);
42
- }
43
- }
44
- return out;
45
- }
46
-
47
- delete(topicId: Uint8Array, participantId: Uint8Array): void {
48
- const tk = bytesKey(topicId);
49
- const inner = this.byTopic.get(tk);
50
- if (inner === undefined) return;
51
- inner.delete(bytesKey(participantId));
52
- if (inner.size === 0) {
53
- this.byTopic.delete(tk);
54
- }
55
- }
56
-
57
- directParticipants(topicId: Uint8Array): number {
58
- return this.byTopic.get(bytesKey(topicId))?.size ?? 0;
59
- }
60
-
61
- evictStale(now: number): readonly RegistrationRecord[] {
62
- const evicted: RegistrationRecord[] = [];
63
- for (const [tk, inner] of this.byTopic) {
64
- for (const [pk, rec] of inner) {
65
- if (now - rec.lastPing > rec.ttl) {
66
- evicted.push(rec);
67
- inner.delete(pk);
68
- }
69
- }
70
- if (inner.size === 0) {
71
- this.byTopic.delete(tk);
72
- }
73
- }
74
- return evicted;
75
- }
76
- }
77
-
78
- /** Construct an empty {@link RegistrationStore}. */
79
- export function createRegistrationStore(): RegistrationStore {
80
- return new InMemoryRegistrationStore();
81
- }
1
+ /**
2
+ * Cohort-topic substrate — in-memory registration store.
3
+ *
4
+ * Per `docs/cohort-topic.md` §Registration mechanics. Records are doubly indexed: an outer map
5
+ * keyed by topic, each holding an inner map keyed by participant. This gives O(1)
6
+ * {@link RegistrationStore.getByParticipant} / {@link RegistrationStore.delete} and O(participants)
7
+ * {@link RegistrationStore.listByTopic} without a secondary index to keep in sync. The store is
8
+ * local soft state; cross-member replication runs over cohort gossip in a later ticket.
9
+ */
10
+
11
+ import { bytesKey } from "./bytes.js";
12
+ import type { RegistrationRecord, RegistrationStore } from "./types.js";
13
+
14
+ class InMemoryRegistrationStore implements RegistrationStore {
15
+ /** topicKey → (participantKey → record). Inner maps are pruned when they empty. */
16
+ private readonly byTopic = new Map<string, Map<string, RegistrationRecord>>();
17
+
18
+ put(rec: RegistrationRecord): void {
19
+ const tk = bytesKey(rec.topicId);
20
+ let inner = this.byTopic.get(tk);
21
+ if (inner === undefined) {
22
+ inner = new Map<string, RegistrationRecord>();
23
+ this.byTopic.set(tk, inner);
24
+ }
25
+ inner.set(bytesKey(rec.participantId), rec);
26
+ }
27
+
28
+ getByParticipant(topicId: Uint8Array, participantId: Uint8Array): RegistrationRecord | undefined {
29
+ return this.byTopic.get(bytesKey(topicId))?.get(bytesKey(participantId));
30
+ }
31
+
32
+ listByTopic(topicId: Uint8Array): readonly RegistrationRecord[] {
33
+ const inner = this.byTopic.get(bytesKey(topicId));
34
+ return inner === undefined ? [] : [...inner.values()];
35
+ }
36
+
37
+ listAll(): readonly RegistrationRecord[] {
38
+ const out: RegistrationRecord[] = [];
39
+ for (const inner of this.byTopic.values()) {
40
+ for (const rec of inner.values()) {
41
+ out.push(rec);
42
+ }
43
+ }
44
+ return out;
45
+ }
46
+
47
+ delete(topicId: Uint8Array, participantId: Uint8Array): void {
48
+ const tk = bytesKey(topicId);
49
+ const inner = this.byTopic.get(tk);
50
+ if (inner === undefined) return;
51
+ inner.delete(bytesKey(participantId));
52
+ if (inner.size === 0) {
53
+ this.byTopic.delete(tk);
54
+ }
55
+ }
56
+
57
+ directParticipants(topicId: Uint8Array): number {
58
+ return this.byTopic.get(bytesKey(topicId))?.size ?? 0;
59
+ }
60
+
61
+ evictStale(now: number): readonly RegistrationRecord[] {
62
+ const evicted: RegistrationRecord[] = [];
63
+ for (const [tk, inner] of this.byTopic) {
64
+ for (const [pk, rec] of inner) {
65
+ if (now - rec.lastPing > rec.ttl) {
66
+ evicted.push(rec);
67
+ inner.delete(pk);
68
+ }
69
+ }
70
+ if (inner.size === 0) {
71
+ this.byTopic.delete(tk);
72
+ }
73
+ }
74
+ return evicted;
75
+ }
76
+ }
77
+
78
+ /** Construct an empty {@link RegistrationStore}. */
79
+ export function createRegistrationStore(): RegistrationStore {
80
+ return new InMemoryRegistrationStore();
81
+ }
@@ -1,91 +1,91 @@
1
- /**
2
- * Cohort-topic substrate — registration record store, types and constants.
3
- *
4
- * Transcribed from `docs/cohort-topic.md` §Registration mechanics and §Primary and backup
5
- * sharding. This module owns the **local**, transport-agnostic shapes: the soft-state record a
6
- * cohort member holds per participant, the in-memory store interface over those records, and the
7
- * TTL constants. Cross-member replication (cohort gossip) is layered on top by db-p2p and the
8
- * bridge ticket; db-core never imports FRET or libp2p here.
9
- *
10
- * Peer ids are the opaque byte-array references the cohort-topic substrate uses throughout — the
11
- * same representation as {@link import("../ports.js").PeerRef}`.id` and
12
- * {@link import("../ports.js").RingCoord} (raw `Uint8Array`, not the structural
13
- * {@link import("../../network/types.js").PeerId}). The wire layer carries them as base64url
14
- * strings; this store works in raw bytes and the renewal/handoff bridges translate at the wire
15
- * boundary.
16
- */
17
-
18
- /**
19
- * Soft-state registration a cohort member holds per participant. Replicated across the `~k`
20
- * members by cohort gossip; only {@link RegistrationRecord.primary} serves, with
21
- * {@link RegistrationRecord.backups} watching for warm failover.
22
- */
23
- export interface RegistrationRecord {
24
- /** Topic id, 32 bytes. */
25
- topicId: Uint8Array;
26
- /** Registering participant. */
27
- participantId: Uint8Array;
28
- /** Tier this registration sits at (0..3). */
29
- tier: number;
30
- /** Cohort member assigned to serve this participant. */
31
- primary: Uint8Array;
32
- /** 1..2 warm-failover cohort members. */
33
- backups: Uint8Array[];
34
- /** Unix ms the registration first attached. */
35
- attachedAt: number;
36
- /** Unix ms of the most recent successful ping/touch. */
37
- lastPing: number;
38
- /** Lifetime in ms; record is stale once `now − lastPing > ttl`. */
39
- ttl: number;
40
- /** Opaque application-defined per-registration state; the layer never interprets it. */
41
- appState?: Uint8Array;
42
- }
43
-
44
- /**
45
- * In-memory registration store, indexed for both per-participant lookup and per-topic listing.
46
- * This ticket owns the **local** store and its indexes; the gossip layer and TTL loop call the
47
- * deterministic functions over it.
48
- */
49
- export interface RegistrationStore {
50
- /** Insert or replace the record for `(topicId, participantId)`. */
51
- put(rec: RegistrationRecord): void;
52
- /** Record for `(topicId, participantId)`, or `undefined`. */
53
- getByParticipant(topicId: Uint8Array, participantId: Uint8Array): RegistrationRecord | undefined;
54
- /** All records held for `topicId` (empty if none). */
55
- listByTopic(topicId: Uint8Array): readonly RegistrationRecord[];
56
- /** Every record across all topics — used by the rotation handoff inventory pass. */
57
- listAll(): readonly RegistrationRecord[];
58
- /** Remove the record for `(topicId, participantId)`. */
59
- delete(topicId: Uint8Array, participantId: Uint8Array): void;
60
- /** Stock count of direct participants for `topicId` (drives promotion). */
61
- directParticipants(topicId: Uint8Array): number;
62
- /** Remove and return every record where `now − lastPing > ttl`. */
63
- evictStale(now: number): readonly RegistrationRecord[];
64
- }
65
-
66
- /** Core-tier default registration TTL (ms). */
67
- export const DEFAULT_TTL_MS = 90_000;
68
- /** Edge-tier default registration TTL (ms). */
69
- export const EDGE_TTL_MS = 60_000;
70
- /** Minimum accepted registration TTL (ms). Requests below this are clamped up. */
71
- export const MIN_TTL_MS = 10_000;
72
- /** Maximum accepted registration TTL (ms). Requests above this are clamped down.
73
- * 10 × DEFAULT_TTL_MS keeps the window predictable; prevents a wedged budget slot from a poison TTL. */
74
- export const MAX_TTL_MS = 10 * DEFAULT_TTL_MS;
75
- /** Consecutive ping failures before a participant promotes `backups[0]`. */
76
- export const MAX_PING_FAILURES = 3;
77
-
78
- /**
79
- * Clamp a requested or replicated TTL into the accepted `[MIN_TTL_MS, MAX_TTL_MS]` window.
80
- * Non-positive input falls to {@link DEFAULT_TTL_MS} first. This is the single TTL policy gate:
81
- * both local admission (`accept()`) and gossip replication (`mergeRecords`) run every TTL through
82
- * it, so no store — local or replica — can ever hold a record whose lifetime dodges the cap.
83
- */
84
- export function clampTtl(ttl: number): number {
85
- return Math.min(Math.max(ttl > 0 ? ttl : DEFAULT_TTL_MS, MIN_TTL_MS), MAX_TTL_MS);
86
- }
87
-
88
- /** `ping_interval = ttl / 3` (default 30s Core, 20s Edge), floored to whole ms. */
89
- export function pingIntervalMs(ttl: number): number {
90
- return Math.floor(ttl / 3);
91
- }
1
+ /**
2
+ * Cohort-topic substrate — registration record store, types and constants.
3
+ *
4
+ * Transcribed from `docs/cohort-topic.md` §Registration mechanics and §Primary and backup
5
+ * sharding. This module owns the **local**, transport-agnostic shapes: the soft-state record a
6
+ * cohort member holds per participant, the in-memory store interface over those records, and the
7
+ * TTL constants. Cross-member replication (cohort gossip) is layered on top by db-p2p and the
8
+ * bridge ticket; db-core never imports FRET or libp2p here.
9
+ *
10
+ * Peer ids are the opaque byte-array references the cohort-topic substrate uses throughout — the
11
+ * same representation as {@link import("../ports.js").PeerRef}`.id` and
12
+ * {@link import("../ports.js").RingCoord} (raw `Uint8Array`, not the structural
13
+ * {@link import("../../network/types.js").PeerId}). The wire layer carries them as base64url
14
+ * strings; this store works in raw bytes and the renewal/handoff bridges translate at the wire
15
+ * boundary.
16
+ */
17
+
18
+ /**
19
+ * Soft-state registration a cohort member holds per participant. Replicated across the `~k`
20
+ * members by cohort gossip; only {@link RegistrationRecord.primary} serves, with
21
+ * {@link RegistrationRecord.backups} watching for warm failover.
22
+ */
23
+ export interface RegistrationRecord {
24
+ /** Topic id, 32 bytes. */
25
+ topicId: Uint8Array;
26
+ /** Registering participant. */
27
+ participantId: Uint8Array;
28
+ /** Tier this registration sits at (0..3). */
29
+ tier: number;
30
+ /** Cohort member assigned to serve this participant. */
31
+ primary: Uint8Array;
32
+ /** 1..2 warm-failover cohort members. */
33
+ backups: Uint8Array[];
34
+ /** Unix ms the registration first attached. */
35
+ attachedAt: number;
36
+ /** Unix ms of the most recent successful ping/touch. */
37
+ lastPing: number;
38
+ /** Lifetime in ms; record is stale once `now − lastPing > ttl`. */
39
+ ttl: number;
40
+ /** Opaque application-defined per-registration state; the layer never interprets it. */
41
+ appState?: Uint8Array;
42
+ }
43
+
44
+ /**
45
+ * In-memory registration store, indexed for both per-participant lookup and per-topic listing.
46
+ * This ticket owns the **local** store and its indexes; the gossip layer and TTL loop call the
47
+ * deterministic functions over it.
48
+ */
49
+ export interface RegistrationStore {
50
+ /** Insert or replace the record for `(topicId, participantId)`. */
51
+ put(rec: RegistrationRecord): void;
52
+ /** Record for `(topicId, participantId)`, or `undefined`. */
53
+ getByParticipant(topicId: Uint8Array, participantId: Uint8Array): RegistrationRecord | undefined;
54
+ /** All records held for `topicId` (empty if none). */
55
+ listByTopic(topicId: Uint8Array): readonly RegistrationRecord[];
56
+ /** Every record across all topics — used by the rotation handoff inventory pass. */
57
+ listAll(): readonly RegistrationRecord[];
58
+ /** Remove the record for `(topicId, participantId)`. */
59
+ delete(topicId: Uint8Array, participantId: Uint8Array): void;
60
+ /** Stock count of direct participants for `topicId` (drives promotion). */
61
+ directParticipants(topicId: Uint8Array): number;
62
+ /** Remove and return every record where `now − lastPing > ttl`. */
63
+ evictStale(now: number): readonly RegistrationRecord[];
64
+ }
65
+
66
+ /** Core-tier default registration TTL (ms). */
67
+ export const DEFAULT_TTL_MS = 90_000;
68
+ /** Edge-tier default registration TTL (ms). */
69
+ export const EDGE_TTL_MS = 60_000;
70
+ /** Minimum accepted registration TTL (ms). Requests below this are clamped up. */
71
+ export const MIN_TTL_MS = 10_000;
72
+ /** Maximum accepted registration TTL (ms). Requests above this are clamped down.
73
+ * 10 × DEFAULT_TTL_MS keeps the window predictable; prevents a wedged budget slot from a poison TTL. */
74
+ export const MAX_TTL_MS = 10 * DEFAULT_TTL_MS;
75
+ /** Consecutive ping failures before a participant promotes `backups[0]`. */
76
+ export const MAX_PING_FAILURES = 3;
77
+
78
+ /**
79
+ * Clamp a requested or replicated TTL into the accepted `[MIN_TTL_MS, MAX_TTL_MS]` window.
80
+ * Non-positive input falls to {@link DEFAULT_TTL_MS} first. This is the single TTL policy gate:
81
+ * both local admission (`accept()`) and gossip replication (`mergeRecords`) run every TTL through
82
+ * it, so no store — local or replica — can ever hold a record whose lifetime dodges the cap.
83
+ */
84
+ export function clampTtl(ttl: number): number {
85
+ return Math.min(Math.max(ttl > 0 ? ttl : DEFAULT_TTL_MS, MIN_TTL_MS), MAX_TTL_MS);
86
+ }
87
+
88
+ /** `ping_interval = ttl / 3` (default 30s Core, 20s Edge), floored to whole ms. */
89
+ export function pingIntervalMs(ttl: number): number {
90
+ return Math.floor(ttl / 3);
91
+ }
@@ -1,50 +1,50 @@
1
- import { sha256 } from '@noble/hashes/sha2.js';
2
- import type { IRingHash, RingCoord } from './ports.js';
3
-
4
- /**
5
- * Default ring width in bits. 256 = the full SHA-256 digest, which lines up byte-for-byte with
6
- * FRET's coordinate type (FRET hashes ring keys with SHA-256). Override only when targeting a
7
- * DHT with a narrower ring.
8
- */
9
- export const RING_BITS = 256;
10
-
11
- /**
12
- * db-core's {@link IRingHash}, backed by its own SHA-256 (the same digest db-core already uses
13
- * for logs and block ids) — **not** an import of any FRET hash module. This keeps db-core
14
- * FRET-free while guaranteeing on-the-wire coord compatibility: at the default `ringBits = 256`,
15
- * `H(bytes)` is the full SHA-256 digest, identical to what FRET produces for the same input.
16
- *
17
- * For ring widths that are not a whole number of bytes, the digest is truncated to
18
- * `ceil(ringBits / 8)` bytes and the trailing partial byte's unused low bits are zeroed, so two
19
- * inputs whose digests share the first `ringBits` bits yield byte-identical coords.
20
- */
21
- export class RingHash implements IRingHash {
22
- public readonly ringBits: number;
23
- private readonly nBytes: number;
24
- private readonly tailMask: number;
25
-
26
- constructor(ringBits: number = RING_BITS) {
27
- if (!Number.isInteger(ringBits) || ringBits <= 0 || ringBits > 256) {
28
- throw new RangeError(`ringBits must be an integer in [1, 256], got ${ringBits}`);
29
- }
30
- this.ringBits = ringBits;
31
- this.nBytes = Math.ceil(ringBits / 8);
32
- const remBits = ringBits - (this.nBytes - 1) * 8;
33
- this.tailMask = remBits === 8 ? 0xff : (0xff << (8 - remBits)) & 0xff;
34
- }
35
-
36
- H(bytes: Uint8Array): RingCoord {
37
- const digest = sha256(bytes);
38
- if (this.nBytes === digest.length && this.tailMask === 0xff) {
39
- return digest;
40
- }
41
- const coord = digest.slice(0, this.nBytes);
42
- coord[this.nBytes - 1] = coord[this.nBytes - 1]! & this.tailMask;
43
- return coord;
44
- }
45
- }
46
-
47
- /** Convenience factory mirroring the db-p2p adapter construction style. */
48
- export function createRingHash(ringBits: number = RING_BITS): IRingHash {
49
- return new RingHash(ringBits);
50
- }
1
+ import { sha256 } from '@noble/hashes/sha2.js';
2
+ import type { IRingHash, RingCoord } from './ports.js';
3
+
4
+ /**
5
+ * Default ring width in bits. 256 = the full SHA-256 digest, which lines up byte-for-byte with
6
+ * FRET's coordinate type (FRET hashes ring keys with SHA-256). Override only when targeting a
7
+ * DHT with a narrower ring.
8
+ */
9
+ export const RING_BITS = 256;
10
+
11
+ /**
12
+ * db-core's {@link IRingHash}, backed by its own SHA-256 (the same digest db-core already uses
13
+ * for logs and block ids) — **not** an import of any FRET hash module. This keeps db-core
14
+ * FRET-free while guaranteeing on-the-wire coord compatibility: at the default `ringBits = 256`,
15
+ * `H(bytes)` is the full SHA-256 digest, identical to what FRET produces for the same input.
16
+ *
17
+ * For ring widths that are not a whole number of bytes, the digest is truncated to
18
+ * `ceil(ringBits / 8)` bytes and the trailing partial byte's unused low bits are zeroed, so two
19
+ * inputs whose digests share the first `ringBits` bits yield byte-identical coords.
20
+ */
21
+ export class RingHash implements IRingHash {
22
+ public readonly ringBits: number;
23
+ private readonly nBytes: number;
24
+ private readonly tailMask: number;
25
+
26
+ constructor(ringBits: number = RING_BITS) {
27
+ if (!Number.isInteger(ringBits) || ringBits <= 0 || ringBits > 256) {
28
+ throw new RangeError(`ringBits must be an integer in [1, 256], got ${ringBits}`);
29
+ }
30
+ this.ringBits = ringBits;
31
+ this.nBytes = Math.ceil(ringBits / 8);
32
+ const remBits = ringBits - (this.nBytes - 1) * 8;
33
+ this.tailMask = remBits === 8 ? 0xff : (0xff << (8 - remBits)) & 0xff;
34
+ }
35
+
36
+ H(bytes: Uint8Array): RingCoord {
37
+ const digest = sha256(bytes);
38
+ if (this.nBytes === digest.length && this.tailMask === 0xff) {
39
+ return digest;
40
+ }
41
+ const coord = digest.slice(0, this.nBytes);
42
+ coord[this.nBytes - 1] = coord[this.nBytes - 1]! & this.tailMask;
43
+ return coord;
44
+ }
45
+ }
46
+
47
+ /** Convenience factory mirroring the db-p2p adapter construction style. */
48
+ export function createRingHash(ringBits: number = RING_BITS): IRingHash {
49
+ return new RingHash(ringBits);
50
+ }