@optimystic/db-core 0.21.0 → 0.24.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (185) hide show
  1. package/README.md +336 -336
  2. package/dist/src/btree/btree.d.ts +2 -1
  3. package/dist/src/btree/btree.d.ts.map +1 -1
  4. package/dist/src/btree/btree.js +1 -1
  5. package/dist/src/btree/btree.js.map +1 -1
  6. package/dist/src/chain/chain.d.ts +1 -1
  7. package/dist/src/chain/chain.d.ts.map +1 -1
  8. package/dist/src/chain/chain.js +1 -1
  9. package/dist/src/chain/chain.js.map +1 -1
  10. package/dist/src/cluster/structs.d.ts +39 -1
  11. package/dist/src/cluster/structs.d.ts.map +1 -1
  12. package/dist/src/cluster/structs.js +24 -0
  13. package/dist/src/cluster/structs.js.map +1 -1
  14. package/dist/src/collection/collection.d.ts +20 -1
  15. package/dist/src/collection/collection.d.ts.map +1 -1
  16. package/dist/src/collection/collection.js +31 -4
  17. package/dist/src/collection/collection.js.map +1 -1
  18. package/dist/src/collections/diary/diary.d.ts.map +1 -1
  19. package/dist/src/collections/diary/diary.js +2 -1
  20. package/dist/src/collections/diary/diary.js.map +1 -1
  21. package/dist/src/collections/diary/struct.js +1 -1
  22. package/dist/src/collections/diary/struct.js.map +1 -1
  23. package/dist/src/collections/tree/collection-trunk.js +1 -1
  24. package/dist/src/collections/tree/collection-trunk.js.map +1 -1
  25. package/dist/src/collections/tree/struct.d.ts +1 -1
  26. package/dist/src/collections/tree/struct.d.ts.map +1 -1
  27. package/dist/src/collections/tree/struct.js +2 -1
  28. package/dist/src/collections/tree/struct.js.map +1 -1
  29. package/dist/src/collections/tree/tree.d.ts +5 -0
  30. package/dist/src/collections/tree/tree.d.ts.map +1 -1
  31. package/dist/src/collections/tree/tree.js +7 -0
  32. package/dist/src/collections/tree/tree.js.map +1 -1
  33. package/dist/src/log/log.d.ts +1 -1
  34. package/dist/src/log/log.d.ts.map +1 -1
  35. package/dist/src/log/log.js +2 -2
  36. package/dist/src/log/log.js.map +1 -1
  37. package/dist/src/network/i-peer-network.d.ts +16 -0
  38. package/dist/src/network/i-peer-network.d.ts.map +1 -1
  39. package/dist/src/network/struct.d.ts +39 -2
  40. package/dist/src/network/struct.d.ts.map +1 -1
  41. package/dist/src/network/struct.js +18 -0
  42. package/dist/src/network/struct.js.map +1 -1
  43. package/dist/src/testing/test-transactor.d.ts +95 -8
  44. package/dist/src/testing/test-transactor.d.ts.map +1 -1
  45. package/dist/src/testing/test-transactor.js +133 -12
  46. package/dist/src/testing/test-transactor.js.map +1 -1
  47. package/dist/src/transaction/coordinator.d.ts.map +1 -1
  48. package/dist/src/transaction/coordinator.js +2 -1
  49. package/dist/src/transaction/coordinator.js.map +1 -1
  50. package/dist/src/transaction/transaction.d.ts +1 -1
  51. package/dist/src/transaction/transaction.js +1 -1
  52. package/dist/src/transactor/network-transactor.d.ts.map +1 -1
  53. package/dist/src/transactor/network-transactor.js +57 -18
  54. package/dist/src/transactor/network-transactor.js.map +1 -1
  55. package/dist/src/transactor/transactor-source.d.ts.map +1 -1
  56. package/dist/src/transactor/transactor-source.js +25 -2
  57. package/dist/src/transactor/transactor-source.js.map +1 -1
  58. package/dist/src/transform/cache-source.js +1 -1
  59. package/dist/src/transform/cache-source.js.map +1 -1
  60. package/dist/src/transform/helpers.d.ts +6 -1
  61. package/dist/src/transform/helpers.d.ts.map +1 -1
  62. package/dist/src/transform/helpers.js +7 -6
  63. package/dist/src/transform/helpers.js.map +1 -1
  64. package/dist/src/transform/tracker.d.ts.map +1 -1
  65. package/dist/src/transform/tracker.js +2 -1
  66. package/dist/src/transform/tracker.js.map +1 -1
  67. package/package.json +1 -1
  68. package/src/btree/btree.ts +2 -1
  69. package/src/chain/chain.ts +2 -1
  70. package/src/cluster/membership.ts +85 -85
  71. package/src/cluster/structs.ts +43 -4
  72. package/src/cohort-topic/addressing.ts +120 -120
  73. package/src/cohort-topic/antidos/bootstrap-evidence-envelope.ts +253 -253
  74. package/src/cohort-topic/antidos/bootstrap-evidence.ts +106 -106
  75. package/src/cohort-topic/antidos/index.ts +5 -5
  76. package/src/cohort-topic/antidos/rate-limiter.ts +210 -210
  77. package/src/cohort-topic/antidos/replay-guard.ts +146 -146
  78. package/src/cohort-topic/antidos/topic-budget.ts +160 -160
  79. package/src/cohort-topic/antiflood/index.ts +2 -2
  80. package/src/cohort-topic/antiflood/invariants.ts +108 -108
  81. package/src/cohort-topic/antiflood/jitter.ts +117 -117
  82. package/src/cohort-topic/coldstart.ts +237 -237
  83. package/src/cohort-topic/dmax.ts +88 -88
  84. package/src/cohort-topic/gossip/bus.ts +254 -254
  85. package/src/cohort-topic/gossip/index.ts +3 -3
  86. package/src/cohort-topic/gossip/records.ts +45 -45
  87. package/src/cohort-topic/gossip/view.ts +91 -91
  88. package/src/cohort-topic/index.ts +20 -20
  89. package/src/cohort-topic/load/barometer.ts +134 -134
  90. package/src/cohort-topic/load/index.ts +1 -1
  91. package/src/cohort-topic/member-engine.ts +430 -430
  92. package/src/cohort-topic/membership/index.ts +3 -3
  93. package/src/cohort-topic/membership/publisher.ts +163 -163
  94. package/src/cohort-topic/membership/source.ts +41 -41
  95. package/src/cohort-topic/membership/verifier.ts +461 -461
  96. package/src/cohort-topic/ports.ts +157 -157
  97. package/src/cohort-topic/promotion.ts +405 -405
  98. package/src/cohort-topic/registration/bytes.ts +37 -37
  99. package/src/cohort-topic/registration/handoff.ts +154 -154
  100. package/src/cohort-topic/registration/index.ts +6 -6
  101. package/src/cohort-topic/registration/renewal.ts +495 -495
  102. package/src/cohort-topic/registration/sharding.ts +61 -61
  103. package/src/cohort-topic/registration/store.ts +81 -81
  104. package/src/cohort-topic/registration/types.ts +91 -91
  105. package/src/cohort-topic/ring-hash.ts +50 -50
  106. package/src/cohort-topic/service.ts +416 -416
  107. package/src/cohort-topic/sig/index.ts +2 -2
  108. package/src/cohort-topic/sig/payloads.ts +59 -59
  109. package/src/cohort-topic/sig/threshold.ts +64 -64
  110. package/src/cohort-topic/tiers.ts +74 -74
  111. package/src/cohort-topic/traffic.ts +233 -233
  112. package/src/cohort-topic/walk.ts +326 -326
  113. package/src/cohort-topic/willingness.ts +237 -237
  114. package/src/cohort-topic/wire/codec.ts +216 -216
  115. package/src/cohort-topic/wire/index.ts +18 -18
  116. package/src/cohort-topic/wire/payloads.ts +126 -126
  117. package/src/cohort-topic/wire/primitives.ts +188 -188
  118. package/src/cohort-topic/wire/types.ts +475 -475
  119. package/src/cohort-topic/wire/validate.ts +512 -512
  120. package/src/collection/collection-type-registry.ts +37 -37
  121. package/src/collection/collection.ts +32 -4
  122. package/src/collections/diary/diary.ts +68 -67
  123. package/src/collections/diary/struct.ts +1 -1
  124. package/src/collections/tree/collection-trunk.ts +1 -1
  125. package/src/collections/tree/readme.md +4 -0
  126. package/src/collections/tree/struct.ts +3 -1
  127. package/src/collections/tree/tree.ts +320 -312
  128. package/src/log/log.ts +2 -2
  129. package/src/matchmaking/capability-filter.ts +45 -45
  130. package/src/matchmaking/config.ts +98 -98
  131. package/src/matchmaking/index.ts +21 -21
  132. package/src/matchmaking/multi-cohort-seeker.ts +234 -234
  133. package/src/matchmaking/provider.ts +123 -123
  134. package/src/matchmaking/query-eval.ts +105 -105
  135. package/src/matchmaking/seeker-walk.ts +127 -127
  136. package/src/matchmaking/seeker.ts +86 -86
  137. package/src/matchmaking/topic-anchor.ts +90 -90
  138. package/src/matchmaking/voting-quorum.ts +394 -394
  139. package/src/matchmaking/wire.ts +603 -603
  140. package/src/network/i-peer-network.ts +17 -0
  141. package/src/network/stale-failure.ts +43 -43
  142. package/src/network/struct.ts +41 -2
  143. package/src/network/types.ts +37 -37
  144. package/src/reactivity/backfill.ts +220 -220
  145. package/src/reactivity/backpressure.ts +191 -191
  146. package/src/reactivity/checkpoint.ts +308 -308
  147. package/src/reactivity/config.ts +172 -172
  148. package/src/reactivity/dedupe.ts +132 -132
  149. package/src/reactivity/forwarder.ts +87 -87
  150. package/src/reactivity/index.ts +34 -34
  151. package/src/reactivity/notification.ts +123 -123
  152. package/src/reactivity/policy.ts +79 -79
  153. package/src/reactivity/push-state.ts +310 -310
  154. package/src/reactivity/recover.ts +153 -153
  155. package/src/reactivity/replay-buffer.ts +141 -141
  156. package/src/reactivity/resume.ts +549 -549
  157. package/src/reactivity/rotation.ts +415 -415
  158. package/src/reactivity/subscriber.ts +132 -132
  159. package/src/reactivity/subscription.ts +66 -66
  160. package/src/reactivity/topic-anchor.ts +71 -71
  161. package/src/reactivity/verify.ts +73 -73
  162. package/src/reactivity/wire-validate.ts +13 -13
  163. package/src/reactivity/wire.ts +224 -224
  164. package/src/testing/async-wait.ts +65 -65
  165. package/src/testing/index.ts +2 -2
  166. package/src/testing/test-transactor.ts +638 -489
  167. package/src/transaction/coordinator.ts +2 -1
  168. package/src/transaction/errors.ts +91 -91
  169. package/src/transaction/operations-hash.ts +196 -196
  170. package/src/transaction/read-dependency-collector.ts +78 -78
  171. package/src/transaction/transaction.ts +1 -1
  172. package/src/transactor/change-notifier.ts +80 -80
  173. package/src/transactor/index.ts +5 -5
  174. package/src/transactor/network-transactor.ts +58 -19
  175. package/src/transactor/transactor-source.ts +25 -2
  176. package/src/transform/atomic-proxy.ts +92 -92
  177. package/src/transform/cache-source.ts +1 -1
  178. package/src/transform/helpers.ts +159 -158
  179. package/src/transform/tracker.ts +2 -1
  180. package/src/utility/backoff.ts +95 -95
  181. package/src/utility/batch-coordinator.ts +191 -191
  182. package/dist/src/transaction/context.d.ts +0 -60
  183. package/dist/src/transaction/context.d.ts.map +0 -1
  184. package/dist/src/transaction/context.js +0 -91
  185. package/dist/src/transaction/context.js.map +0 -1
@@ -1,37 +1,37 @@
1
- /**
2
- * Cohort-topic substrate — byte-array helpers shared by the registration store, sharding, and
3
- * handoff. Peer ids and topic ids are raw {@link Uint8Array}; these give them stable string map
4
- * keys, value equality, and the ascending order the sharding hash relies on.
5
- */
6
-
7
- import { compare as compareU8 } from "uint8arrays/compare";
8
- import { equals as equalsU8 } from "uint8arrays/equals";
9
- import { bytesToB64url } from "../wire/codec.js";
10
-
11
- /** Stable, collision-free string map key for a byte array (base64url, the package's canonical form). */
12
- export function bytesKey(b: Uint8Array): string {
13
- return bytesToB64url(b);
14
- }
15
-
16
- /** Composite key for a registration, `topicKey|participantKey`. */
17
- export function recordKey(topicId: Uint8Array, participantId: Uint8Array): string {
18
- return `${bytesKey(topicId)}|${bytesKey(participantId)}`;
19
- }
20
-
21
- /** Value equality over byte arrays. */
22
- export function bytesEqual(a: Uint8Array, b: Uint8Array): boolean {
23
- return equalsU8(a, b);
24
- }
25
-
26
- /** Lexicographic byte order — the `sort(... by PeerId ascending)` of §Primary and backup sharding. */
27
- export function compareBytes(a: Uint8Array, b: Uint8Array): number {
28
- return compareU8(a, b);
29
- }
30
-
31
- /** Concatenate two byte arrays into a fresh buffer (the `participantId ‖ cohortEpoch` hash input). */
32
- export function concatBytes(a: Uint8Array, b: Uint8Array): Uint8Array {
33
- const out = new Uint8Array(a.length + b.length);
34
- out.set(a, 0);
35
- out.set(b, a.length);
36
- return out;
37
- }
1
+ /**
2
+ * Cohort-topic substrate — byte-array helpers shared by the registration store, sharding, and
3
+ * handoff. Peer ids and topic ids are raw {@link Uint8Array}; these give them stable string map
4
+ * keys, value equality, and the ascending order the sharding hash relies on.
5
+ */
6
+
7
+ import { compare as compareU8 } from "uint8arrays/compare";
8
+ import { equals as equalsU8 } from "uint8arrays/equals";
9
+ import { bytesToB64url } from "../wire/codec.js";
10
+
11
+ /** Stable, collision-free string map key for a byte array (base64url, the package's canonical form). */
12
+ export function bytesKey(b: Uint8Array): string {
13
+ return bytesToB64url(b);
14
+ }
15
+
16
+ /** Composite key for a registration, `topicKey|participantKey`. */
17
+ export function recordKey(topicId: Uint8Array, participantId: Uint8Array): string {
18
+ return `${bytesKey(topicId)}|${bytesKey(participantId)}`;
19
+ }
20
+
21
+ /** Value equality over byte arrays. */
22
+ export function bytesEqual(a: Uint8Array, b: Uint8Array): boolean {
23
+ return equalsU8(a, b);
24
+ }
25
+
26
+ /** Lexicographic byte order — the `sort(... by PeerId ascending)` of §Primary and backup sharding. */
27
+ export function compareBytes(a: Uint8Array, b: Uint8Array): number {
28
+ return compareU8(a, b);
29
+ }
30
+
31
+ /** Concatenate two byte arrays into a fresh buffer (the `participantId ‖ cohortEpoch` hash input). */
32
+ export function concatBytes(a: Uint8Array, b: Uint8Array): Uint8Array {
33
+ const out = new Uint8Array(a.length + b.length);
34
+ out.set(a, 0);
35
+ out.set(b, a.length);
36
+ return out;
37
+ }
@@ -1,154 +1,154 @@
1
- /**
2
- * Cohort-topic substrate — membership-rotation primary handoff.
3
- *
4
- * Per `docs/cohort-topic.md` §Membership rotation and primary handoff. When membership changes the
5
- * deterministic slot may move a registration's primary. Each member runs this state machine:
6
- *
7
- * 1. New membership stabilizes; all members compute the new `cohortEpoch`.
8
- * 2. Members exchange a "primary inventory" listing the records they hold as primary ({@link start}).
9
- * 3. For each record, the new computed primary either already holds it (no-op) or **pulls** it from
10
- * the previous holder ({@link onInventory} → `transport.pull`).
11
- * 4. **Resolved (GROUNDING):** the previous primary keeps serving (**dual-serve**) until the new
12
- * primary acknowledges receipt ({@link onAck}) — closing the delivery gap.
13
- * 5. Participants discover the new primary on their next ping/delivery (renewal module).
14
- *
15
- * The gossip *transport* for the inventory comes from a later ticket; it is injected here so the
16
- * machine is unit-testable with a mock. The machine is purely cohort-local — FRET is unaware of it.
17
- */
18
-
19
- import { bytesEqual, recordKey } from "./bytes.js";
20
- import type { SlotAssigner } from "./sharding.js";
21
- import type { RegistrationRecord, RegistrationStore } from "./types.js";
22
-
23
- /** Identifies one registration in a {@link PrimaryInventory} or pull/ack exchange. */
24
- export interface RecordRef {
25
- topicId: Uint8Array;
26
- participantId: Uint8Array;
27
- }
28
-
29
- /** A member's list of registrations it currently holds as primary, under the new `cohortEpoch`. */
30
- export interface PrimaryInventory {
31
- /** The advertising member. */
32
- from: Uint8Array;
33
- /** The new cohort epoch this inventory was computed under. */
34
- epoch: Uint8Array;
35
- entries: readonly RecordRef[];
36
- }
37
-
38
- /** Transport the handoff drives; supplied by the cohort-gossip ticket, mocked in tests. */
39
- export interface HandoffTransport {
40
- /** Broadcast this member's primary inventory to the cohort. */
41
- sendInventory(inv: PrimaryInventory): void;
42
- /** Pull a record this member is now primary for from its previous holder. */
43
- pull(from: Uint8Array, ref: RecordRef): Promise<RegistrationRecord | undefined>;
44
- /** Acknowledge receipt to the previous holder, ending its dual-serve for `ref`. */
45
- ack(to: Uint8Array, ref: RecordRef): void;
46
- }
47
-
48
- export interface HandoffDeps {
49
- store: RegistrationStore;
50
- /** This member's own peer id. */
51
- self: Uint8Array;
52
- slots: SlotAssigner;
53
- /** The new (post-rotation) cohort snapshot. */
54
- cohort: () => { members: readonly Uint8Array[]; cohortEpoch: Uint8Array };
55
- transport: HandoffTransport;
56
- }
57
-
58
- /** Per-member primary-handoff driver. One instance handles one rotation. */
59
- export interface MembershipHandoff {
60
- /** Step 1+2: recompute under the new epoch, publish this member's inventory, mark hand-offs. */
61
- start(): void;
62
- /** Step 3: react to a peer's inventory — pull every record now assigned to this member, then ack. */
63
- onInventory(inv: PrimaryInventory): Promise<void>;
64
- /** Serve a pull from the new primary (this member is the previous holder); stays dual-serving. */
65
- onPull(ref: RecordRef): RegistrationRecord | undefined;
66
- /** New primary acked receipt: stop dual-serving `ref`. */
67
- onAck(ref: RecordRef): void;
68
- /** Whether this member should still answer renews for `(topicId, participantId)` right now. */
69
- isServing(topicId: Uint8Array, participantId: Uint8Array): boolean;
70
- }
71
-
72
- class StoreMembershipHandoff implements MembershipHandoff {
73
- /** Records this member held as primary but no longer is — kept serving until acked. */
74
- private readonly dualServing = new Set<string>();
75
- /** In-flight pulls, so a duplicate inventory does not pull twice. */
76
- private readonly pulling = new Set<string>();
77
-
78
- constructor(private readonly deps: HandoffDeps) {}
79
-
80
- start(): void {
81
- const { members, cohortEpoch } = this.deps.cohort();
82
- const entries: RecordRef[] = [];
83
- for (const rec of this.deps.store.listAll()) {
84
- if (!bytesEqual(rec.primary, this.deps.self)) {
85
- continue; // only records this member currently holds as primary
86
- }
87
- entries.push({ topicId: rec.topicId, participantId: rec.participantId });
88
- const { primary } = this.deps.slots.assignSlots(rec.participantId, cohortEpoch, members);
89
- if (!bytesEqual(primary, this.deps.self)) {
90
- // Primary moved away from this member: keep serving until the new primary acks.
91
- this.dualServing.add(recordKey(rec.topicId, rec.participantId));
92
- }
93
- }
94
- this.deps.transport.sendInventory({ from: this.deps.self, epoch: cohortEpoch, entries });
95
- }
96
-
97
- async onInventory(inv: PrimaryInventory): Promise<void> {
98
- if (bytesEqual(inv.from, this.deps.self)) {
99
- return; // ignore our own inventory
100
- }
101
- const { members, cohortEpoch } = this.deps.cohort();
102
- for (const ref of inv.entries) {
103
- await this.maybePull(inv.from, ref, members, cohortEpoch);
104
- }
105
- }
106
-
107
- private async maybePull(from: Uint8Array, ref: RecordRef, members: readonly Uint8Array[], cohortEpoch: Uint8Array): Promise<void> {
108
- const { primary, backups } = this.deps.slots.assignSlots(ref.participantId, cohortEpoch, members);
109
- if (!bytesEqual(primary, this.deps.self)) {
110
- return; // not assigned to this member now
111
- }
112
- if (this.deps.store.getByParticipant(ref.topicId, ref.participantId) !== undefined) {
113
- return; // already hold it (no-op handoff)
114
- }
115
- const key = recordKey(ref.topicId, ref.participantId);
116
- if (this.pulling.has(key)) {
117
- return;
118
- }
119
- this.pulling.add(key);
120
- try {
121
- const rec = await this.deps.transport.pull(from, ref);
122
- if (rec !== undefined) {
123
- // Re-stamp the assignment so a later rotation's `start()` recognises this member as primary.
124
- this.deps.store.put({ ...rec, primary, backups });
125
- this.deps.transport.ack(from, ref); // ends dual-serve at the previous holder
126
- }
127
- } finally {
128
- this.pulling.delete(key);
129
- }
130
- }
131
-
132
- onPull(ref: RecordRef): RegistrationRecord | undefined {
133
- // Previous holder serves the record; it remains in dualServing until the ack arrives.
134
- return this.deps.store.getByParticipant(ref.topicId, ref.participantId);
135
- }
136
-
137
- onAck(ref: RecordRef): void {
138
- this.dualServing.delete(recordKey(ref.topicId, ref.participantId));
139
- }
140
-
141
- isServing(topicId: Uint8Array, participantId: Uint8Array): boolean {
142
- const { members, cohortEpoch } = this.deps.cohort();
143
- const { primary } = this.deps.slots.assignSlots(participantId, cohortEpoch, members);
144
- if (bytesEqual(primary, this.deps.self)) {
145
- return true;
146
- }
147
- return this.dualServing.has(recordKey(topicId, participantId));
148
- }
149
- }
150
-
151
- /** Build a {@link MembershipHandoff} for one rotation. */
152
- export function createMembershipHandoff(deps: HandoffDeps): MembershipHandoff {
153
- return new StoreMembershipHandoff(deps);
154
- }
1
+ /**
2
+ * Cohort-topic substrate — membership-rotation primary handoff.
3
+ *
4
+ * Per `docs/cohort-topic.md` §Membership rotation and primary handoff. When membership changes the
5
+ * deterministic slot may move a registration's primary. Each member runs this state machine:
6
+ *
7
+ * 1. New membership stabilizes; all members compute the new `cohortEpoch`.
8
+ * 2. Members exchange a "primary inventory" listing the records they hold as primary ({@link start}).
9
+ * 3. For each record, the new computed primary either already holds it (no-op) or **pulls** it from
10
+ * the previous holder ({@link onInventory} → `transport.pull`).
11
+ * 4. **Resolved (GROUNDING):** the previous primary keeps serving (**dual-serve**) until the new
12
+ * primary acknowledges receipt ({@link onAck}) — closing the delivery gap.
13
+ * 5. Participants discover the new primary on their next ping/delivery (renewal module).
14
+ *
15
+ * The gossip *transport* for the inventory comes from a later ticket; it is injected here so the
16
+ * machine is unit-testable with a mock. The machine is purely cohort-local — FRET is unaware of it.
17
+ */
18
+
19
+ import { bytesEqual, recordKey } from "./bytes.js";
20
+ import type { SlotAssigner } from "./sharding.js";
21
+ import type { RegistrationRecord, RegistrationStore } from "./types.js";
22
+
23
+ /** Identifies one registration in a {@link PrimaryInventory} or pull/ack exchange. */
24
+ export interface RecordRef {
25
+ topicId: Uint8Array;
26
+ participantId: Uint8Array;
27
+ }
28
+
29
+ /** A member's list of registrations it currently holds as primary, under the new `cohortEpoch`. */
30
+ export interface PrimaryInventory {
31
+ /** The advertising member. */
32
+ from: Uint8Array;
33
+ /** The new cohort epoch this inventory was computed under. */
34
+ epoch: Uint8Array;
35
+ entries: readonly RecordRef[];
36
+ }
37
+
38
+ /** Transport the handoff drives; supplied by the cohort-gossip ticket, mocked in tests. */
39
+ export interface HandoffTransport {
40
+ /** Broadcast this member's primary inventory to the cohort. */
41
+ sendInventory(inv: PrimaryInventory): void;
42
+ /** Pull a record this member is now primary for from its previous holder. */
43
+ pull(from: Uint8Array, ref: RecordRef): Promise<RegistrationRecord | undefined>;
44
+ /** Acknowledge receipt to the previous holder, ending its dual-serve for `ref`. */
45
+ ack(to: Uint8Array, ref: RecordRef): void;
46
+ }
47
+
48
+ export interface HandoffDeps {
49
+ store: RegistrationStore;
50
+ /** This member's own peer id. */
51
+ self: Uint8Array;
52
+ slots: SlotAssigner;
53
+ /** The new (post-rotation) cohort snapshot. */
54
+ cohort: () => { members: readonly Uint8Array[]; cohortEpoch: Uint8Array };
55
+ transport: HandoffTransport;
56
+ }
57
+
58
+ /** Per-member primary-handoff driver. One instance handles one rotation. */
59
+ export interface MembershipHandoff {
60
+ /** Step 1+2: recompute under the new epoch, publish this member's inventory, mark hand-offs. */
61
+ start(): void;
62
+ /** Step 3: react to a peer's inventory — pull every record now assigned to this member, then ack. */
63
+ onInventory(inv: PrimaryInventory): Promise<void>;
64
+ /** Serve a pull from the new primary (this member is the previous holder); stays dual-serving. */
65
+ onPull(ref: RecordRef): RegistrationRecord | undefined;
66
+ /** New primary acked receipt: stop dual-serving `ref`. */
67
+ onAck(ref: RecordRef): void;
68
+ /** Whether this member should still answer renews for `(topicId, participantId)` right now. */
69
+ isServing(topicId: Uint8Array, participantId: Uint8Array): boolean;
70
+ }
71
+
72
+ class StoreMembershipHandoff implements MembershipHandoff {
73
+ /** Records this member held as primary but no longer is — kept serving until acked. */
74
+ private readonly dualServing = new Set<string>();
75
+ /** In-flight pulls, so a duplicate inventory does not pull twice. */
76
+ private readonly pulling = new Set<string>();
77
+
78
+ constructor(private readonly deps: HandoffDeps) {}
79
+
80
+ start(): void {
81
+ const { members, cohortEpoch } = this.deps.cohort();
82
+ const entries: RecordRef[] = [];
83
+ for (const rec of this.deps.store.listAll()) {
84
+ if (!bytesEqual(rec.primary, this.deps.self)) {
85
+ continue; // only records this member currently holds as primary
86
+ }
87
+ entries.push({ topicId: rec.topicId, participantId: rec.participantId });
88
+ const { primary } = this.deps.slots.assignSlots(rec.participantId, cohortEpoch, members);
89
+ if (!bytesEqual(primary, this.deps.self)) {
90
+ // Primary moved away from this member: keep serving until the new primary acks.
91
+ this.dualServing.add(recordKey(rec.topicId, rec.participantId));
92
+ }
93
+ }
94
+ this.deps.transport.sendInventory({ from: this.deps.self, epoch: cohortEpoch, entries });
95
+ }
96
+
97
+ async onInventory(inv: PrimaryInventory): Promise<void> {
98
+ if (bytesEqual(inv.from, this.deps.self)) {
99
+ return; // ignore our own inventory
100
+ }
101
+ const { members, cohortEpoch } = this.deps.cohort();
102
+ for (const ref of inv.entries) {
103
+ await this.maybePull(inv.from, ref, members, cohortEpoch);
104
+ }
105
+ }
106
+
107
+ private async maybePull(from: Uint8Array, ref: RecordRef, members: readonly Uint8Array[], cohortEpoch: Uint8Array): Promise<void> {
108
+ const { primary, backups } = this.deps.slots.assignSlots(ref.participantId, cohortEpoch, members);
109
+ if (!bytesEqual(primary, this.deps.self)) {
110
+ return; // not assigned to this member now
111
+ }
112
+ if (this.deps.store.getByParticipant(ref.topicId, ref.participantId) !== undefined) {
113
+ return; // already hold it (no-op handoff)
114
+ }
115
+ const key = recordKey(ref.topicId, ref.participantId);
116
+ if (this.pulling.has(key)) {
117
+ return;
118
+ }
119
+ this.pulling.add(key);
120
+ try {
121
+ const rec = await this.deps.transport.pull(from, ref);
122
+ if (rec !== undefined) {
123
+ // Re-stamp the assignment so a later rotation's `start()` recognises this member as primary.
124
+ this.deps.store.put({ ...rec, primary, backups });
125
+ this.deps.transport.ack(from, ref); // ends dual-serve at the previous holder
126
+ }
127
+ } finally {
128
+ this.pulling.delete(key);
129
+ }
130
+ }
131
+
132
+ onPull(ref: RecordRef): RegistrationRecord | undefined {
133
+ // Previous holder serves the record; it remains in dualServing until the ack arrives.
134
+ return this.deps.store.getByParticipant(ref.topicId, ref.participantId);
135
+ }
136
+
137
+ onAck(ref: RecordRef): void {
138
+ this.dualServing.delete(recordKey(ref.topicId, ref.participantId));
139
+ }
140
+
141
+ isServing(topicId: Uint8Array, participantId: Uint8Array): boolean {
142
+ const { members, cohortEpoch } = this.deps.cohort();
143
+ const { primary } = this.deps.slots.assignSlots(participantId, cohortEpoch, members);
144
+ if (bytesEqual(primary, this.deps.self)) {
145
+ return true;
146
+ }
147
+ return this.dualServing.has(recordKey(topicId, participantId));
148
+ }
149
+ }
150
+
151
+ /** Build a {@link MembershipHandoff} for one rotation. */
152
+ export function createMembershipHandoff(deps: HandoffDeps): MembershipHandoff {
153
+ return new StoreMembershipHandoff(deps);
154
+ }
@@ -1,6 +1,6 @@
1
- export * from "./types.js";
2
- export * from "./bytes.js";
3
- export * from "./store.js";
4
- export * from "./sharding.js";
5
- export * from "./renewal.js";
6
- export * from "./handoff.js";
1
+ export * from "./types.js";
2
+ export * from "./bytes.js";
3
+ export * from "./store.js";
4
+ export * from "./sharding.js";
5
+ export * from "./renewal.js";
6
+ export * from "./handoff.js";