@optimystic/db-p2p 0.21.0 → 0.24.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (219) hide show
  1. package/dist/src/cluster/client.d.ts +10 -0
  2. package/dist/src/cluster/client.d.ts.map +1 -1
  3. package/dist/src/cluster/client.js +30 -1
  4. package/dist/src/cluster/client.js.map +1 -1
  5. package/dist/src/cluster/cluster-policy.d.ts +13 -2
  6. package/dist/src/cluster/cluster-policy.d.ts.map +1 -1
  7. package/dist/src/cluster/cluster-policy.js +51 -4
  8. package/dist/src/cluster/cluster-policy.js.map +1 -1
  9. package/dist/src/cluster/cluster-repo.d.ts +42 -17
  10. package/dist/src/cluster/cluster-repo.d.ts.map +1 -1
  11. package/dist/src/cluster/cluster-repo.js +229 -122
  12. package/dist/src/cluster/cluster-repo.js.map +1 -1
  13. package/dist/src/cluster/cluster-size-coupling.d.ts +28 -0
  14. package/dist/src/cluster/cluster-size-coupling.d.ts.map +1 -0
  15. package/dist/src/cluster/cluster-size-coupling.js +35 -0
  16. package/dist/src/cluster/cluster-size-coupling.js.map +1 -0
  17. package/dist/src/cluster/quorum-restore.d.ts +6 -0
  18. package/dist/src/cluster/quorum-restore.d.ts.map +1 -1
  19. package/dist/src/cluster/quorum-restore.js +1 -1
  20. package/dist/src/cluster/quorum-restore.js.map +1 -1
  21. package/dist/src/cluster/reconcile-block.d.ts.map +1 -1
  22. package/dist/src/cluster/reconcile-block.js +15 -3
  23. package/dist/src/cluster/reconcile-block.js.map +1 -1
  24. package/dist/src/cluster/service.d.ts +32 -1
  25. package/dist/src/cluster/service.d.ts.map +1 -1
  26. package/dist/src/cluster/service.js +43 -2
  27. package/dist/src/cluster/service.js.map +1 -1
  28. package/dist/src/cohort-topic/stream-util.d.ts +22 -6
  29. package/dist/src/cohort-topic/stream-util.d.ts.map +1 -1
  30. package/dist/src/cohort-topic/stream-util.js +56 -10
  31. package/dist/src/cohort-topic/stream-util.js.map +1 -1
  32. package/dist/src/dispute/dispute-service.d.ts.map +1 -1
  33. package/dist/src/dispute/dispute-service.js +9 -3
  34. package/dist/src/dispute/dispute-service.js.map +1 -1
  35. package/dist/src/index.d.ts +5 -0
  36. package/dist/src/index.d.ts.map +1 -1
  37. package/dist/src/index.js +5 -0
  38. package/dist/src/index.js.map +1 -1
  39. package/dist/src/libp2p-key-network.d.ts +134 -7
  40. package/dist/src/libp2p-key-network.d.ts.map +1 -1
  41. package/dist/src/libp2p-key-network.js +174 -37
  42. package/dist/src/libp2p-key-network.js.map +1 -1
  43. package/dist/src/libp2p-node-base.d.ts +3 -2
  44. package/dist/src/libp2p-node-base.d.ts.map +1 -1
  45. package/dist/src/libp2p-node-base.js +859 -778
  46. package/dist/src/libp2p-node-base.js.map +1 -1
  47. package/dist/src/libp2p-node-rn.d.ts +2 -2
  48. package/dist/src/libp2p-node-rn.d.ts.map +1 -1
  49. package/dist/src/libp2p-node-rn.js.map +1 -1
  50. package/dist/src/libp2p-node.d.ts +2 -2
  51. package/dist/src/libp2p-node.d.ts.map +1 -1
  52. package/dist/src/libp2p-node.js.map +1 -1
  53. package/dist/src/logger.d.ts +17 -1
  54. package/dist/src/logger.d.ts.map +1 -1
  55. package/dist/src/logger.js +19 -2
  56. package/dist/src/logger.js.map +1 -1
  57. package/dist/src/network/network-manager-service.d.ts +2 -0
  58. package/dist/src/network/network-manager-service.d.ts.map +1 -1
  59. package/dist/src/network/network-manager-service.js +4 -0
  60. package/dist/src/network/network-manager-service.js.map +1 -1
  61. package/dist/src/optimystic-node.d.ts +35 -0
  62. package/dist/src/optimystic-node.d.ts.map +1 -0
  63. package/dist/src/optimystic-node.js +2 -0
  64. package/dist/src/optimystic-node.js.map +1 -0
  65. package/dist/src/owned-block-seed.d.ts +6 -3
  66. package/dist/src/owned-block-seed.d.ts.map +1 -1
  67. package/dist/src/owned-block-seed.js +16 -3
  68. package/dist/src/owned-block-seed.js.map +1 -1
  69. package/dist/src/peer-address-book.d.ts +72 -0
  70. package/dist/src/peer-address-book.d.ts.map +1 -0
  71. package/dist/src/peer-address-book.js +123 -0
  72. package/dist/src/peer-address-book.js.map +1 -0
  73. package/dist/src/repo/client.d.ts.map +1 -1
  74. package/dist/src/repo/client.js +11 -2
  75. package/dist/src/repo/client.js.map +1 -1
  76. package/dist/src/repo/cluster-coordinator.d.ts +30 -0
  77. package/dist/src/repo/cluster-coordinator.d.ts.map +1 -1
  78. package/dist/src/repo/cluster-coordinator.js +95 -3
  79. package/dist/src/repo/cluster-coordinator.js.map +1 -1
  80. package/dist/src/repo/coordinator-repo.d.ts +78 -14
  81. package/dist/src/repo/coordinator-repo.d.ts.map +1 -1
  82. package/dist/src/repo/coordinator-repo.js +266 -81
  83. package/dist/src/repo/coordinator-repo.js.map +1 -1
  84. package/dist/src/rn.d.ts +5 -0
  85. package/dist/src/rn.d.ts.map +1 -1
  86. package/dist/src/rn.js +5 -0
  87. package/dist/src/rn.js.map +1 -1
  88. package/dist/src/storage/block-storage.d.ts.map +1 -1
  89. package/dist/src/storage/block-storage.js +57 -5
  90. package/dist/src/storage/block-storage.js.map +1 -1
  91. package/dist/src/storage/cached-raw-storage.d.ts +83 -0
  92. package/dist/src/storage/cached-raw-storage.d.ts.map +1 -0
  93. package/dist/src/storage/cached-raw-storage.js +152 -0
  94. package/dist/src/storage/cached-raw-storage.js.map +1 -0
  95. package/dist/src/storage/cached-store-driver.d.ts +186 -0
  96. package/dist/src/storage/cached-store-driver.d.ts.map +1 -0
  97. package/dist/src/storage/cached-store-driver.js +775 -0
  98. package/dist/src/storage/cached-store-driver.js.map +1 -0
  99. package/dist/src/storage/i-block-storage.d.ts +20 -1
  100. package/dist/src/storage/i-block-storage.d.ts.map +1 -1
  101. package/dist/src/storage/i-raw-storage.d.ts +12 -5
  102. package/dist/src/storage/i-raw-storage.d.ts.map +1 -1
  103. package/dist/src/storage/shared-cache-pool.d.ts +234 -0
  104. package/dist/src/storage/shared-cache-pool.d.ts.map +1 -0
  105. package/dist/src/storage/shared-cache-pool.js +354 -0
  106. package/dist/src/storage/shared-cache-pool.js.map +1 -0
  107. package/dist/src/storage/storage-repo.d.ts +56 -3
  108. package/dist/src/storage/storage-repo.d.ts.map +1 -1
  109. package/dist/src/storage/storage-repo.js +124 -18
  110. package/dist/src/storage/storage-repo.js.map +1 -1
  111. package/dist/src/testing/raw-storage-conformance.d.ts +2 -1
  112. package/dist/src/testing/raw-storage-conformance.d.ts.map +1 -1
  113. package/dist/src/testing/raw-storage-conformance.js +52 -2
  114. package/dist/src/testing/raw-storage-conformance.js.map +1 -1
  115. package/package.json +3 -3
  116. package/readme.md +668 -653
  117. package/src/cluster/block-transfer.ts +424 -424
  118. package/src/cluster/client.ts +119 -88
  119. package/src/cluster/cluster-error.ts +64 -64
  120. package/src/cluster/cluster-policy.ts +203 -152
  121. package/src/cluster/cluster-repo.ts +245 -125
  122. package/src/cluster/cluster-size-coupling.ts +45 -0
  123. package/src/cluster/commit-cert.ts +139 -139
  124. package/src/cluster/i-transaction-state-store.ts +43 -43
  125. package/src/cluster/memory-transaction-state-store.ts +56 -56
  126. package/src/cluster/peer-key-binding.ts +37 -37
  127. package/src/cluster/persistent-transaction-state-store.ts +92 -92
  128. package/src/cluster/quorum-restore.ts +223 -223
  129. package/src/cluster/reconcile-block.ts +203 -191
  130. package/src/cluster/service.ts +293 -241
  131. package/src/cluster/supermajority-coupling.ts +37 -37
  132. package/src/cohort-topic/bootstrap-evidence-builder.ts +122 -122
  133. package/src/cohort-topic/bootstrap-evidence-verifiers.ts +132 -132
  134. package/src/cohort-topic/bootstrap-parent-reference.ts +159 -159
  135. package/src/cohort-topic/change-bridge.ts +109 -109
  136. package/src/cohort-topic/cohort-gossip-driver.ts +231 -231
  137. package/src/cohort-topic/cohort-gossip-transport.ts +84 -84
  138. package/src/cohort-topic/fret-trust-anchor.ts +153 -153
  139. package/src/cohort-topic/host.ts +2901 -2901
  140. package/src/cohort-topic/index.ts +13 -13
  141. package/src/cohort-topic/membership-publish-sink.ts +20 -20
  142. package/src/cohort-topic/membership-source.ts +68 -68
  143. package/src/cohort-topic/peer-codec.ts +31 -31
  144. package/src/cohort-topic/peer-sig.ts +86 -86
  145. package/src/cohort-topic/protocols.ts +71 -71
  146. package/src/cohort-topic/reactivity-membership-gate.ts +77 -77
  147. package/src/cohort-topic/size-estimator.ts +16 -16
  148. package/src/cohort-topic/stream-util.ts +135 -87
  149. package/src/cohort-topic/threshold-crypto.ts +239 -239
  150. package/src/cohort-topic/topic-router.ts +77 -77
  151. package/src/dispute/arbitrator-selection.ts +138 -138
  152. package/src/dispute/cascade.ts +524 -524
  153. package/src/dispute/dispute-service.ts +11 -5
  154. package/src/dispute/invalidation.ts +625 -625
  155. package/src/inbound-authorization.ts +190 -190
  156. package/src/index.ts +52 -47
  157. package/src/libp2p-key-network.ts +1120 -958
  158. package/src/libp2p-node-base.ts +1675 -1591
  159. package/src/libp2p-node-rn.ts +30 -30
  160. package/src/libp2p-node.ts +36 -36
  161. package/src/logger.ts +19 -2
  162. package/src/matchmaking/aggregate-counts.ts +104 -104
  163. package/src/matchmaking/index.ts +20 -20
  164. package/src/matchmaking/module.ts +363 -363
  165. package/src/matchmaking/protocols.ts +51 -51
  166. package/src/matchmaking/provider-manager.ts +95 -95
  167. package/src/matchmaking/query-handler.ts +88 -88
  168. package/src/matchmaking/query-transport.ts +492 -492
  169. package/src/matchmaking/seeker-manager.ts +64 -64
  170. package/src/matchmaking/seeker-walk-client.ts +293 -293
  171. package/src/matchmaking/traffic-validation.ts +195 -195
  172. package/src/network/network-manager-service.ts +5 -0
  173. package/src/optimystic-node.ts +36 -0
  174. package/src/owned-block-seed.ts +53 -40
  175. package/src/peer-address-book.ts +149 -0
  176. package/src/protocol-limits.ts +33 -33
  177. package/src/reactivity/forwarder-host.ts +438 -438
  178. package/src/reactivity/index.ts +19 -19
  179. package/src/reactivity/notify-transport.ts +144 -144
  180. package/src/reactivity/origination-manager.ts +192 -192
  181. package/src/reactivity/protocols.ts +61 -61
  182. package/src/reactivity/push-state-gossip.ts +291 -291
  183. package/src/reactivity/recover-transport.ts +408 -408
  184. package/src/reactivity/rotation-rereg-scheduler.ts +256 -256
  185. package/src/reactivity/subscriber-registry.ts +96 -96
  186. package/src/reactivity/subscription-manager.ts +450 -450
  187. package/src/reactivity/topic-bytes.ts +37 -37
  188. package/src/repo/client.ts +12 -2
  189. package/src/repo/cluster-coordinator.ts +99 -3
  190. package/src/repo/coordinator-repo.ts +305 -82
  191. package/src/repo/types.ts +7 -7
  192. package/src/rn.ts +39 -34
  193. package/src/rpc-deadline.ts +45 -45
  194. package/src/storage/arachnode-partition.ts +74 -74
  195. package/src/storage/block-storage.ts +59 -6
  196. package/src/storage/cached-raw-storage.ts +180 -0
  197. package/src/storage/cached-store-driver.ts +859 -0
  198. package/src/storage/i-block-storage.ts +20 -1
  199. package/src/storage/i-kv-store.ts +8 -8
  200. package/src/storage/i-raw-storage.ts +12 -5
  201. package/src/storage/kv-raw-storage.ts +135 -135
  202. package/src/storage/memory-kv-store.ts +28 -28
  203. package/src/storage/memory-storage.ts +25 -25
  204. package/src/storage/memory-store-driver.ts +157 -157
  205. package/src/storage/raw-store-codec.ts +42 -42
  206. package/src/storage/raw-store-driver.ts +80 -80
  207. package/src/storage/ring-selector.ts +317 -317
  208. package/src/storage/ring-shift-coordinator.ts +271 -271
  209. package/src/storage/shared-cache-pool.ts +452 -0
  210. package/src/storage/storage-repo.ts +1014 -903
  211. package/src/testing/cohort-topic-mesh-harness.ts +663 -663
  212. package/src/testing/index.ts +8 -8
  213. package/src/testing/matchmaking-mesh-harness.ts +475 -475
  214. package/src/testing/raw-storage-conformance.ts +453 -397
  215. package/src/testing/reactivity-mesh-harness.ts +922 -922
  216. package/dist/src/storage/restoration-coordinator-v2.d.ts +0 -67
  217. package/dist/src/storage/restoration-coordinator-v2.d.ts.map +0 -1
  218. package/dist/src/storage/restoration-coordinator-v2.js +0 -172
  219. package/dist/src/storage/restoration-coordinator-v2.js.map +0 -1
@@ -1,192 +1,192 @@
1
- /**
2
- * Reactivity — notification origination manager (db-p2p, wires to the cohort-topic substrate).
3
- *
4
- * Installs the substrate's {@link CohortTopicService.onLocalCommit} origination hook so a commit landing
5
- * on a node that is a tail-cohort member for the collection's reactivity topic emits a signed
6
- * {@link NotificationV1} (`docs/reactivity.md` §Notification origination). The local-change-notifier
7
- * bridge ([local-change-notifier-bridge]) supplies the {@link CollectionChangeEvent} + pass-through
8
- * {@link CommitCert}; this manager calls db-core's {@link buildNotificationV1} (which reuses the commit
9
- * cert's threshold signature **unchanged** — reactivity never re-signs) and hands the result to the
10
- * injected {@link emit} transport, which fans it out to direct subscribers and child cohorts.
11
- *
12
- * The cluster keys its commit votes by **peer-id string**; the cohort-topic membership verifier compares
13
- * signers as the member-id **bytes** (UTF-8 of the peer-id string, base64url on the wire). So this
14
- * manager supplies `encodeSigner = s ⇒ bytesToB64url(peerIdToBytes(s))`, the inverse the subscriber's
15
- * {@link createNotificationVerifier} default (`b64urlToBytes`) consumes — closing the encoding loop end
16
- * to end.
17
- */
18
-
19
- import {
20
- buildNotificationV1,
21
- bytesToB64url,
22
- b64urlToBytes,
23
- reactivityTopicId,
24
- BlockFillTracker,
25
- type BlockFillTrackerInit,
26
- type CohortTopicService,
27
- type CollectionChangeEvent,
28
- type CommitCert,
29
- type NotificationV1,
30
- type RotationHintV1,
31
- } from "@optimystic/db-core";
32
- import { peerIdToBytes } from "../cohort-topic/peer-codec.js";
33
- import { createLogger } from "../logger.js";
34
-
35
- const log = createLogger("reactivity-origination");
36
-
37
- /** Per-collection origination context the manager resolves at emit time. */
38
- export interface OriginationCollectionContext {
39
- /** Current tail block id the reactivity topic is anchored on (raw bytes). */
40
- readonly tailId: Uint8Array;
41
- /** Per-collection delta budget (bytes); `0` ⇒ omit `delta` (Edge / collection declines deltas). */
42
- readonly deltaMaxBytes: number;
43
- /** Optional bounded delta to attach (raw bytes). */
44
- readonly delta?: Uint8Array;
45
- /** Optional tail-rotation pre-announce (rotation ticket supplies it). */
46
- readonly rotationHint?: RotationHintV1;
47
- }
48
-
49
- /** Construction inputs for a {@link ReactivityOriginationManager}. */
50
- export interface ReactivityOriginationManagerOptions {
51
- /** The cohort-topic substrate whose `onLocalCommit` hook this manager installs. */
52
- readonly service: CohortTopicService;
53
- /**
54
- * Resolve the per-collection origination context (tail id, delta budget) for a change event. Returns
55
- * `undefined` to skip origination for this collection (e.g. this node is not the tail primary for it).
56
- */
57
- readonly resolveContext: (event: CollectionChangeEvent) => OriginationCollectionContext | undefined;
58
- /** Fan the built notification out to direct subscribers and child cohorts (the reactivity transport). */
59
- readonly emit: (notification: NotificationV1) => void;
60
- /**
61
- * Start the **outgoing** tail's drain when this manager observes a collection's tail id **change**
62
- * between commits (the authoritative, observable live-node rotation signal — the pre-announce
63
- * `rotationHint{ newTailId }` cannot be built on a live node because the successor tail id is not knowable
64
- * at the filling commit; see `docs/reactivity.md` §Tail rotation and the `6.5-block-id-derivation` gate).
65
- * The node binds this to {@link import("./forwarder-host.js").ReactivityForwarderHost.markRotated} so the
66
- * old cohort's recover serve begins redirecting. `oldTopicId` is the **previous** tail's reactivity topic
67
- * id (`reactivityTopicId` over the resolved tail bytes — the SAME encoding a subscriber subscribes under).
68
- * Absent ⇒ origination is unchanged (rotation observation is inert), preserving existing callers/tests.
69
- */
70
- readonly markRotated?: (oldTopicId: Uint8Array, redirect: { newTailId: string; effectiveAtRevision: number }, now: number) => void;
71
- /**
72
- * Per-collection {@link BlockFillTracker} tuning for the anticipatory **warm-up** signal. The warm-up is
73
- * best-effort and **signal-only** on a live node (the next `tailId` is not knowable, so no successor coord
74
- * is fabricated — the bias is logged, never acted on; `docs/reactivity.md` §Anticipatory warm-up). Defaults
75
- * to the db-core block-fill defaults.
76
- */
77
- readonly blockFill?: BlockFillTrackerInit;
78
- /** Wall clock (unix ms) stamped on each notification. Default `Date.now`. */
79
- readonly clock?: () => number;
80
- }
81
-
82
- /** Installs and drives the reactivity origination hook on a {@link CohortTopicService}. */
83
- export class ReactivityOriginationManager {
84
- private readonly service: CohortTopicService;
85
- private readonly resolveContext: (event: CollectionChangeEvent) => OriginationCollectionContext | undefined;
86
- private readonly emit: (notification: NotificationV1) => void;
87
- private readonly markRotated?: (oldTopicId: Uint8Array, redirect: { newTailId: string; effectiveAtRevision: number }, now: number) => void;
88
- private readonly blockFill?: BlockFillTrackerInit;
89
- private readonly clock: () => number;
90
-
91
- /** Last-seen reactivity tail anchor (base64url of the resolved tail bytes) per collection — the rotation signal. */
92
- private readonly lastSeenTail = new Map<string, string>();
93
- /** Per-collection block-fill tracker driving the anticipatory warm-up signal (signal-only on a live node). */
94
- private readonly fillTrackers = new Map<string, BlockFillTracker>();
95
-
96
- constructor(options: ReactivityOriginationManagerOptions) {
97
- this.service = options.service;
98
- this.resolveContext = options.resolveContext;
99
- this.emit = options.emit;
100
- this.markRotated = options.markRotated;
101
- this.blockFill = options.blockFill;
102
- this.clock = options.clock ?? ((): number => Date.now());
103
- }
104
-
105
- /** Install the origination hook (overwrites any prior `onLocalCommit`). */
106
- install(): void {
107
- this.service.onLocalCommit = (event, commitCert): void => this.originate(event, commitCert);
108
- }
109
-
110
- /** Build + emit the notification for one committed change; isolates throws so commit is never broken. */
111
- private originate(event: CollectionChangeEvent, commitCert: CommitCert): void {
112
- try {
113
- const ctx = this.resolveContext(event);
114
- if (ctx === undefined) {
115
- return; // not the origination point for this collection (tail-less / non-member)
116
- }
117
- // Observe the tail (rotation detection + block-fill warm-up) BEFORE emit, fully isolated so neither
118
- // ever blocks the notification — the delivery-critical path.
119
- this.observeTail(event, ctx);
120
- const notification = buildNotificationV1(event, commitCert, {
121
- tailId: bytesToB64url(ctx.tailId),
122
- timestamp: this.clock(),
123
- deltaMaxBytes: ctx.deltaMaxBytes,
124
- delta: ctx.delta,
125
- rotationHint: ctx.rotationHint,
126
- encodeSigner: (s) => bytesToB64url(peerIdToBytes(s)),
127
- });
128
- this.emit(notification);
129
- } catch (err) {
130
- log("origination failed for collection=%s rev=%d: %o", event.collectionId, event.rev, err);
131
- }
132
- }
133
-
134
- /**
135
- * Track the collection's reactivity tail and drive the rotation + warm-up signals. Isolated so a fault
136
- * here never blocks the notification emit. Only reached for commits this node originates (a defined `ctx`),
137
- * so a tail-less (read-driven) commit never records or clears the last-seen tail (the early-return holds).
138
- */
139
- private observeTail(event: CollectionChangeEvent, ctx: OriginationCollectionContext): void {
140
- try {
141
- this.trackBlockFill(event);
142
- this.detectTailRotation(event, ctx);
143
- } catch (err) {
144
- log("rotation/warm-up observation failed for collection=%s rev=%d (isolated): %o", event.collectionId, event.rev, err);
145
- }
146
- }
147
-
148
- /**
149
- * Feed the per-collection {@link BlockFillTracker} one commit. The `warmup` signal is **best-effort and
150
- * signal-only** on a live node: the next `tailId` is not knowable (block ids are random until
151
- * `6.5-block-id-derivation`), so the anticipatory pre-dial bias is logged, never fabricated into a
152
- * successor coord (`docs/reactivity.md` §Anticipatory warm-up). The `filling` signal cannot pre-announce a
153
- * hint on a live node for the same reason — it is logged only.
154
- */
155
- private trackBlockFill(event: CollectionChangeEvent): void {
156
- const key = event.collectionId;
157
- let tracker = this.fillTrackers.get(key);
158
- if (tracker === undefined) {
159
- tracker = new BlockFillTracker(this.blockFill);
160
- this.fillTrackers.set(key, tracker);
161
- }
162
- const signal = tracker.onCommit();
163
- if (signal.kind === "warmup") {
164
- log("block-fill warm-up for collection=%s (%d committed, %d remaining) — anticipatory pre-dial is signal-only on a live node (successor tail not knowable; gated on 6.5-block-id-derivation)", key, signal.count, signal.remaining);
165
- } else if (signal.kind === "filling") {
166
- log("block-fill filling commit for collection=%s (%d committed) — no live pre-announce (successor tail id not knowable; rotation observed on the next commit's tail-id change)", key, signal.count);
167
- }
168
- }
169
-
170
- /**
171
- * Detect a tail rotation by comparing the resolved tail anchor against the last-seen one for the
172
- * collection. The first commit records the baseline (no rotation). On a **change**, the previous tail's
173
- * reactivity topic has rotated to this one: fire {@link markRotated} for the OLD topic so the old cohort's
174
- * recover serve begins redirecting to the new tree.
175
- *
176
- * **Encoding contract.** `ctx.tailId` is the reactivity tail anchor bytes the node resolved
177
- * (`reactivityTailBytes(event.tailId)` in production — the SAME utf8 encoding origination's membership gate
178
- * and a subscriber's `reactivityTopicId(reactivityTailBytes(tail))` use, NOT the double-hashing
179
- * `blockIdToBytes`). So `oldTopicId = reactivityTopicId(oldAnchorBytes)` is byte-identical to the topic a
180
- * subscriber subscribed under — a mismatch would silently never redirect.
181
- */
182
- private detectTailRotation(event: CollectionChangeEvent, ctx: OriginationCollectionContext): void {
183
- const key = event.collectionId;
184
- const newTailB64 = bytesToB64url(ctx.tailId);
185
- const lastTailB64 = this.lastSeenTail.get(key);
186
- if (lastTailB64 !== undefined && lastTailB64 !== newTailB64) {
187
- const oldTopicId = reactivityTopicId(b64urlToBytes(lastTailB64));
188
- this.markRotated?.(oldTopicId, { newTailId: newTailB64, effectiveAtRevision: event.rev }, this.clock());
189
- }
190
- this.lastSeenTail.set(key, newTailB64);
191
- }
192
- }
1
+ /**
2
+ * Reactivity — notification origination manager (db-p2p, wires to the cohort-topic substrate).
3
+ *
4
+ * Installs the substrate's {@link CohortTopicService.onLocalCommit} origination hook so a commit landing
5
+ * on a node that is a tail-cohort member for the collection's reactivity topic emits a signed
6
+ * {@link NotificationV1} (`docs/reactivity.md` §Notification origination). The local-change-notifier
7
+ * bridge ([local-change-notifier-bridge]) supplies the {@link CollectionChangeEvent} + pass-through
8
+ * {@link CommitCert}; this manager calls db-core's {@link buildNotificationV1} (which reuses the commit
9
+ * cert's threshold signature **unchanged** — reactivity never re-signs) and hands the result to the
10
+ * injected {@link emit} transport, which fans it out to direct subscribers and child cohorts.
11
+ *
12
+ * The cluster keys its commit votes by **peer-id string**; the cohort-topic membership verifier compares
13
+ * signers as the member-id **bytes** (UTF-8 of the peer-id string, base64url on the wire). So this
14
+ * manager supplies `encodeSigner = s ⇒ bytesToB64url(peerIdToBytes(s))`, the inverse the subscriber's
15
+ * {@link createNotificationVerifier} default (`b64urlToBytes`) consumes — closing the encoding loop end
16
+ * to end.
17
+ */
18
+
19
+ import {
20
+ buildNotificationV1,
21
+ bytesToB64url,
22
+ b64urlToBytes,
23
+ reactivityTopicId,
24
+ BlockFillTracker,
25
+ type BlockFillTrackerInit,
26
+ type CohortTopicService,
27
+ type CollectionChangeEvent,
28
+ type CommitCert,
29
+ type NotificationV1,
30
+ type RotationHintV1,
31
+ } from "@optimystic/db-core";
32
+ import { peerIdToBytes } from "../cohort-topic/peer-codec.js";
33
+ import { createLogger } from "../logger.js";
34
+
35
+ const log = createLogger("reactivity-origination");
36
+
37
+ /** Per-collection origination context the manager resolves at emit time. */
38
+ export interface OriginationCollectionContext {
39
+ /** Current tail block id the reactivity topic is anchored on (raw bytes). */
40
+ readonly tailId: Uint8Array;
41
+ /** Per-collection delta budget (bytes); `0` ⇒ omit `delta` (Edge / collection declines deltas). */
42
+ readonly deltaMaxBytes: number;
43
+ /** Optional bounded delta to attach (raw bytes). */
44
+ readonly delta?: Uint8Array;
45
+ /** Optional tail-rotation pre-announce (rotation ticket supplies it). */
46
+ readonly rotationHint?: RotationHintV1;
47
+ }
48
+
49
+ /** Construction inputs for a {@link ReactivityOriginationManager}. */
50
+ export interface ReactivityOriginationManagerOptions {
51
+ /** The cohort-topic substrate whose `onLocalCommit` hook this manager installs. */
52
+ readonly service: CohortTopicService;
53
+ /**
54
+ * Resolve the per-collection origination context (tail id, delta budget) for a change event. Returns
55
+ * `undefined` to skip origination for this collection (e.g. this node is not the tail primary for it).
56
+ */
57
+ readonly resolveContext: (event: CollectionChangeEvent) => OriginationCollectionContext | undefined;
58
+ /** Fan the built notification out to direct subscribers and child cohorts (the reactivity transport). */
59
+ readonly emit: (notification: NotificationV1) => void;
60
+ /**
61
+ * Start the **outgoing** tail's drain when this manager observes a collection's tail id **change**
62
+ * between commits (the authoritative, observable live-node rotation signal — the pre-announce
63
+ * `rotationHint{ newTailId }` cannot be built on a live node because the successor tail id is not knowable
64
+ * at the filling commit; see `docs/reactivity.md` §Tail rotation and the `6.5-block-id-derivation` gate).
65
+ * The node binds this to {@link import("./forwarder-host.js").ReactivityForwarderHost.markRotated} so the
66
+ * old cohort's recover serve begins redirecting. `oldTopicId` is the **previous** tail's reactivity topic
67
+ * id (`reactivityTopicId` over the resolved tail bytes — the SAME encoding a subscriber subscribes under).
68
+ * Absent ⇒ origination is unchanged (rotation observation is inert), preserving existing callers/tests.
69
+ */
70
+ readonly markRotated?: (oldTopicId: Uint8Array, redirect: { newTailId: string; effectiveAtRevision: number }, now: number) => void;
71
+ /**
72
+ * Per-collection {@link BlockFillTracker} tuning for the anticipatory **warm-up** signal. The warm-up is
73
+ * best-effort and **signal-only** on a live node (the next `tailId` is not knowable, so no successor coord
74
+ * is fabricated — the bias is logged, never acted on; `docs/reactivity.md` §Anticipatory warm-up). Defaults
75
+ * to the db-core block-fill defaults.
76
+ */
77
+ readonly blockFill?: BlockFillTrackerInit;
78
+ /** Wall clock (unix ms) stamped on each notification. Default `Date.now`. */
79
+ readonly clock?: () => number;
80
+ }
81
+
82
+ /** Installs and drives the reactivity origination hook on a {@link CohortTopicService}. */
83
+ export class ReactivityOriginationManager {
84
+ private readonly service: CohortTopicService;
85
+ private readonly resolveContext: (event: CollectionChangeEvent) => OriginationCollectionContext | undefined;
86
+ private readonly emit: (notification: NotificationV1) => void;
87
+ private readonly markRotated?: (oldTopicId: Uint8Array, redirect: { newTailId: string; effectiveAtRevision: number }, now: number) => void;
88
+ private readonly blockFill?: BlockFillTrackerInit;
89
+ private readonly clock: () => number;
90
+
91
+ /** Last-seen reactivity tail anchor (base64url of the resolved tail bytes) per collection — the rotation signal. */
92
+ private readonly lastSeenTail = new Map<string, string>();
93
+ /** Per-collection block-fill tracker driving the anticipatory warm-up signal (signal-only on a live node). */
94
+ private readonly fillTrackers = new Map<string, BlockFillTracker>();
95
+
96
+ constructor(options: ReactivityOriginationManagerOptions) {
97
+ this.service = options.service;
98
+ this.resolveContext = options.resolveContext;
99
+ this.emit = options.emit;
100
+ this.markRotated = options.markRotated;
101
+ this.blockFill = options.blockFill;
102
+ this.clock = options.clock ?? ((): number => Date.now());
103
+ }
104
+
105
+ /** Install the origination hook (overwrites any prior `onLocalCommit`). */
106
+ install(): void {
107
+ this.service.onLocalCommit = (event, commitCert): void => this.originate(event, commitCert);
108
+ }
109
+
110
+ /** Build + emit the notification for one committed change; isolates throws so commit is never broken. */
111
+ private originate(event: CollectionChangeEvent, commitCert: CommitCert): void {
112
+ try {
113
+ const ctx = this.resolveContext(event);
114
+ if (ctx === undefined) {
115
+ return; // not the origination point for this collection (tail-less / non-member)
116
+ }
117
+ // Observe the tail (rotation detection + block-fill warm-up) BEFORE emit, fully isolated so neither
118
+ // ever blocks the notification — the delivery-critical path.
119
+ this.observeTail(event, ctx);
120
+ const notification = buildNotificationV1(event, commitCert, {
121
+ tailId: bytesToB64url(ctx.tailId),
122
+ timestamp: this.clock(),
123
+ deltaMaxBytes: ctx.deltaMaxBytes,
124
+ delta: ctx.delta,
125
+ rotationHint: ctx.rotationHint,
126
+ encodeSigner: (s) => bytesToB64url(peerIdToBytes(s)),
127
+ });
128
+ this.emit(notification);
129
+ } catch (err) {
130
+ log("origination failed for collection=%s rev=%d: %o", event.collectionId, event.rev, err);
131
+ }
132
+ }
133
+
134
+ /**
135
+ * Track the collection's reactivity tail and drive the rotation + warm-up signals. Isolated so a fault
136
+ * here never blocks the notification emit. Only reached for commits this node originates (a defined `ctx`),
137
+ * so a tail-less (read-driven) commit never records or clears the last-seen tail (the early-return holds).
138
+ */
139
+ private observeTail(event: CollectionChangeEvent, ctx: OriginationCollectionContext): void {
140
+ try {
141
+ this.trackBlockFill(event);
142
+ this.detectTailRotation(event, ctx);
143
+ } catch (err) {
144
+ log("rotation/warm-up observation failed for collection=%s rev=%d (isolated): %o", event.collectionId, event.rev, err);
145
+ }
146
+ }
147
+
148
+ /**
149
+ * Feed the per-collection {@link BlockFillTracker} one commit. The `warmup` signal is **best-effort and
150
+ * signal-only** on a live node: the next `tailId` is not knowable (block ids are random until
151
+ * `6.5-block-id-derivation`), so the anticipatory pre-dial bias is logged, never fabricated into a
152
+ * successor coord (`docs/reactivity.md` §Anticipatory warm-up). The `filling` signal cannot pre-announce a
153
+ * hint on a live node for the same reason — it is logged only.
154
+ */
155
+ private trackBlockFill(event: CollectionChangeEvent): void {
156
+ const key = event.collectionId;
157
+ let tracker = this.fillTrackers.get(key);
158
+ if (tracker === undefined) {
159
+ tracker = new BlockFillTracker(this.blockFill);
160
+ this.fillTrackers.set(key, tracker);
161
+ }
162
+ const signal = tracker.onCommit();
163
+ if (signal.kind === "warmup") {
164
+ log("block-fill warm-up for collection=%s (%d committed, %d remaining) — anticipatory pre-dial is signal-only on a live node (successor tail not knowable; gated on 6.5-block-id-derivation)", key, signal.count, signal.remaining);
165
+ } else if (signal.kind === "filling") {
166
+ log("block-fill filling commit for collection=%s (%d committed) — no live pre-announce (successor tail id not knowable; rotation observed on the next commit's tail-id change)", key, signal.count);
167
+ }
168
+ }
169
+
170
+ /**
171
+ * Detect a tail rotation by comparing the resolved tail anchor against the last-seen one for the
172
+ * collection. The first commit records the baseline (no rotation). On a **change**, the previous tail's
173
+ * reactivity topic has rotated to this one: fire {@link markRotated} for the OLD topic so the old cohort's
174
+ * recover serve begins redirecting to the new tree.
175
+ *
176
+ * **Encoding contract.** `ctx.tailId` is the reactivity tail anchor bytes the node resolved
177
+ * (`reactivityTailBytes(event.tailId)` in production — the SAME utf8 encoding origination's membership gate
178
+ * and a subscriber's `reactivityTopicId(reactivityTailBytes(tail))` use, NOT the double-hashing
179
+ * `blockIdToBytes`). So `oldTopicId = reactivityTopicId(oldAnchorBytes)` is byte-identical to the topic a
180
+ * subscriber subscribed under — a mismatch would silently never redirect.
181
+ */
182
+ private detectTailRotation(event: CollectionChangeEvent, ctx: OriginationCollectionContext): void {
183
+ const key = event.collectionId;
184
+ const newTailB64 = bytesToB64url(ctx.tailId);
185
+ const lastTailB64 = this.lastSeenTail.get(key);
186
+ if (lastTailB64 !== undefined && lastTailB64 !== newTailB64) {
187
+ const oldTopicId = reactivityTopicId(b64urlToBytes(lastTailB64));
188
+ this.markRotated?.(oldTopicId, { newTailId: newTailB64, effectiveAtRevision: event.rev }, this.clock());
189
+ }
190
+ this.lastSeenTail.set(key, newTailB64);
191
+ }
192
+ }
@@ -1,61 +1,61 @@
1
- /**
2
- * Reactivity libp2p protocol IDs (`docs/reactivity.md` §Propagation / §Notification origination).
3
- *
4
- * The reactivity family rides the same FRET/libp2p node as the cohort-topic substrate it is layered on:
5
- *
6
- * - `notify` — `NotificationV1` fan-out down the reactivity tree (one-way, fire-and-forget).
7
- * - `push-state-gossip` — `PushStateGossipV1` intra-cohort push-state convergence (one-way). Declared
8
- * here so one place owns the family; the `reactivity-pushstate-gossip` ticket
9
- * consumes it.
10
- * - `recover` — `RecoverRequestV1`/`RecoverReplyV1` backfill/resume recovery RPC
11
- * (request-reply, one frame each way). Consumed by `reactivity-recover-rpc-transport`.
12
- *
13
- * The default (network-agnostic) IDs omit the network segment; {@link makeReactivityProtocols} mirrors
14
- * FRET's `makeProtocols(networkName)` so a named network namespaces its reactivity protocols the same way
15
- * FRET namespaces its routing protocols (and the cohort-topic family namespaces via
16
- * {@link import("../cohort-topic/protocols.js").makeCohortTopicProtocols}).
17
- */
18
-
19
- /** Base path for the reactivity protocol family. */
20
- export const REACTIVITY_BASE = "/optimystic/reactivity/1.0.0" as const;
21
-
22
- /** `NotificationV1` — change-notification fan-out down the reactivity tree (one-way). */
23
- export const PROTOCOL_REACTIVITY_NOTIFY = `${REACTIVITY_BASE}/notify` as const;
24
- /** `PushStateGossipV1` — intra-cohort push-state convergence (one-way) [used by reactivity-pushstate-gossip]. */
25
- export const PROTOCOL_REACTIVITY_PUSH_STATE_GOSSIP = `${REACTIVITY_BASE}/push-state-gossip` as const;
26
- /** `RecoverRequestV1`/`RecoverReplyV1` — backfill/resume recovery RPC (request-reply) [used by reactivity-recover-rpc-transport]. */
27
- export const PROTOCOL_REACTIVITY_RECOVER = `${REACTIVITY_BASE}/recover` as const;
28
-
29
- /** The reactivity protocol IDs in registration order. */
30
- export interface ReactivityProtocols {
31
- readonly notify: string;
32
- readonly pushStateGossip: string;
33
- readonly recover: string;
34
- }
35
-
36
- /** Default (network-agnostic) protocol IDs, matching `docs/reactivity.md`. */
37
- export const DEFAULT_REACTIVITY_PROTOCOLS: ReactivityProtocols = {
38
- notify: PROTOCOL_REACTIVITY_NOTIFY,
39
- pushStateGossip: PROTOCOL_REACTIVITY_PUSH_STATE_GOSSIP,
40
- recover: PROTOCOL_REACTIVITY_RECOVER,
41
- };
42
-
43
- /**
44
- * Namespaced reactivity protocol IDs for `networkName` (mirrors FRET's `makeProtocols`, which inserts the
45
- * network segment even for `"default"` → `/optimystic/default/...`). Note this does NOT equal
46
- * {@link DEFAULT_REACTIVITY_PROTOCOLS}: the canonical, network-agnostic IDs omit the segment entirely
47
- * (`/optimystic/reactivity/1.0.0/...`); use those unless you need per-network namespacing.
48
- */
49
- export function makeReactivityProtocols(networkName = "default"): ReactivityProtocols {
50
- const base = `/optimystic/${networkName}/reactivity/1.0.0`;
51
- return {
52
- notify: `${base}/notify`,
53
- pushStateGossip: `${base}/push-state-gossip`,
54
- recover: `${base}/recover`,
55
- };
56
- }
57
-
58
- /** All reactivity protocol IDs as an array (for `node.handle` / `unhandle` over the set). */
59
- export function reactivityProtocolList(p: ReactivityProtocols): string[] {
60
- return [p.notify, p.pushStateGossip, p.recover];
61
- }
1
+ /**
2
+ * Reactivity libp2p protocol IDs (`docs/reactivity.md` §Propagation / §Notification origination).
3
+ *
4
+ * The reactivity family rides the same FRET/libp2p node as the cohort-topic substrate it is layered on:
5
+ *
6
+ * - `notify` — `NotificationV1` fan-out down the reactivity tree (one-way, fire-and-forget).
7
+ * - `push-state-gossip` — `PushStateGossipV1` intra-cohort push-state convergence (one-way). Declared
8
+ * here so one place owns the family; the `reactivity-pushstate-gossip` ticket
9
+ * consumes it.
10
+ * - `recover` — `RecoverRequestV1`/`RecoverReplyV1` backfill/resume recovery RPC
11
+ * (request-reply, one frame each way). Consumed by `reactivity-recover-rpc-transport`.
12
+ *
13
+ * The default (network-agnostic) IDs omit the network segment; {@link makeReactivityProtocols} mirrors
14
+ * FRET's `makeProtocols(networkName)` so a named network namespaces its reactivity protocols the same way
15
+ * FRET namespaces its routing protocols (and the cohort-topic family namespaces via
16
+ * {@link import("../cohort-topic/protocols.js").makeCohortTopicProtocols}).
17
+ */
18
+
19
+ /** Base path for the reactivity protocol family. */
20
+ export const REACTIVITY_BASE = "/optimystic/reactivity/1.0.0" as const;
21
+
22
+ /** `NotificationV1` — change-notification fan-out down the reactivity tree (one-way). */
23
+ export const PROTOCOL_REACTIVITY_NOTIFY = `${REACTIVITY_BASE}/notify` as const;
24
+ /** `PushStateGossipV1` — intra-cohort push-state convergence (one-way) [used by reactivity-pushstate-gossip]. */
25
+ export const PROTOCOL_REACTIVITY_PUSH_STATE_GOSSIP = `${REACTIVITY_BASE}/push-state-gossip` as const;
26
+ /** `RecoverRequestV1`/`RecoverReplyV1` — backfill/resume recovery RPC (request-reply) [used by reactivity-recover-rpc-transport]. */
27
+ export const PROTOCOL_REACTIVITY_RECOVER = `${REACTIVITY_BASE}/recover` as const;
28
+
29
+ /** The reactivity protocol IDs in registration order. */
30
+ export interface ReactivityProtocols {
31
+ readonly notify: string;
32
+ readonly pushStateGossip: string;
33
+ readonly recover: string;
34
+ }
35
+
36
+ /** Default (network-agnostic) protocol IDs, matching `docs/reactivity.md`. */
37
+ export const DEFAULT_REACTIVITY_PROTOCOLS: ReactivityProtocols = {
38
+ notify: PROTOCOL_REACTIVITY_NOTIFY,
39
+ pushStateGossip: PROTOCOL_REACTIVITY_PUSH_STATE_GOSSIP,
40
+ recover: PROTOCOL_REACTIVITY_RECOVER,
41
+ };
42
+
43
+ /**
44
+ * Namespaced reactivity protocol IDs for `networkName` (mirrors FRET's `makeProtocols`, which inserts the
45
+ * network segment even for `"default"` → `/optimystic/default/...`). Note this does NOT equal
46
+ * {@link DEFAULT_REACTIVITY_PROTOCOLS}: the canonical, network-agnostic IDs omit the segment entirely
47
+ * (`/optimystic/reactivity/1.0.0/...`); use those unless you need per-network namespacing.
48
+ */
49
+ export function makeReactivityProtocols(networkName = "default"): ReactivityProtocols {
50
+ const base = `/optimystic/${networkName}/reactivity/1.0.0`;
51
+ return {
52
+ notify: `${base}/notify`,
53
+ pushStateGossip: `${base}/push-state-gossip`,
54
+ recover: `${base}/recover`,
55
+ };
56
+ }
57
+
58
+ /** All reactivity protocol IDs as an array (for `node.handle` / `unhandle` over the set). */
59
+ export function reactivityProtocolList(p: ReactivityProtocols): string[] {
60
+ return [p.notify, p.pushStateGossip, p.recover];
61
+ }