@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,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
|
+
}
|