@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,87 +1,87 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* Reactivity — forwarder receive path (`docs/reactivity.md` §Propagation).
|
|
3
|
-
*
|
|
4
|
-
* A receiving forwarder primary, for each inbound notification:
|
|
5
|
-
* 1. **verifies** the threshold signature against the tail cohort's `MembershipCertV1`;
|
|
6
|
-
* 2. runs the **dedupe** check (sliding `(revision, sigDigest)` window);
|
|
7
|
-
* 3. **appends** to the replay buffer;
|
|
8
|
-
* 4. **forwards the unmodified notification** to its own direct subscribers and child cohorts.
|
|
9
|
-
*
|
|
10
|
-
* This module owns steps 1–3 and the forward *decision* (step 4's transport — dialing each subscriber's
|
|
11
|
-
* primary and each child cohort — is the db-p2p binding). Forwarders never re-sign: a compromised
|
|
12
|
-
* forwarder can drop or delay, but cannot forge. An unverifiable notification is dropped **before**
|
|
13
|
-
* touching the dedupe set or buffer, so a forged payload can neither poison dedupe nor occupy a ring
|
|
14
|
-
* slot. The buffer + dedupe set are gossiped across the cohort by the host's gossip cadence (see
|
|
15
|
-
* {@link PushState}), so any member can serve a replay.
|
|
16
|
-
*/
|
|
17
|
-
|
|
18
|
-
import { dedupeKey, sigDigest } from "./notification.js";
|
|
19
|
-
import type { PushState } from "./push-state.js";
|
|
20
|
-
import type { NotificationV1 } from "./wire.js";
|
|
21
|
-
import type { NotificationVerifier } from "./verify.js";
|
|
22
|
-
import type { IRingHash } from "../cohort-topic/ports.js";
|
|
23
|
-
|
|
24
|
-
/** The forwarder's decision for one inbound notification. */
|
|
25
|
-
export type ForwardDecision =
|
|
26
|
-
/** Verified, fresh: append to buffer and fan out unmodified. */
|
|
27
|
-
| "forward"
|
|
28
|
-
/** Already in the dedupe set: drop silently (an honest retransmit or merge duplicate). */
|
|
29
|
-
| "duplicate"
|
|
30
|
-
/** Signature did not verify against the tail cohort: drop without buffering. */
|
|
31
|
-
| "untrusted";
|
|
32
|
-
|
|
33
|
-
/** Drives the forwarder receive path over one collection's {@link PushState}. */
|
|
34
|
-
export interface ReactivityForwarder {
|
|
35
|
-
/** The per-collection push state this forwarder serves. */
|
|
36
|
-
readonly state: PushState;
|
|
37
|
-
/**
|
|
38
|
-
* Run verify → dedupe → append for one inbound notification, returning the forward decision. The
|
|
39
|
-
* caller fans the *unmodified* notification out only on `"forward"`.
|
|
40
|
-
*/
|
|
41
|
-
receive(n: NotificationV1, now: number): Promise<ForwardDecision>;
|
|
42
|
-
}
|
|
43
|
-
|
|
44
|
-
/** Construction inputs for a {@link ReactivityForwarder}. */
|
|
45
|
-
export interface ReactivityForwarderDeps {
|
|
46
|
-
readonly state: PushState;
|
|
47
|
-
readonly verifier: NotificationVerifier;
|
|
48
|
-
/** Ring hash for the `sigDigest` dedupe key (must match the verifier's). Default db-core SHA-256. */
|
|
49
|
-
readonly hash?: IRingHash;
|
|
50
|
-
}
|
|
51
|
-
|
|
52
|
-
class PushStateForwarder implements ReactivityForwarder {
|
|
53
|
-
readonly state: PushState;
|
|
54
|
-
|
|
55
|
-
constructor(private readonly deps: ReactivityForwarderDeps) {
|
|
56
|
-
this.state = deps.state;
|
|
57
|
-
}
|
|
58
|
-
|
|
59
|
-
async receive(n: NotificationV1, now: number): Promise<ForwardDecision> {
|
|
60
|
-
// 1. Verify end-to-end before any state mutation — a forged notification touches nothing.
|
|
61
|
-
const verdict = await this.deps.verifier.verify(n);
|
|
62
|
-
if (verdict !== "verified") {
|
|
63
|
-
return "untrusted";
|
|
64
|
-
}
|
|
65
|
-
// 2. Dedupe on `(revision, sigDigest)`. Already-seen → drop silently.
|
|
66
|
-
const digest = sigDigest(n.sig, this.deps.hash);
|
|
67
|
-
if (this.state.dedupe.observe(n.revision, digest) === "duplicate") {
|
|
68
|
-
return "duplicate";
|
|
69
|
-
}
|
|
70
|
-
// 3. Append the full signed notification to the replay ring (gossiped across the cohort).
|
|
71
|
-
this.state.replayBuffer.append({ revision: n.revision, payload: n, receivedAt: now });
|
|
72
|
-
if (n.revision > this.state.lastRevision) {
|
|
73
|
-
this.state.lastRevision = n.revision;
|
|
74
|
-
}
|
|
75
|
-
return "forward";
|
|
76
|
-
}
|
|
77
|
-
}
|
|
78
|
-
|
|
79
|
-
/** Build a {@link ReactivityForwarder} over a collection's {@link PushState} and a verifier. */
|
|
80
|
-
export function createReactivityForwarder(deps: ReactivityForwarderDeps): ReactivityForwarder {
|
|
81
|
-
return new PushStateForwarder(deps);
|
|
82
|
-
}
|
|
83
|
-
|
|
84
|
-
/** The dedupe key a forwarder would compute for a notification (exposed for tests/diagnostics). */
|
|
85
|
-
export function notificationDedupeKey(n: NotificationV1, hash?: IRingHash): string {
|
|
86
|
-
return dedupeKey(n.revision, n.sig, hash);
|
|
87
|
-
}
|
|
1
|
+
/**
|
|
2
|
+
* Reactivity — forwarder receive path (`docs/reactivity.md` §Propagation).
|
|
3
|
+
*
|
|
4
|
+
* A receiving forwarder primary, for each inbound notification:
|
|
5
|
+
* 1. **verifies** the threshold signature against the tail cohort's `MembershipCertV1`;
|
|
6
|
+
* 2. runs the **dedupe** check (sliding `(revision, sigDigest)` window);
|
|
7
|
+
* 3. **appends** to the replay buffer;
|
|
8
|
+
* 4. **forwards the unmodified notification** to its own direct subscribers and child cohorts.
|
|
9
|
+
*
|
|
10
|
+
* This module owns steps 1–3 and the forward *decision* (step 4's transport — dialing each subscriber's
|
|
11
|
+
* primary and each child cohort — is the db-p2p binding). Forwarders never re-sign: a compromised
|
|
12
|
+
* forwarder can drop or delay, but cannot forge. An unverifiable notification is dropped **before**
|
|
13
|
+
* touching the dedupe set or buffer, so a forged payload can neither poison dedupe nor occupy a ring
|
|
14
|
+
* slot. The buffer + dedupe set are gossiped across the cohort by the host's gossip cadence (see
|
|
15
|
+
* {@link PushState}), so any member can serve a replay.
|
|
16
|
+
*/
|
|
17
|
+
|
|
18
|
+
import { dedupeKey, sigDigest } from "./notification.js";
|
|
19
|
+
import type { PushState } from "./push-state.js";
|
|
20
|
+
import type { NotificationV1 } from "./wire.js";
|
|
21
|
+
import type { NotificationVerifier } from "./verify.js";
|
|
22
|
+
import type { IRingHash } from "../cohort-topic/ports.js";
|
|
23
|
+
|
|
24
|
+
/** The forwarder's decision for one inbound notification. */
|
|
25
|
+
export type ForwardDecision =
|
|
26
|
+
/** Verified, fresh: append to buffer and fan out unmodified. */
|
|
27
|
+
| "forward"
|
|
28
|
+
/** Already in the dedupe set: drop silently (an honest retransmit or merge duplicate). */
|
|
29
|
+
| "duplicate"
|
|
30
|
+
/** Signature did not verify against the tail cohort: drop without buffering. */
|
|
31
|
+
| "untrusted";
|
|
32
|
+
|
|
33
|
+
/** Drives the forwarder receive path over one collection's {@link PushState}. */
|
|
34
|
+
export interface ReactivityForwarder {
|
|
35
|
+
/** The per-collection push state this forwarder serves. */
|
|
36
|
+
readonly state: PushState;
|
|
37
|
+
/**
|
|
38
|
+
* Run verify → dedupe → append for one inbound notification, returning the forward decision. The
|
|
39
|
+
* caller fans the *unmodified* notification out only on `"forward"`.
|
|
40
|
+
*/
|
|
41
|
+
receive(n: NotificationV1, now: number): Promise<ForwardDecision>;
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
/** Construction inputs for a {@link ReactivityForwarder}. */
|
|
45
|
+
export interface ReactivityForwarderDeps {
|
|
46
|
+
readonly state: PushState;
|
|
47
|
+
readonly verifier: NotificationVerifier;
|
|
48
|
+
/** Ring hash for the `sigDigest` dedupe key (must match the verifier's). Default db-core SHA-256. */
|
|
49
|
+
readonly hash?: IRingHash;
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
class PushStateForwarder implements ReactivityForwarder {
|
|
53
|
+
readonly state: PushState;
|
|
54
|
+
|
|
55
|
+
constructor(private readonly deps: ReactivityForwarderDeps) {
|
|
56
|
+
this.state = deps.state;
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
async receive(n: NotificationV1, now: number): Promise<ForwardDecision> {
|
|
60
|
+
// 1. Verify end-to-end before any state mutation — a forged notification touches nothing.
|
|
61
|
+
const verdict = await this.deps.verifier.verify(n);
|
|
62
|
+
if (verdict !== "verified") {
|
|
63
|
+
return "untrusted";
|
|
64
|
+
}
|
|
65
|
+
// 2. Dedupe on `(revision, sigDigest)`. Already-seen → drop silently.
|
|
66
|
+
const digest = sigDigest(n.sig, this.deps.hash);
|
|
67
|
+
if (this.state.dedupe.observe(n.revision, digest) === "duplicate") {
|
|
68
|
+
return "duplicate";
|
|
69
|
+
}
|
|
70
|
+
// 3. Append the full signed notification to the replay ring (gossiped across the cohort).
|
|
71
|
+
this.state.replayBuffer.append({ revision: n.revision, payload: n, receivedAt: now });
|
|
72
|
+
if (n.revision > this.state.lastRevision) {
|
|
73
|
+
this.state.lastRevision = n.revision;
|
|
74
|
+
}
|
|
75
|
+
return "forward";
|
|
76
|
+
}
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
/** Build a {@link ReactivityForwarder} over a collection's {@link PushState} and a verifier. */
|
|
80
|
+
export function createReactivityForwarder(deps: ReactivityForwarderDeps): ReactivityForwarder {
|
|
81
|
+
return new PushStateForwarder(deps);
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
/** The dedupe key a forwarder would compute for a notification (exposed for tests/diagnostics). */
|
|
85
|
+
export function notificationDedupeKey(n: NotificationV1, hash?: IRingHash): string {
|
|
86
|
+
return dedupeKey(n.revision, n.sig, hash);
|
|
87
|
+
}
|
package/src/reactivity/index.ts
CHANGED
|
@@ -1,34 +1,34 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* Reactivity — push-based change notifications on the cohort-topic substrate.
|
|
3
|
-
*
|
|
4
|
-
* See `docs/reactivity.md`. This module lands the reactivity hot path — the rotating tail-anchored topic,
|
|
5
|
-
* the subscribe `appPayload`, notification origination (reusing the commit cert unchanged), the forwarder
|
|
6
|
-
* receive path (verify → dedupe → buffer → forward), the `W`-entry replay ring with cohort gossip, the
|
|
7
|
-
* sliding `(revision, sigDigest)` dedupe window, and subscriber-side verify/deliver with gap detection
|
|
8
|
-
* ([reactivity-origination-replay-delivery]) — plus **recovery beyond the live stream**
|
|
9
|
-
* ([reactivity-backfill-resume-checkpoints]): the {@link ./backfill.js} RPC served from the replay ring,
|
|
10
|
-
* the rolling parent {@link ./checkpoint.js} stacked below it, and the four-variant {@link ./resume.js}
|
|
11
|
-
* protocol for mobile wake — plus **tail rotation, slow-subscriber backpressure, and Edge/Core policy**
|
|
12
|
-
* ([reactivity-rotation-backpressure-policy]): the {@link ./rotation.js} lifecycle (pre-announce, drain,
|
|
13
|
-
* jittered re-registration, buffer-to-checkpoint handoff, warm-up), the per-subscriber drop-oldest
|
|
14
|
-
* {@link ./backpressure.js}, and the {@link ./policy.js} Edge-subscriber-only / `delta_max` gate.
|
|
15
|
-
*/
|
|
16
|
-
|
|
17
|
-
export * from "./config.js";
|
|
18
|
-
export * from "./topic-anchor.js";
|
|
19
|
-
export * from "./wire.js";
|
|
20
|
-
export * from "./notification.js";
|
|
21
|
-
export * from "./dedupe.js";
|
|
22
|
-
export * from "./replay-buffer.js";
|
|
23
|
-
export * from "./checkpoint.js";
|
|
24
|
-
export * from "./backpressure.js";
|
|
25
|
-
export * from "./push-state.js";
|
|
26
|
-
export * from "./verify.js";
|
|
27
|
-
export * from "./forwarder.js";
|
|
28
|
-
export * from "./subscriber.js";
|
|
29
|
-
export * from "./subscription.js";
|
|
30
|
-
export * from "./backfill.js";
|
|
31
|
-
export * from "./resume.js";
|
|
32
|
-
export * from "./recover.js";
|
|
33
|
-
export * from "./rotation.js";
|
|
34
|
-
export * from "./policy.js";
|
|
1
|
+
/**
|
|
2
|
+
* Reactivity — push-based change notifications on the cohort-topic substrate.
|
|
3
|
+
*
|
|
4
|
+
* See `docs/reactivity.md`. This module lands the reactivity hot path — the rotating tail-anchored topic,
|
|
5
|
+
* the subscribe `appPayload`, notification origination (reusing the commit cert unchanged), the forwarder
|
|
6
|
+
* receive path (verify → dedupe → buffer → forward), the `W`-entry replay ring with cohort gossip, the
|
|
7
|
+
* sliding `(revision, sigDigest)` dedupe window, and subscriber-side verify/deliver with gap detection
|
|
8
|
+
* ([reactivity-origination-replay-delivery]) — plus **recovery beyond the live stream**
|
|
9
|
+
* ([reactivity-backfill-resume-checkpoints]): the {@link ./backfill.js} RPC served from the replay ring,
|
|
10
|
+
* the rolling parent {@link ./checkpoint.js} stacked below it, and the four-variant {@link ./resume.js}
|
|
11
|
+
* protocol for mobile wake — plus **tail rotation, slow-subscriber backpressure, and Edge/Core policy**
|
|
12
|
+
* ([reactivity-rotation-backpressure-policy]): the {@link ./rotation.js} lifecycle (pre-announce, drain,
|
|
13
|
+
* jittered re-registration, buffer-to-checkpoint handoff, warm-up), the per-subscriber drop-oldest
|
|
14
|
+
* {@link ./backpressure.js}, and the {@link ./policy.js} Edge-subscriber-only / `delta_max` gate.
|
|
15
|
+
*/
|
|
16
|
+
|
|
17
|
+
export * from "./config.js";
|
|
18
|
+
export * from "./topic-anchor.js";
|
|
19
|
+
export * from "./wire.js";
|
|
20
|
+
export * from "./notification.js";
|
|
21
|
+
export * from "./dedupe.js";
|
|
22
|
+
export * from "./replay-buffer.js";
|
|
23
|
+
export * from "./checkpoint.js";
|
|
24
|
+
export * from "./backpressure.js";
|
|
25
|
+
export * from "./push-state.js";
|
|
26
|
+
export * from "./verify.js";
|
|
27
|
+
export * from "./forwarder.js";
|
|
28
|
+
export * from "./subscriber.js";
|
|
29
|
+
export * from "./subscription.js";
|
|
30
|
+
export * from "./backfill.js";
|
|
31
|
+
export * from "./resume.js";
|
|
32
|
+
export * from "./recover.js";
|
|
33
|
+
export * from "./rotation.js";
|
|
34
|
+
export * from "./policy.js";
|
|
@@ -1,123 +1,123 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* Reactivity — notification origination (`docs/reactivity.md` §Notification origination).
|
|
3
|
-
*
|
|
4
|
-
* The tail cohort's primary for a collection is, by construction, the transaction-layer tail-cluster:
|
|
5
|
-
* the cohort-topic primary at `coord_0(_, topicId)` where `topicId = H(tailId ‖ "reactivity")`. As soon
|
|
6
|
-
* as the commit's threshold signature is assembled, it emits a {@link NotificationV1} whose `sig` is
|
|
7
|
-
* **bit-for-bit** the commit certificate's threshold signature — reactivity never re-signs.
|
|
8
|
-
*
|
|
9
|
-
* This module is the pure assembler. The local-change-notifier bridge ([local-change-notifier-bridge])
|
|
10
|
-
* supplies the {@link CollectionChangeEvent} and the pass-through {@link CommitCert}; db-p2p's
|
|
11
|
-
* origination manager calls {@link buildNotificationV1} with those plus the per-emission context
|
|
12
|
-
* (`tailId`, `timestamp`, the per-collection `deltaMaxBytes`) and fans the result out. Crypto-free: it
|
|
13
|
-
* copies `commitCert.thresholdSig` and `commitCert.signers` through unchanged.
|
|
14
|
-
*/
|
|
15
|
-
|
|
16
|
-
import { createRingHash } from "../cohort-topic/ring-hash.js";
|
|
17
|
-
import { bytesToB64url } from "../cohort-topic/wire/codec.js";
|
|
18
|
-
import type { IRingHash } from "../cohort-topic/ports.js";
|
|
19
|
-
import type { CollectionChangeEvent, CommitCert } from "../transactor/change-notifier.js";
|
|
20
|
-
import type { NotificationV1, RotationHintV1 } from "./wire.js";
|
|
21
|
-
|
|
22
|
-
const utf8 = new TextEncoder();
|
|
23
|
-
|
|
24
|
-
/** Per-emission context the origination point supplies alongside the bridge's event + cert. */
|
|
25
|
-
export interface OriginationContext {
|
|
26
|
-
/** Current tail block id (base64url) the reactivity topic is anchored on. */
|
|
27
|
-
readonly tailId: string;
|
|
28
|
-
/** Emission timestamp (unix ms). */
|
|
29
|
-
readonly timestamp: number;
|
|
30
|
-
/**
|
|
31
|
-
* Per-collection delta budget (bytes). `0` ⇒ omit `delta` entirely (Edge profile, or a collection
|
|
32
|
-
* that declines deltas). Origination MUST respect a `deltaMaxBytes` of `0` by omitting the field.
|
|
33
|
-
*/
|
|
34
|
-
readonly deltaMaxBytes: number;
|
|
35
|
-
/** Optional bounded delta (raw bytes); included only when within `deltaMaxBytes` and `> 0`. */
|
|
36
|
-
readonly delta?: Uint8Array;
|
|
37
|
-
/** Tail-rotation pre-announce to embed (rotation ticket supplies it). */
|
|
38
|
-
readonly rotationHint?: RotationHintV1;
|
|
39
|
-
/**
|
|
40
|
-
* Map a {@link CommitCert} signer (the cluster's peer-id string keying its commit vote) to the
|
|
41
|
-
* base64url cohort member-id bytes the subscriber's membership verifier compares against. Default:
|
|
42
|
-
* identity (the signer is already base64url) — db-p2p supplies `s ⇒ bytesToB64url(peerIdToBytes(s))`.
|
|
43
|
-
*/
|
|
44
|
-
readonly encodeSigner?: (signer: string) => string;
|
|
45
|
-
}
|
|
46
|
-
|
|
47
|
-
/**
|
|
48
|
-
* Assemble the {@link NotificationV1} for one committed change, reusing the commit cert's threshold
|
|
49
|
-
* signature unchanged.
|
|
50
|
-
*
|
|
51
|
-
* `digest` carries the commit-vote **signed payload** the threshold signature was computed over —
|
|
52
|
-
* `commitCert.signedPayload`, the cluster's per-member `utf8(commitHash + ":approve")` byte image,
|
|
53
|
-
* identical across all approving signers. Because the verifier recomputes the cohort threshold check over
|
|
54
|
-
* exactly these bytes (`payload = b64urlToBytes(n.digest)`), a subscriber's **real** Ed25519
|
|
55
|
-
* threshold-verify over `digest` reproduces the signed image and succeeds — the cluster↔reactivity
|
|
56
|
-
* integration seam is **closed** (no pass-crypto stub required). The hint-only contract still holds for
|
|
57
|
-
* the optional `delta`: deltas are advisory; the digest + threshold signature are the authoritative proof.
|
|
58
|
-
*/
|
|
59
|
-
export function buildNotificationV1(event: CollectionChangeEvent, commitCert: CommitCert, ctx: OriginationContext): NotificationV1 {
|
|
60
|
-
const encodeSigner = ctx.encodeSigner ?? ((s: string): string => s);
|
|
61
|
-
const notification: NotificationV1 = {
|
|
62
|
-
v: 1,
|
|
63
|
-
collectionId: event.collectionId,
|
|
64
|
-
tailId: ctx.tailId,
|
|
65
|
-
revision: event.rev,
|
|
66
|
-
digest: bytesToB64url(commitCert.signedPayload),
|
|
67
|
-
timestamp: ctx.timestamp,
|
|
68
|
-
sig: bytesToB64url(commitCert.thresholdSig),
|
|
69
|
-
signers: commitCert.signers.map(encodeSigner),
|
|
70
|
-
};
|
|
71
|
-
if (ctx.deltaMaxBytes > 0 && ctx.delta !== undefined && ctx.delta.length <= ctx.deltaMaxBytes) {
|
|
72
|
-
notification.delta = bytesToB64url(ctx.delta);
|
|
73
|
-
}
|
|
74
|
-
if (ctx.rotationHint !== undefined) {
|
|
75
|
-
notification.rotationHint = ctx.rotationHint;
|
|
76
|
-
}
|
|
77
|
-
// An invalidation change event carries a typed marker the subscriber uses to distinguish a reversal
|
|
78
|
-
// from an ordinary commit (drop derived results + resubmit, vs. refresh). It rides the same path and
|
|
79
|
-
// reuses the invalidation's commit cert as `sig` — see {@link CollectionChangeEvent.invalidation}.
|
|
80
|
-
if (event.invalidation) {
|
|
81
|
-
notification.invalidation = true;
|
|
82
|
-
if (event.invalidatedActionId !== undefined) {
|
|
83
|
-
notification.invalidatedActionId = event.invalidatedActionId;
|
|
84
|
-
}
|
|
85
|
-
}
|
|
86
|
-
return notification;
|
|
87
|
-
}
|
|
88
|
-
|
|
89
|
-
/**
|
|
90
|
-
* Compact, stable digest of a notification's signature for the dedupe key `(revision, sigDigest)`
|
|
91
|
-
* (`docs/reactivity.md` §Per-revision dedupe). Hashing the signature bytes keeps the key bounded and
|
|
92
|
-
* distinguishes two distinct threshold sigs that legitimately share a revision during partition merge.
|
|
93
|
-
*/
|
|
94
|
-
export function sigDigest(sig: string, hash: IRingHash = createRingHash()): string {
|
|
95
|
-
return bytesToB64url(hash.H(utf8.encode(sig)));
|
|
96
|
-
}
|
|
97
|
-
|
|
98
|
-
/** The dedupe-set key for a notification: `${revision}:${sigDigest(sig)}`. */
|
|
99
|
-
export function dedupeKey(revision: number, sig: string, hash?: IRingHash): string {
|
|
100
|
-
return `${revision}:${sigDigest(sig, hash)}`;
|
|
101
|
-
}
|
|
102
|
-
|
|
103
|
-
/**
|
|
104
|
-
* Collapse a batch of inbound notifications to the distinct **invalidated action ids** a client must act
|
|
105
|
-
* on, in first-seen order. A single dispute can fan out several invalidation notifications — one per
|
|
106
|
-
* cascade child (`docs/right-is-right.md` §Read-Dependency Cascade) — and a client holding dependents of
|
|
107
|
-
* several may receive several; coalescing by `invalidatedActionId` lets it drop + resubmit each affected
|
|
108
|
-
* unit exactly once instead of once per notification. Notifications that are not invalidations (ordinary
|
|
109
|
-
* commits) and invalidations missing an `invalidatedActionId` are ignored here — the former drive a plain
|
|
110
|
-
* refresh, the latter still surface the reverted state on the next authoritative read. A hint-layer helper:
|
|
111
|
-
* the client still verifies against committed state.
|
|
112
|
-
*/
|
|
113
|
-
export function coalesceInvalidatedActionIds(notifications: Iterable<NotificationV1>): string[] {
|
|
114
|
-
const seen = new Set<string>();
|
|
115
|
-
const ordered: string[] = [];
|
|
116
|
-
for (const n of notifications) {
|
|
117
|
-
if (n.invalidation && n.invalidatedActionId !== undefined && !seen.has(n.invalidatedActionId)) {
|
|
118
|
-
seen.add(n.invalidatedActionId);
|
|
119
|
-
ordered.push(n.invalidatedActionId);
|
|
120
|
-
}
|
|
121
|
-
}
|
|
122
|
-
return ordered;
|
|
123
|
-
}
|
|
1
|
+
/**
|
|
2
|
+
* Reactivity — notification origination (`docs/reactivity.md` §Notification origination).
|
|
3
|
+
*
|
|
4
|
+
* The tail cohort's primary for a collection is, by construction, the transaction-layer tail-cluster:
|
|
5
|
+
* the cohort-topic primary at `coord_0(_, topicId)` where `topicId = H(tailId ‖ "reactivity")`. As soon
|
|
6
|
+
* as the commit's threshold signature is assembled, it emits a {@link NotificationV1} whose `sig` is
|
|
7
|
+
* **bit-for-bit** the commit certificate's threshold signature — reactivity never re-signs.
|
|
8
|
+
*
|
|
9
|
+
* This module is the pure assembler. The local-change-notifier bridge ([local-change-notifier-bridge])
|
|
10
|
+
* supplies the {@link CollectionChangeEvent} and the pass-through {@link CommitCert}; db-p2p's
|
|
11
|
+
* origination manager calls {@link buildNotificationV1} with those plus the per-emission context
|
|
12
|
+
* (`tailId`, `timestamp`, the per-collection `deltaMaxBytes`) and fans the result out. Crypto-free: it
|
|
13
|
+
* copies `commitCert.thresholdSig` and `commitCert.signers` through unchanged.
|
|
14
|
+
*/
|
|
15
|
+
|
|
16
|
+
import { createRingHash } from "../cohort-topic/ring-hash.js";
|
|
17
|
+
import { bytesToB64url } from "../cohort-topic/wire/codec.js";
|
|
18
|
+
import type { IRingHash } from "../cohort-topic/ports.js";
|
|
19
|
+
import type { CollectionChangeEvent, CommitCert } from "../transactor/change-notifier.js";
|
|
20
|
+
import type { NotificationV1, RotationHintV1 } from "./wire.js";
|
|
21
|
+
|
|
22
|
+
const utf8 = new TextEncoder();
|
|
23
|
+
|
|
24
|
+
/** Per-emission context the origination point supplies alongside the bridge's event + cert. */
|
|
25
|
+
export interface OriginationContext {
|
|
26
|
+
/** Current tail block id (base64url) the reactivity topic is anchored on. */
|
|
27
|
+
readonly tailId: string;
|
|
28
|
+
/** Emission timestamp (unix ms). */
|
|
29
|
+
readonly timestamp: number;
|
|
30
|
+
/**
|
|
31
|
+
* Per-collection delta budget (bytes). `0` ⇒ omit `delta` entirely (Edge profile, or a collection
|
|
32
|
+
* that declines deltas). Origination MUST respect a `deltaMaxBytes` of `0` by omitting the field.
|
|
33
|
+
*/
|
|
34
|
+
readonly deltaMaxBytes: number;
|
|
35
|
+
/** Optional bounded delta (raw bytes); included only when within `deltaMaxBytes` and `> 0`. */
|
|
36
|
+
readonly delta?: Uint8Array;
|
|
37
|
+
/** Tail-rotation pre-announce to embed (rotation ticket supplies it). */
|
|
38
|
+
readonly rotationHint?: RotationHintV1;
|
|
39
|
+
/**
|
|
40
|
+
* Map a {@link CommitCert} signer (the cluster's peer-id string keying its commit vote) to the
|
|
41
|
+
* base64url cohort member-id bytes the subscriber's membership verifier compares against. Default:
|
|
42
|
+
* identity (the signer is already base64url) — db-p2p supplies `s ⇒ bytesToB64url(peerIdToBytes(s))`.
|
|
43
|
+
*/
|
|
44
|
+
readonly encodeSigner?: (signer: string) => string;
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
/**
|
|
48
|
+
* Assemble the {@link NotificationV1} for one committed change, reusing the commit cert's threshold
|
|
49
|
+
* signature unchanged.
|
|
50
|
+
*
|
|
51
|
+
* `digest` carries the commit-vote **signed payload** the threshold signature was computed over —
|
|
52
|
+
* `commitCert.signedPayload`, the cluster's per-member `utf8(commitHash + ":approve")` byte image,
|
|
53
|
+
* identical across all approving signers. Because the verifier recomputes the cohort threshold check over
|
|
54
|
+
* exactly these bytes (`payload = b64urlToBytes(n.digest)`), a subscriber's **real** Ed25519
|
|
55
|
+
* threshold-verify over `digest` reproduces the signed image and succeeds — the cluster↔reactivity
|
|
56
|
+
* integration seam is **closed** (no pass-crypto stub required). The hint-only contract still holds for
|
|
57
|
+
* the optional `delta`: deltas are advisory; the digest + threshold signature are the authoritative proof.
|
|
58
|
+
*/
|
|
59
|
+
export function buildNotificationV1(event: CollectionChangeEvent, commitCert: CommitCert, ctx: OriginationContext): NotificationV1 {
|
|
60
|
+
const encodeSigner = ctx.encodeSigner ?? ((s: string): string => s);
|
|
61
|
+
const notification: NotificationV1 = {
|
|
62
|
+
v: 1,
|
|
63
|
+
collectionId: event.collectionId,
|
|
64
|
+
tailId: ctx.tailId,
|
|
65
|
+
revision: event.rev,
|
|
66
|
+
digest: bytesToB64url(commitCert.signedPayload),
|
|
67
|
+
timestamp: ctx.timestamp,
|
|
68
|
+
sig: bytesToB64url(commitCert.thresholdSig),
|
|
69
|
+
signers: commitCert.signers.map(encodeSigner),
|
|
70
|
+
};
|
|
71
|
+
if (ctx.deltaMaxBytes > 0 && ctx.delta !== undefined && ctx.delta.length <= ctx.deltaMaxBytes) {
|
|
72
|
+
notification.delta = bytesToB64url(ctx.delta);
|
|
73
|
+
}
|
|
74
|
+
if (ctx.rotationHint !== undefined) {
|
|
75
|
+
notification.rotationHint = ctx.rotationHint;
|
|
76
|
+
}
|
|
77
|
+
// An invalidation change event carries a typed marker the subscriber uses to distinguish a reversal
|
|
78
|
+
// from an ordinary commit (drop derived results + resubmit, vs. refresh). It rides the same path and
|
|
79
|
+
// reuses the invalidation's commit cert as `sig` — see {@link CollectionChangeEvent.invalidation}.
|
|
80
|
+
if (event.invalidation) {
|
|
81
|
+
notification.invalidation = true;
|
|
82
|
+
if (event.invalidatedActionId !== undefined) {
|
|
83
|
+
notification.invalidatedActionId = event.invalidatedActionId;
|
|
84
|
+
}
|
|
85
|
+
}
|
|
86
|
+
return notification;
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
/**
|
|
90
|
+
* Compact, stable digest of a notification's signature for the dedupe key `(revision, sigDigest)`
|
|
91
|
+
* (`docs/reactivity.md` §Per-revision dedupe). Hashing the signature bytes keeps the key bounded and
|
|
92
|
+
* distinguishes two distinct threshold sigs that legitimately share a revision during partition merge.
|
|
93
|
+
*/
|
|
94
|
+
export function sigDigest(sig: string, hash: IRingHash = createRingHash()): string {
|
|
95
|
+
return bytesToB64url(hash.H(utf8.encode(sig)));
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
/** The dedupe-set key for a notification: `${revision}:${sigDigest(sig)}`. */
|
|
99
|
+
export function dedupeKey(revision: number, sig: string, hash?: IRingHash): string {
|
|
100
|
+
return `${revision}:${sigDigest(sig, hash)}`;
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
/**
|
|
104
|
+
* Collapse a batch of inbound notifications to the distinct **invalidated action ids** a client must act
|
|
105
|
+
* on, in first-seen order. A single dispute can fan out several invalidation notifications — one per
|
|
106
|
+
* cascade child (`docs/right-is-right.md` §Read-Dependency Cascade) — and a client holding dependents of
|
|
107
|
+
* several may receive several; coalescing by `invalidatedActionId` lets it drop + resubmit each affected
|
|
108
|
+
* unit exactly once instead of once per notification. Notifications that are not invalidations (ordinary
|
|
109
|
+
* commits) and invalidations missing an `invalidatedActionId` are ignored here — the former drive a plain
|
|
110
|
+
* refresh, the latter still surface the reverted state on the next authoritative read. A hint-layer helper:
|
|
111
|
+
* the client still verifies against committed state.
|
|
112
|
+
*/
|
|
113
|
+
export function coalesceInvalidatedActionIds(notifications: Iterable<NotificationV1>): string[] {
|
|
114
|
+
const seen = new Set<string>();
|
|
115
|
+
const ordered: string[] = [];
|
|
116
|
+
for (const n of notifications) {
|
|
117
|
+
if (n.invalidation && n.invalidatedActionId !== undefined && !seen.has(n.invalidatedActionId)) {
|
|
118
|
+
seen.add(n.invalidatedActionId);
|
|
119
|
+
ordered.push(n.invalidatedActionId);
|
|
120
|
+
}
|
|
121
|
+
}
|
|
122
|
+
return ordered;
|
|
123
|
+
}
|