@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.
- package/README.md +336 -336
- package/dist/src/btree/btree.d.ts +2 -1
- package/dist/src/btree/btree.d.ts.map +1 -1
- package/dist/src/btree/btree.js +1 -1
- package/dist/src/btree/btree.js.map +1 -1
- package/dist/src/chain/chain.d.ts +1 -1
- package/dist/src/chain/chain.d.ts.map +1 -1
- package/dist/src/chain/chain.js +1 -1
- package/dist/src/chain/chain.js.map +1 -1
- 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 +20 -1
- package/dist/src/collection/collection.d.ts.map +1 -1
- package/dist/src/collection/collection.js +31 -4
- package/dist/src/collection/collection.js.map +1 -1
- package/dist/src/collections/diary/diary.d.ts.map +1 -1
- package/dist/src/collections/diary/diary.js +2 -1
- package/dist/src/collections/diary/diary.js.map +1 -1
- package/dist/src/collections/diary/struct.js +1 -1
- package/dist/src/collections/diary/struct.js.map +1 -1
- package/dist/src/collections/tree/collection-trunk.js +1 -1
- package/dist/src/collections/tree/collection-trunk.js.map +1 -1
- package/dist/src/collections/tree/struct.d.ts +1 -1
- package/dist/src/collections/tree/struct.d.ts.map +1 -1
- package/dist/src/collections/tree/struct.js +2 -1
- package/dist/src/collections/tree/struct.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/log/log.d.ts +1 -1
- package/dist/src/log/log.d.ts.map +1 -1
- package/dist/src/log/log.js +2 -2
- package/dist/src/log/log.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 +133 -12
- package/dist/src/testing/test-transactor.js.map +1 -1
- package/dist/src/transaction/coordinator.d.ts.map +1 -1
- package/dist/src/transaction/coordinator.js +2 -1
- package/dist/src/transaction/coordinator.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 +57 -18
- 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/dist/src/transform/cache-source.js +1 -1
- package/dist/src/transform/cache-source.js.map +1 -1
- package/dist/src/transform/helpers.d.ts +6 -1
- package/dist/src/transform/helpers.d.ts.map +1 -1
- package/dist/src/transform/helpers.js +7 -6
- package/dist/src/transform/helpers.js.map +1 -1
- package/dist/src/transform/tracker.d.ts.map +1 -1
- package/dist/src/transform/tracker.js +2 -1
- package/dist/src/transform/tracker.js.map +1 -1
- package/package.json +1 -1
- package/src/btree/btree.ts +2 -1
- package/src/chain/chain.ts +2 -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 +32 -4
- package/src/collections/diary/diary.ts +68 -67
- package/src/collections/diary/struct.ts +1 -1
- package/src/collections/tree/collection-trunk.ts +1 -1
- package/src/collections/tree/readme.md +4 -0
- package/src/collections/tree/struct.ts +3 -1
- package/src/collections/tree/tree.ts +320 -312
- package/src/log/log.ts +2 -2
- 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 -489
- package/src/transaction/coordinator.ts +2 -1
- 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 +58 -19
- package/src/transactor/transactor-source.ts +25 -2
- package/src/transform/atomic-proxy.ts +92 -92
- package/src/transform/cache-source.ts +1 -1
- package/src/transform/helpers.ts +159 -158
- package/src/transform/tracker.ts +2 -1
- 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,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";
|