@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,19 +1,19 @@
1
- /**
2
- * Reactivity — db-p2p wiring to the cohort-topic substrate.
3
- *
4
- * The db-core `reactivity` module owns the wire codecs, topic anchor, config, origination assembler,
5
- * forwarder/dedupe/replay logic, and subscriber-side verify/deliver; these managers bind that to the
6
- * participant-facing `CohortTopicService` (subscribe/renew/withdraw at tier T3) and install the
7
- * `onLocalCommit` origination hook. See `docs/reactivity.md` §Subscription / §Notification origination.
8
- */
9
-
10
- export * from "./topic-bytes.js";
11
- export * from "./protocols.js";
12
- export * from "./notify-transport.js";
13
- export * from "./recover-transport.js";
14
- export * from "./subscription-manager.js";
15
- export * from "./rotation-rereg-scheduler.js";
16
- export * from "./origination-manager.js";
17
- export * from "./forwarder-host.js";
18
- export * from "./push-state-gossip.js";
19
- export * from "./subscriber-registry.js";
1
+ /**
2
+ * Reactivity — db-p2p wiring to the cohort-topic substrate.
3
+ *
4
+ * The db-core `reactivity` module owns the wire codecs, topic anchor, config, origination assembler,
5
+ * forwarder/dedupe/replay logic, and subscriber-side verify/deliver; these managers bind that to the
6
+ * participant-facing `CohortTopicService` (subscribe/renew/withdraw at tier T3) and install the
7
+ * `onLocalCommit` origination hook. See `docs/reactivity.md` §Subscription / §Notification origination.
8
+ */
9
+
10
+ export * from "./topic-bytes.js";
11
+ export * from "./protocols.js";
12
+ export * from "./notify-transport.js";
13
+ export * from "./recover-transport.js";
14
+ export * from "./subscription-manager.js";
15
+ export * from "./rotation-rereg-scheduler.js";
16
+ export * from "./origination-manager.js";
17
+ export * from "./forwarder-host.js";
18
+ export * from "./push-state-gossip.js";
19
+ export * from "./subscriber-registry.js";
@@ -1,144 +1,144 @@
1
- /**
2
- * Reactivity notify transport — the one-way `NotificationV1` delivery primitive (`docs/reactivity.md`
3
- * §Propagation).
4
- *
5
- * Unlike the cohort-gossip transport (which *broadcasts* a frame to a FRET-assembled cohort), notify is
6
- * **unicast**: the fan-out orchestration above this layer (the `reactivity-forwarder-host` ticket) decides
7
- * who to dial and calls {@link ReactivityNotifyTransport.send} once per named target. This module owns only
8
- * the framing + dial + inbound-decode plumbing — no fan-out, no role decision, no gossip.
9
- *
10
- * The db-core `NotificationV1` codec ({@link encodeNotificationV1} / {@link decodeNotificationV1}) does the
11
- * length-prefixed JSON framing; this layer rides one self-delimiting frame each way over the notify
12
- * protocol, reusing the cohort-topic {@link sendOneWay} / {@link readAllBounded} stream lifecycle so the
13
- * two protocol families behave identically on the wire.
14
- *
15
- * Failure isolation is the load-bearing property: notify is fire-and-forget and hint-only, so a dead /
16
- * unreachable target's rejection is swallowed (logged), never propagated to the caller's fan-out loop or a
17
- * commit. There is no reply frame — a handler that tried to send one back would desync the dialer's
18
- * {@link sendOneWay} (which closes after send).
19
- */
20
-
21
- import type { NotificationV1, PeerRef } from "@optimystic/db-core";
22
- import { encodeNotificationV1, decodeNotificationV1 } from "@optimystic/db-core";
23
- import type { Libp2p } from "libp2p";
24
- import type { Connection, Stream } from "@libp2p/interface";
25
- import { peerIdFromString } from "@libp2p/peer-id";
26
- import { readAllBounded } from "p2p-fret";
27
- import { peerIdToBytes } from "../cohort-topic/peer-codec.js";
28
- import { sendOneWay, DEFAULT_STREAM_MAX_BYTES } from "../cohort-topic/stream-util.js";
29
- import { PROTOCOL_REACTIVITY_NOTIFY } from "./protocols.js";
30
- import { createLogger } from "../logger.js";
31
-
32
- const log = createLogger("reactivity-notify");
33
-
34
- /** One-way `NotificationV1` transport: unicast send, inbound subscribe, and the host's deliver seam. */
35
- export interface ReactivityNotifyTransport {
36
- /**
37
- * Frame `n` ({@link encodeNotificationV1}) and dial `target` (peer-id string) over the notify protocol.
38
- * Fire-and-forget, failure-isolated: a dead/unreachable target's rejection is swallowed (logged), never
39
- * propagated to the caller's fan-out loop.
40
- */
41
- send(target: string, n: NotificationV1): Promise<void>;
42
- /** Subscribe to inbound notifications (after decode); returns an unsubscribe handle. */
43
- onNotification(handler: (from: PeerRef, n: NotificationV1) => void): () => void;
44
- /** Feed an inbound notify frame (called by the host's notify protocol handler). */
45
- deliver(fromPeerId: string, frame: Uint8Array): void;
46
- }
47
-
48
- /** Construction options for {@link Libp2pReactivityNotifyTransport}. */
49
- export interface ReactivityNotifyTransportOptions {
50
- /** Notify protocol ID; default {@link PROTOCOL_REACTIVITY_NOTIFY}. */
51
- readonly notifyProtocol?: string;
52
- /** Per-frame ceiling for the inbound decode bound. Default {@link DEFAULT_STREAM_MAX_BYTES}. */
53
- readonly maxBytes?: number;
54
- /** This node's peer-id string; when set, {@link Libp2pReactivityNotifyTransport.send} never dials self. */
55
- readonly selfPeerId?: string;
56
- }
57
-
58
- /**
59
- * libp2p-backed {@link ReactivityNotifyTransport}: {@link send} frames + dials a single target over
60
- * `/optimystic/reactivity/1.0.0/notify` (fire-and-forget, failure-isolated); inbound frames arrive through
61
- * the host's notify protocol handler ({@link registerNotifyHandler}), which calls {@link deliver}.
62
- * Subscribers registered via {@link onNotification} see every decoded notification.
63
- */
64
- export class Libp2pReactivityNotifyTransport implements ReactivityNotifyTransport {
65
- private readonly handlers = new Set<(from: PeerRef, n: NotificationV1) => void>();
66
- private readonly notifyProtocol: string;
67
- private readonly maxBytes: number;
68
- private readonly selfPeerId?: string;
69
-
70
- constructor(private readonly node: Libp2p, options: ReactivityNotifyTransportOptions = {}) {
71
- this.notifyProtocol = options.notifyProtocol ?? PROTOCOL_REACTIVITY_NOTIFY;
72
- this.maxBytes = options.maxBytes ?? DEFAULT_STREAM_MAX_BYTES;
73
- this.selfPeerId = options.selfPeerId;
74
- }
75
-
76
- send(target: string, n: NotificationV1): Promise<void> {
77
- if (this.selfPeerId !== undefined && target === this.selfPeerId) {
78
- // Never dial self; a co-located subscriber is delivered in-process by the forwarder host.
79
- return Promise.resolve();
80
- }
81
- try {
82
- const frame = encodeNotificationV1(n);
83
- return sendOneWay(this.node, peerIdFromString(target), this.notifyProtocol, frame).catch((err: unknown) => {
84
- // Best-effort, failure-isolated: a dead/unreachable target must not break the fan-out or a commit.
85
- log("send to %s failed (swallowed): %o", target, err);
86
- });
87
- } catch (err) {
88
- // Malformed notification or peer-id string: log + drop, never reject (reactivity is hint-only).
89
- log("dropped a send to %s: %o", target, err);
90
- return Promise.resolve();
91
- }
92
- }
93
-
94
- onNotification(handler: (from: PeerRef, n: NotificationV1) => void): () => void {
95
- this.handlers.add(handler);
96
- return () => this.handlers.delete(handler);
97
- }
98
-
99
- /** Feed an inbound notify frame (called by the host's notify protocol handler). */
100
- deliver(fromPeerId: string, frame: Uint8Array): void {
101
- let n: NotificationV1;
102
- try {
103
- n = decodeNotificationV1(frame, this.maxBytes);
104
- } catch (err) {
105
- // A malformed frame must never throw out of a stream handler: log + drop.
106
- log("dropped an undecodable inbound frame from %s: %o", fromPeerId, err);
107
- return;
108
- }
109
- const from: PeerRef = { id: peerIdToBytes(fromPeerId) };
110
- for (const handler of this.handlers) {
111
- handler(from, n);
112
- }
113
- }
114
- }
115
-
116
- /**
117
- * Register the inbound notify protocol handler: read one bounded frame and hand it to
118
- * {@link ReactivityNotifyTransport.deliver}, then close. One-way — no reply frame (notify is strictly
119
- * fire-and-forget; a reply would desync the dialer's {@link sendOneWay}). Mirrors the cohort-topic
120
- * one-way handlers: a read error aborts the stream, and {@link ReactivityNotifyTransport.deliver}
121
- * swallows a decode failure, so the handler never throws on the stream.
122
- */
123
- export function registerNotifyHandler(
124
- node: Libp2p,
125
- protocol: string,
126
- transport: ReactivityNotifyTransport,
127
- maxBytes = DEFAULT_STREAM_MAX_BYTES,
128
- ): void {
129
- void node.handle(protocol, (stream: Stream, connection: Connection) => {
130
- void (async (): Promise<void> => {
131
- try {
132
- const frame = await readAllBounded(stream, maxBytes);
133
- transport.deliver(connection.remotePeer.toString(), frame);
134
- await stream.close();
135
- } catch {
136
- try {
137
- stream.abort(new Error("reactivity notify stream handler error"));
138
- } catch {
139
- /* already aborted */
140
- }
141
- }
142
- })();
143
- });
144
- }
1
+ /**
2
+ * Reactivity notify transport — the one-way `NotificationV1` delivery primitive (`docs/reactivity.md`
3
+ * §Propagation).
4
+ *
5
+ * Unlike the cohort-gossip transport (which *broadcasts* a frame to a FRET-assembled cohort), notify is
6
+ * **unicast**: the fan-out orchestration above this layer (the `reactivity-forwarder-host` ticket) decides
7
+ * who to dial and calls {@link ReactivityNotifyTransport.send} once per named target. This module owns only
8
+ * the framing + dial + inbound-decode plumbing — no fan-out, no role decision, no gossip.
9
+ *
10
+ * The db-core `NotificationV1` codec ({@link encodeNotificationV1} / {@link decodeNotificationV1}) does the
11
+ * length-prefixed JSON framing; this layer rides one self-delimiting frame each way over the notify
12
+ * protocol, reusing the cohort-topic {@link sendOneWay} / {@link readAllBounded} stream lifecycle so the
13
+ * two protocol families behave identically on the wire.
14
+ *
15
+ * Failure isolation is the load-bearing property: notify is fire-and-forget and hint-only, so a dead /
16
+ * unreachable target's rejection is swallowed (logged), never propagated to the caller's fan-out loop or a
17
+ * commit. There is no reply frame — a handler that tried to send one back would desync the dialer's
18
+ * {@link sendOneWay} (which closes after send).
19
+ */
20
+
21
+ import type { NotificationV1, PeerRef } from "@optimystic/db-core";
22
+ import { encodeNotificationV1, decodeNotificationV1 } from "@optimystic/db-core";
23
+ import type { Libp2p } from "libp2p";
24
+ import type { Connection, Stream } from "@libp2p/interface";
25
+ import { peerIdFromString } from "@libp2p/peer-id";
26
+ import { readAllBounded } from "p2p-fret";
27
+ import { peerIdToBytes } from "../cohort-topic/peer-codec.js";
28
+ import { sendOneWay, DEFAULT_STREAM_MAX_BYTES } from "../cohort-topic/stream-util.js";
29
+ import { PROTOCOL_REACTIVITY_NOTIFY } from "./protocols.js";
30
+ import { createLogger } from "../logger.js";
31
+
32
+ const log = createLogger("reactivity-notify");
33
+
34
+ /** One-way `NotificationV1` transport: unicast send, inbound subscribe, and the host's deliver seam. */
35
+ export interface ReactivityNotifyTransport {
36
+ /**
37
+ * Frame `n` ({@link encodeNotificationV1}) and dial `target` (peer-id string) over the notify protocol.
38
+ * Fire-and-forget, failure-isolated: a dead/unreachable target's rejection is swallowed (logged), never
39
+ * propagated to the caller's fan-out loop.
40
+ */
41
+ send(target: string, n: NotificationV1): Promise<void>;
42
+ /** Subscribe to inbound notifications (after decode); returns an unsubscribe handle. */
43
+ onNotification(handler: (from: PeerRef, n: NotificationV1) => void): () => void;
44
+ /** Feed an inbound notify frame (called by the host's notify protocol handler). */
45
+ deliver(fromPeerId: string, frame: Uint8Array): void;
46
+ }
47
+
48
+ /** Construction options for {@link Libp2pReactivityNotifyTransport}. */
49
+ export interface ReactivityNotifyTransportOptions {
50
+ /** Notify protocol ID; default {@link PROTOCOL_REACTIVITY_NOTIFY}. */
51
+ readonly notifyProtocol?: string;
52
+ /** Per-frame ceiling for the inbound decode bound. Default {@link DEFAULT_STREAM_MAX_BYTES}. */
53
+ readonly maxBytes?: number;
54
+ /** This node's peer-id string; when set, {@link Libp2pReactivityNotifyTransport.send} never dials self. */
55
+ readonly selfPeerId?: string;
56
+ }
57
+
58
+ /**
59
+ * libp2p-backed {@link ReactivityNotifyTransport}: {@link send} frames + dials a single target over
60
+ * `/optimystic/reactivity/1.0.0/notify` (fire-and-forget, failure-isolated); inbound frames arrive through
61
+ * the host's notify protocol handler ({@link registerNotifyHandler}), which calls {@link deliver}.
62
+ * Subscribers registered via {@link onNotification} see every decoded notification.
63
+ */
64
+ export class Libp2pReactivityNotifyTransport implements ReactivityNotifyTransport {
65
+ private readonly handlers = new Set<(from: PeerRef, n: NotificationV1) => void>();
66
+ private readonly notifyProtocol: string;
67
+ private readonly maxBytes: number;
68
+ private readonly selfPeerId?: string;
69
+
70
+ constructor(private readonly node: Libp2p, options: ReactivityNotifyTransportOptions = {}) {
71
+ this.notifyProtocol = options.notifyProtocol ?? PROTOCOL_REACTIVITY_NOTIFY;
72
+ this.maxBytes = options.maxBytes ?? DEFAULT_STREAM_MAX_BYTES;
73
+ this.selfPeerId = options.selfPeerId;
74
+ }
75
+
76
+ send(target: string, n: NotificationV1): Promise<void> {
77
+ if (this.selfPeerId !== undefined && target === this.selfPeerId) {
78
+ // Never dial self; a co-located subscriber is delivered in-process by the forwarder host.
79
+ return Promise.resolve();
80
+ }
81
+ try {
82
+ const frame = encodeNotificationV1(n);
83
+ return sendOneWay(this.node, peerIdFromString(target), this.notifyProtocol, frame).catch((err: unknown) => {
84
+ // Best-effort, failure-isolated: a dead/unreachable target must not break the fan-out or a commit.
85
+ log("send to %s failed (swallowed): %o", target, err);
86
+ });
87
+ } catch (err) {
88
+ // Malformed notification or peer-id string: log + drop, never reject (reactivity is hint-only).
89
+ log("dropped a send to %s: %o", target, err);
90
+ return Promise.resolve();
91
+ }
92
+ }
93
+
94
+ onNotification(handler: (from: PeerRef, n: NotificationV1) => void): () => void {
95
+ this.handlers.add(handler);
96
+ return () => this.handlers.delete(handler);
97
+ }
98
+
99
+ /** Feed an inbound notify frame (called by the host's notify protocol handler). */
100
+ deliver(fromPeerId: string, frame: Uint8Array): void {
101
+ let n: NotificationV1;
102
+ try {
103
+ n = decodeNotificationV1(frame, this.maxBytes);
104
+ } catch (err) {
105
+ // A malformed frame must never throw out of a stream handler: log + drop.
106
+ log("dropped an undecodable inbound frame from %s: %o", fromPeerId, err);
107
+ return;
108
+ }
109
+ const from: PeerRef = { id: peerIdToBytes(fromPeerId) };
110
+ for (const handler of this.handlers) {
111
+ handler(from, n);
112
+ }
113
+ }
114
+ }
115
+
116
+ /**
117
+ * Register the inbound notify protocol handler: read one bounded frame and hand it to
118
+ * {@link ReactivityNotifyTransport.deliver}, then close. One-way — no reply frame (notify is strictly
119
+ * fire-and-forget; a reply would desync the dialer's {@link sendOneWay}). Mirrors the cohort-topic
120
+ * one-way handlers: a read error aborts the stream, and {@link ReactivityNotifyTransport.deliver}
121
+ * swallows a decode failure, so the handler never throws on the stream.
122
+ */
123
+ export function registerNotifyHandler(
124
+ node: Libp2p,
125
+ protocol: string,
126
+ transport: ReactivityNotifyTransport,
127
+ maxBytes = DEFAULT_STREAM_MAX_BYTES,
128
+ ): void {
129
+ void node.handle(protocol, (stream: Stream, connection: Connection) => {
130
+ void (async (): Promise<void> => {
131
+ try {
132
+ const frame = await readAllBounded(stream, maxBytes);
133
+ transport.deliver(connection.remotePeer.toString(), frame);
134
+ await stream.close();
135
+ } catch {
136
+ try {
137
+ stream.abort(new Error("reactivity notify stream handler error"));
138
+ } catch {
139
+ /* already aborted */
140
+ }
141
+ }
142
+ })();
143
+ });
144
+ }