@optimystic/db-core 0.22.0 → 0.24.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (141) hide show
  1. package/README.md +336 -336
  2. package/dist/src/cluster/structs.d.ts +39 -1
  3. package/dist/src/cluster/structs.d.ts.map +1 -1
  4. package/dist/src/cluster/structs.js +24 -0
  5. package/dist/src/cluster/structs.js.map +1 -1
  6. package/dist/src/collection/collection.d.ts +17 -0
  7. package/dist/src/collection/collection.d.ts.map +1 -1
  8. package/dist/src/collection/collection.js +24 -2
  9. package/dist/src/collection/collection.js.map +1 -1
  10. package/dist/src/collections/tree/tree.d.ts +5 -0
  11. package/dist/src/collections/tree/tree.d.ts.map +1 -1
  12. package/dist/src/collections/tree/tree.js +7 -0
  13. package/dist/src/collections/tree/tree.js.map +1 -1
  14. package/dist/src/network/i-peer-network.d.ts +16 -0
  15. package/dist/src/network/i-peer-network.d.ts.map +1 -1
  16. package/dist/src/network/struct.d.ts +39 -2
  17. package/dist/src/network/struct.d.ts.map +1 -1
  18. package/dist/src/network/struct.js +18 -0
  19. package/dist/src/network/struct.js.map +1 -1
  20. package/dist/src/testing/test-transactor.d.ts +95 -8
  21. package/dist/src/testing/test-transactor.d.ts.map +1 -1
  22. package/dist/src/testing/test-transactor.js +121 -8
  23. package/dist/src/testing/test-transactor.js.map +1 -1
  24. package/dist/src/transaction/transaction.d.ts +1 -1
  25. package/dist/src/transaction/transaction.js +1 -1
  26. package/dist/src/transactor/network-transactor.d.ts.map +1 -1
  27. package/dist/src/transactor/network-transactor.js +48 -13
  28. package/dist/src/transactor/network-transactor.js.map +1 -1
  29. package/dist/src/transactor/transactor-source.d.ts.map +1 -1
  30. package/dist/src/transactor/transactor-source.js +25 -2
  31. package/dist/src/transactor/transactor-source.js.map +1 -1
  32. package/package.json +1 -1
  33. package/src/cluster/membership.ts +85 -85
  34. package/src/cluster/structs.ts +43 -4
  35. package/src/cohort-topic/addressing.ts +120 -120
  36. package/src/cohort-topic/antidos/bootstrap-evidence-envelope.ts +253 -253
  37. package/src/cohort-topic/antidos/bootstrap-evidence.ts +106 -106
  38. package/src/cohort-topic/antidos/index.ts +5 -5
  39. package/src/cohort-topic/antidos/rate-limiter.ts +210 -210
  40. package/src/cohort-topic/antidos/replay-guard.ts +146 -146
  41. package/src/cohort-topic/antidos/topic-budget.ts +160 -160
  42. package/src/cohort-topic/antiflood/index.ts +2 -2
  43. package/src/cohort-topic/antiflood/invariants.ts +108 -108
  44. package/src/cohort-topic/antiflood/jitter.ts +117 -117
  45. package/src/cohort-topic/coldstart.ts +237 -237
  46. package/src/cohort-topic/dmax.ts +88 -88
  47. package/src/cohort-topic/gossip/bus.ts +254 -254
  48. package/src/cohort-topic/gossip/index.ts +3 -3
  49. package/src/cohort-topic/gossip/records.ts +45 -45
  50. package/src/cohort-topic/gossip/view.ts +91 -91
  51. package/src/cohort-topic/index.ts +20 -20
  52. package/src/cohort-topic/load/barometer.ts +134 -134
  53. package/src/cohort-topic/load/index.ts +1 -1
  54. package/src/cohort-topic/member-engine.ts +430 -430
  55. package/src/cohort-topic/membership/index.ts +3 -3
  56. package/src/cohort-topic/membership/publisher.ts +163 -163
  57. package/src/cohort-topic/membership/source.ts +41 -41
  58. package/src/cohort-topic/membership/verifier.ts +461 -461
  59. package/src/cohort-topic/ports.ts +157 -157
  60. package/src/cohort-topic/promotion.ts +405 -405
  61. package/src/cohort-topic/registration/bytes.ts +37 -37
  62. package/src/cohort-topic/registration/handoff.ts +154 -154
  63. package/src/cohort-topic/registration/index.ts +6 -6
  64. package/src/cohort-topic/registration/renewal.ts +495 -495
  65. package/src/cohort-topic/registration/sharding.ts +61 -61
  66. package/src/cohort-topic/registration/store.ts +81 -81
  67. package/src/cohort-topic/registration/types.ts +91 -91
  68. package/src/cohort-topic/ring-hash.ts +50 -50
  69. package/src/cohort-topic/service.ts +416 -416
  70. package/src/cohort-topic/sig/index.ts +2 -2
  71. package/src/cohort-topic/sig/payloads.ts +59 -59
  72. package/src/cohort-topic/sig/threshold.ts +64 -64
  73. package/src/cohort-topic/tiers.ts +74 -74
  74. package/src/cohort-topic/traffic.ts +233 -233
  75. package/src/cohort-topic/walk.ts +326 -326
  76. package/src/cohort-topic/willingness.ts +237 -237
  77. package/src/cohort-topic/wire/codec.ts +216 -216
  78. package/src/cohort-topic/wire/index.ts +18 -18
  79. package/src/cohort-topic/wire/payloads.ts +126 -126
  80. package/src/cohort-topic/wire/primitives.ts +188 -188
  81. package/src/cohort-topic/wire/types.ts +475 -475
  82. package/src/cohort-topic/wire/validate.ts +512 -512
  83. package/src/collection/collection-type-registry.ts +37 -37
  84. package/src/collection/collection.ts +25 -2
  85. package/src/collections/diary/diary.ts +68 -68
  86. package/src/collections/tree/readme.md +4 -0
  87. package/src/collections/tree/tree.ts +320 -312
  88. package/src/matchmaking/capability-filter.ts +45 -45
  89. package/src/matchmaking/config.ts +98 -98
  90. package/src/matchmaking/index.ts +21 -21
  91. package/src/matchmaking/multi-cohort-seeker.ts +234 -234
  92. package/src/matchmaking/provider.ts +123 -123
  93. package/src/matchmaking/query-eval.ts +105 -105
  94. package/src/matchmaking/seeker-walk.ts +127 -127
  95. package/src/matchmaking/seeker.ts +86 -86
  96. package/src/matchmaking/topic-anchor.ts +90 -90
  97. package/src/matchmaking/voting-quorum.ts +394 -394
  98. package/src/matchmaking/wire.ts +603 -603
  99. package/src/network/i-peer-network.ts +17 -0
  100. package/src/network/stale-failure.ts +43 -43
  101. package/src/network/struct.ts +41 -2
  102. package/src/network/types.ts +37 -37
  103. package/src/reactivity/backfill.ts +220 -220
  104. package/src/reactivity/backpressure.ts +191 -191
  105. package/src/reactivity/checkpoint.ts +308 -308
  106. package/src/reactivity/config.ts +172 -172
  107. package/src/reactivity/dedupe.ts +132 -132
  108. package/src/reactivity/forwarder.ts +87 -87
  109. package/src/reactivity/index.ts +34 -34
  110. package/src/reactivity/notification.ts +123 -123
  111. package/src/reactivity/policy.ts +79 -79
  112. package/src/reactivity/push-state.ts +310 -310
  113. package/src/reactivity/recover.ts +153 -153
  114. package/src/reactivity/replay-buffer.ts +141 -141
  115. package/src/reactivity/resume.ts +549 -549
  116. package/src/reactivity/rotation.ts +415 -415
  117. package/src/reactivity/subscriber.ts +132 -132
  118. package/src/reactivity/subscription.ts +66 -66
  119. package/src/reactivity/topic-anchor.ts +71 -71
  120. package/src/reactivity/verify.ts +73 -73
  121. package/src/reactivity/wire-validate.ts +13 -13
  122. package/src/reactivity/wire.ts +224 -224
  123. package/src/testing/async-wait.ts +65 -65
  124. package/src/testing/index.ts +2 -2
  125. package/src/testing/test-transactor.ts +638 -502
  126. package/src/transaction/errors.ts +91 -91
  127. package/src/transaction/operations-hash.ts +196 -196
  128. package/src/transaction/read-dependency-collector.ts +78 -78
  129. package/src/transaction/transaction.ts +1 -1
  130. package/src/transactor/change-notifier.ts +80 -80
  131. package/src/transactor/index.ts +5 -5
  132. package/src/transactor/network-transactor.ts +49 -14
  133. package/src/transactor/transactor-source.ts +25 -2
  134. package/src/transform/atomic-proxy.ts +92 -92
  135. package/src/transform/helpers.ts +159 -159
  136. package/src/utility/backoff.ts +95 -95
  137. package/src/utility/batch-coordinator.ts +191 -191
  138. package/dist/src/transaction/context.d.ts +0 -60
  139. package/dist/src/transaction/context.d.ts.map +0 -1
  140. package/dist/src/transaction/context.js +0 -91
  141. package/dist/src/transaction/context.js.map +0 -1
@@ -1,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
+ }
@@ -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
+ }