@optimystic/db-p2p 0.24.1 → 0.24.2

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 (35) hide show
  1. package/dist/src/cluster/service.d.ts +8 -0
  2. package/dist/src/cluster/service.d.ts.map +1 -1
  3. package/dist/src/cluster/service.js +16 -4
  4. package/dist/src/cluster/service.js.map +1 -1
  5. package/dist/src/libp2p-key-network.d.ts +68 -0
  6. package/dist/src/libp2p-key-network.d.ts.map +1 -1
  7. package/dist/src/libp2p-key-network.js +123 -14
  8. package/dist/src/libp2p-key-network.js.map +1 -1
  9. package/dist/src/libp2p-node-base.d.ts.map +1 -1
  10. package/dist/src/libp2p-node-base.js +8 -5
  11. package/dist/src/libp2p-node-base.js.map +1 -1
  12. package/dist/src/logger.d.ts +2 -2
  13. package/dist/src/logger.js +2 -2
  14. package/dist/src/peer-address-book.d.ts +69 -0
  15. package/dist/src/peer-address-book.d.ts.map +1 -1
  16. package/dist/src/peer-address-book.js +110 -15
  17. package/dist/src/peer-address-book.js.map +1 -1
  18. package/dist/src/repo/service.d.ts +6 -0
  19. package/dist/src/repo/service.d.ts.map +1 -1
  20. package/dist/src/repo/service.js +12 -2
  21. package/dist/src/repo/service.js.map +1 -1
  22. package/dist/src/routing/libp2p-known-peers.d.ts.map +1 -1
  23. package/dist/src/routing/libp2p-known-peers.js +5 -0
  24. package/dist/src/routing/libp2p-known-peers.js.map +1 -1
  25. package/package.json +2 -2
  26. package/src/cluster/service.ts +305 -293
  27. package/src/libp2p-key-network.ts +1235 -1120
  28. package/src/libp2p-node-base.ts +1678 -1675
  29. package/src/logger.ts +27 -27
  30. package/src/peer-address-book.ts +266 -149
  31. package/src/reactivity/notify-transport.ts +144 -144
  32. package/src/reactivity/push-state-gossip.ts +291 -291
  33. package/src/repo/service.ts +323 -313
  34. package/src/routing/libp2p-known-peers.ts +31 -26
  35. package/src/testing/cohort-topic-mesh-harness.ts +673 -673
@@ -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}) encodes
11
- * the body; this layer rides one varint-length-prefixed frame each way over the notify protocol, reusing
12
- * the cohort-topic {@link sendOneWay} / FRET `readFramed` stream lifecycle so the two protocol families
13
- * 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 { readFramed } 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 readFramed(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}) encodes
11
+ * the body; this layer rides one varint-length-prefixed frame each way over the notify protocol, reusing
12
+ * the cohort-topic {@link sendOneWay} / FRET `readFramed` stream lifecycle so the two protocol families
13
+ * 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 { readFramed } 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 readFramed(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
+ }