@optimystic/db-p2p 0.24.0 → 0.24.1

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 (29) hide show
  1. package/dist/src/cohort-topic/host.js +34 -11
  2. package/dist/src/cohort-topic/host.js.map +1 -1
  3. package/dist/src/cohort-topic/stream-util.d.ts +25 -11
  4. package/dist/src/cohort-topic/stream-util.d.ts.map +1 -1
  5. package/dist/src/cohort-topic/stream-util.js +31 -19
  6. package/dist/src/cohort-topic/stream-util.js.map +1 -1
  7. package/dist/src/matchmaking/query-transport.js +3 -3
  8. package/dist/src/matchmaking/query-transport.js.map +1 -1
  9. package/dist/src/reactivity/notify-transport.d.ts +4 -4
  10. package/dist/src/reactivity/notify-transport.js +6 -6
  11. package/dist/src/reactivity/notify-transport.js.map +1 -1
  12. package/dist/src/reactivity/push-state-gossip.js +2 -2
  13. package/dist/src/reactivity/push-state-gossip.js.map +1 -1
  14. package/dist/src/reactivity/recover-transport.d.ts +6 -2
  15. package/dist/src/reactivity/recover-transport.d.ts.map +1 -1
  16. package/dist/src/reactivity/recover-transport.js +7 -3
  17. package/dist/src/reactivity/recover-transport.js.map +1 -1
  18. package/dist/src/testing/cohort-topic-mesh-harness.d.ts +13 -6
  19. package/dist/src/testing/cohort-topic-mesh-harness.d.ts.map +1 -1
  20. package/dist/src/testing/cohort-topic-mesh-harness.js +15 -6
  21. package/dist/src/testing/cohort-topic-mesh-harness.js.map +1 -1
  22. package/package.json +3 -3
  23. package/src/cohort-topic/host.ts +2932 -2901
  24. package/src/cohort-topic/stream-util.ts +147 -135
  25. package/src/matchmaking/query-transport.ts +492 -492
  26. package/src/reactivity/notify-transport.ts +6 -6
  27. package/src/reactivity/push-state-gossip.ts +2 -2
  28. package/src/reactivity/recover-transport.ts +412 -408
  29. package/src/testing/cohort-topic-mesh-harness.ts +20 -10
@@ -1,135 +1,147 @@
1
- /**
2
- * Single-frame libp2p stream helpers, shared by cohort-topic, matchmaking, and reactivity.
3
- *
4
- * These protocols exchange a single self-delimiting cohort frame each way (the db-core wire codec
5
- * already length-prefixes the body), so request/response is one `send` + one bounded read. This
6
- * mirrors FRET's `rpc/maybe-act.ts` and reuses FRET's exported `readAllBounded`, keeping the stream
7
- * lifecycle (open send close-write read close) consistent across both protocol families.
8
- *
9
- * {@link openStream} deliberately duplicates FRET's `rpc/protocols.ts#openRpcStream` connection
10
- * selection, because FRET does not export that helper (its package `exports` map exposes only the
11
- * root entry, so there is no deep import either).
12
- * NOTE: if FRET ever exports `openRpcStream`, delete {@link openStream} and call it instead — the
13
- * divergence between the two copies is exactly what let the missing `runOnLimitedConnection` flag
14
- * hide here. Same applies to `libp2p-key-network.ts#connect`, which is a third copy.
15
- */
16
-
17
- import type { Libp2p } from "libp2p";
18
- import type { Connection, PeerId, Stream } from "@libp2p/interface";
19
- import { readAllBounded } from "p2p-fret";
20
-
21
- /** Default per-frame ceiling matches FRET's 512 KiB maybe-act bound. */
22
- export const DEFAULT_STREAM_MAX_BYTES = 512 * 1024;
23
-
24
- /**
25
- * libp2p refuses to open a protocol stream over a *limited* (circuit-relay) connection unless the
26
- * caller opts in, so every peer reachable only through a relay — the steady state for browsers and
27
- * NATed peers — depends on this flag. It is a harmless no-op on a direct connection.
28
- *
29
- * NOTE: accepted tradeoff FRET's `openRpcStream` and `libp2p-key-network.ts#connect` also set
30
- * `negotiateFully: false`; deliberately not set here. It saves a round trip but defers an
31
- * unsupported-protocol failure from stream-open to the first read, which would turn
32
- * {@link sendOneWay} against a peer lacking the protocol into a silent no-op. Revisit if
33
- * stream-open latency shows up in a profile.
34
- */
35
- const STREAM_OPTIONS = { runOnLimitedConnection: true } as const;
36
-
37
- /** True for a circuit-relay ("limited") connection: libp2p stamps one with per-circuit `limits`;
38
- * sniffing `/p2p-circuit` in the multiaddr covers transports that leave `limits` unpopulated. */
39
- function isLimitedConnection(c: Connection): boolean {
40
- if ((c as { limits?: unknown }).limits != null) return true;
41
- return c.remoteAddr?.toString?.().includes("/p2p-circuit") ?? false;
42
- }
43
-
44
- /**
45
- * Open `protocol` to `peer`, reusing a healthy existing connection when there is one.
46
- *
47
- * Skips connections libp2p has not yet evicted from its index but that are no longer open, and
48
- * prefers a direct connection over a relayed one — a relayed connection can be reset once the
49
- * relay's per-circuit cap or reservation lapses, and after DCUtR upgrades a link to direct both
50
- * briefly coexist. Falls back to the relayed connection when it is the only open path.
51
- */
52
- async function openStream(node: Libp2p, peer: PeerId, protocol: string): Promise<Stream> {
53
- const open = node.getConnections(peer).filter(c => c?.status === "open" && typeof c?.newStream === "function");
54
- const chosen = open.find(c => !isLimitedConnection(c)) ?? open[0];
55
- return chosen ? await chosen.newStream([protocol], STREAM_OPTIONS) : await node.dialProtocol(peer, [protocol], STREAM_OPTIONS);
56
- }
57
-
58
- /**
59
- * Open `protocol` to `peer`, send `frame`, and read the bounded reply frame.
60
- *
61
- * NOTE: takes no `AbortSignal`, so a caller cannot set its own deadline. Bounded today anyway
62
- * `readAllBounded` self-times-out at 5s and `dialProtocol` falls back to libp2p's default dial
63
- * timeout (~30s) — so an unresponsive peer is slow, not hung. If a caller ever needs a tighter
64
- * deadline (`membership-source.fetch` walks candidate peers *sequentially*, so its worst case is
65
- * peers × dial-timeout), thread a signal through {@link openStream} the way
66
- * `libp2p-key-network.ts#connect` does.
67
- */
68
- export async function requestResponse(
69
- node: Libp2p,
70
- peer: PeerId,
71
- protocol: string,
72
- frame: Uint8Array,
73
- maxBytes = DEFAULT_STREAM_MAX_BYTES,
74
- ): Promise<Uint8Array> {
75
- let stream: Stream | undefined;
76
- try {
77
- stream = await openStream(node, peer, protocol);
78
- stream.send(frame);
79
- await stream.close();
80
- return await readAllBounded(stream, maxBytes);
81
- } finally {
82
- if (stream != null) {
83
- try {
84
- await stream.close();
85
- } catch {
86
- /* already closed */
87
- }
88
- }
89
- }
90
- }
91
-
92
- /** Open `protocol` to `peer` and send `frame` without awaiting a reply (fire-and-forget gossip). */
93
- export async function sendOneWay(node: Libp2p, peer: PeerId, protocol: string, frame: Uint8Array): Promise<void> {
94
- let stream: Stream | undefined;
95
- try {
96
- stream = await openStream(node, peer, protocol);
97
- stream.send(frame);
98
- await stream.close();
99
- } finally {
100
- if (stream != null) {
101
- try {
102
- await stream.close();
103
- } catch {
104
- /* already closed */
105
- }
106
- }
107
- }
108
- }
109
-
110
- /** Register a request/response handler for `protocol`: read one bounded frame, reply with one frame. */
111
- export function handleRequestResponse(
112
- node: Libp2p,
113
- protocol: string,
114
- handle: (frame: Uint8Array, from: PeerId) => Promise<Uint8Array | undefined>,
115
- maxBytes = DEFAULT_STREAM_MAX_BYTES,
116
- ): void {
117
- void node.handle(protocol, (stream: Stream, connection: Connection) => {
118
- void (async (): Promise<void> => {
119
- try {
120
- const frame = await readAllBounded(stream, maxBytes);
121
- const reply = await handle(frame, connection.remotePeer);
122
- if (reply !== undefined) {
123
- stream.send(reply);
124
- }
125
- await stream.close();
126
- } catch {
127
- try {
128
- stream.abort(new Error("cohort-topic stream handler error"));
129
- } catch {
130
- /* already aborted */
131
- }
132
- }
133
- })();
134
- });
135
- }
1
+ /**
2
+ * Single-frame libp2p stream helpers, shared by cohort-topic, matchmaking, and reactivity.
3
+ *
4
+ * These protocols exchange a single varint-length-prefixed frame each way, via FRET's exported
5
+ * `sendFramed` / `readFramed` pair — the varint prefix is what delimits the frame on the wire
6
+ * (the db-core codec's own internal length prefix travels *inside* the framed body and plays no
7
+ * part in stream delimiting). Request/response is one framed send + one framed read, matching
8
+ * FRET's own four RPC protocols and the `it-length-prefixed` framing the rest of `db-p2p`
9
+ * (`protocol-client.ts`, the cluster/repo/sync/dispute services) already uses, keeping the stream
10
+ * lifecycle (open send close-write read close) consistent across all protocol families.
11
+ *
12
+ * NOTE: every `sendFramed` here discards its boolean result (`false` = "write accepted, transport
13
+ * buffer now full"), so these helpers apply no backpressure. Harmless while each protocol writes
14
+ * exactly one bounded frame per stream and then closes; if a caller ever writes repeatedly on one
15
+ * stream, honor the flag by awaiting the stream's `'drain'` event the way FRET's `rpcRequest` does.
16
+ *
17
+ * {@link openStream} deliberately duplicates FRET's `rpc/protocols.ts#openRpcStream` connection
18
+ * selection. FRET now exports `openRpcStream`, but the accepted tradeoff on
19
+ * {@link STREAM_OPTIONS} is why the local copy stays: FRET's helper pins `negotiateFully: false`,
20
+ * which this module deliberately does not set — swapping would silently reverse that decision.
21
+ * Same applies to `libp2p-key-network.ts#connect`, which is a third copy.
22
+ */
23
+
24
+ import type { Libp2p } from "libp2p";
25
+ import type { Connection, PeerId, Stream } from "@libp2p/interface";
26
+ import { readFramed, sendFramed } from "p2p-fret";
27
+
28
+ /** Default per-frame ceiling — matches FRET's 512 KiB maybe-act bound. */
29
+ export const DEFAULT_STREAM_MAX_BYTES = 512 * 1024;
30
+
31
+ /**
32
+ * libp2p refuses to open a protocol stream over a *limited* (circuit-relay) connection unless the
33
+ * caller opts in, so every peer reachable only through a relay — the steady state for browsers and
34
+ * NATed peers — depends on this flag. It is a harmless no-op on a direct connection.
35
+ *
36
+ * NOTE: accepted tradeoff — FRET's `openRpcStream` and `libp2p-key-network.ts#connect` also set
37
+ * `negotiateFully: false`; deliberately not set here. It saves a round trip but defers an
38
+ * unsupported-protocol failure from stream-open to the first read, which would turn
39
+ * {@link sendOneWay} against a peer lacking the protocol into a silent no-op. Revisit if
40
+ * stream-open latency shows up in a profile.
41
+ */
42
+ const STREAM_OPTIONS = { runOnLimitedConnection: true } as const;
43
+
44
+ /** True for a circuit-relay ("limited") connection: libp2p stamps one with per-circuit `limits`;
45
+ * sniffing `/p2p-circuit` in the multiaddr covers transports that leave `limits` unpopulated. */
46
+ function isLimitedConnection(c: Connection): boolean {
47
+ if ((c as { limits?: unknown }).limits != null) return true;
48
+ return c.remoteAddr?.toString?.().includes("/p2p-circuit") ?? false;
49
+ }
50
+
51
+ /**
52
+ * Open `protocol` to `peer`, reusing a healthy existing connection when there is one.
53
+ *
54
+ * Skips connections libp2p has not yet evicted from its index but that are no longer open, and
55
+ * prefers a direct connection over a relayed one — a relayed connection can be reset once the
56
+ * relay's per-circuit cap or reservation lapses, and after DCUtR upgrades a link to direct both
57
+ * briefly coexist. Falls back to the relayed connection when it is the only open path.
58
+ */
59
+ async function openStream(node: Libp2p, peer: PeerId, protocol: string): Promise<Stream> {
60
+ const open = node.getConnections(peer).filter(c => c?.status === "open" && typeof c?.newStream === "function");
61
+ const chosen = open.find(c => !isLimitedConnection(c)) ?? open[0];
62
+ return chosen ? await chosen.newStream([protocol], STREAM_OPTIONS) : await node.dialProtocol(peer, [protocol], STREAM_OPTIONS);
63
+ }
64
+
65
+ /**
66
+ * Open `protocol` to `peer`, send `frame`, and read the bounded reply frame.
67
+ *
68
+ * NOTE: takes no `AbortSignal`, so a caller cannot set its own deadline. Bounded today anyway —
69
+ * `readFramed` self-times-out at 5s and `dialProtocol` falls back to libp2p's default dial
70
+ * timeout (~30s) — so an unresponsive peer is slow, not hung. If a caller ever needs a tighter
71
+ * deadline (`membership-source.fetch` walks candidate peers *sequentially*, so its worst case is
72
+ * peers × dial-timeout), thread a signal through {@link openStream} the way
73
+ * `libp2p-key-network.ts#connect` does.
74
+ */
75
+ export async function requestResponse(
76
+ node: Libp2p,
77
+ peer: PeerId,
78
+ protocol: string,
79
+ frame: Uint8Array,
80
+ maxBytes = DEFAULT_STREAM_MAX_BYTES,
81
+ ): Promise<Uint8Array> {
82
+ let stream: Stream | undefined;
83
+ try {
84
+ stream = await openStream(node, peer, protocol);
85
+ sendFramed(stream, frame);
86
+ await stream.close();
87
+ return await readFramed(stream, maxBytes);
88
+ } finally {
89
+ if (stream != null) {
90
+ try {
91
+ await stream.close();
92
+ } catch {
93
+ /* already closed */
94
+ }
95
+ }
96
+ }
97
+ }
98
+
99
+ /** Open `protocol` to `peer` and send `frame` without awaiting a reply (fire-and-forget gossip). */
100
+ export async function sendOneWay(node: Libp2p, peer: PeerId, protocol: string, frame: Uint8Array): Promise<void> {
101
+ let stream: Stream | undefined;
102
+ try {
103
+ stream = await openStream(node, peer, protocol);
104
+ sendFramed(stream, frame);
105
+ await stream.close();
106
+ } finally {
107
+ if (stream != null) {
108
+ try {
109
+ await stream.close();
110
+ } catch {
111
+ /* already closed */
112
+ }
113
+ }
114
+ }
115
+ }
116
+
117
+ /**
118
+ * Register a request/response handler for `protocol`: read one bounded frame, reply with one frame.
119
+ *
120
+ * A `handle` that returns `undefined` (a drop, a gate rejection, no serving engine) replies with an
121
+ * explicit **zero-length frame** rather than silence: `readFramed` treats end-of-stream as a
122
+ * truncation error, so "no reply" must travel in-band — the dialer's {@link requestResponse}
123
+ * resolves it as empty bytes, exactly what the old read-to-EOF returned for a silent close.
124
+ */
125
+ export function handleRequestResponse(
126
+ node: Libp2p,
127
+ protocol: string,
128
+ handle: (frame: Uint8Array, from: PeerId) => Promise<Uint8Array | undefined>,
129
+ maxBytes = DEFAULT_STREAM_MAX_BYTES,
130
+ ): void {
131
+ void node.handle(protocol, (stream: Stream, connection: Connection) => {
132
+ void (async (): Promise<void> => {
133
+ try {
134
+ const frame = await readFramed(stream, maxBytes);
135
+ const reply = await handle(frame, connection.remotePeer);
136
+ sendFramed(stream, reply ?? new Uint8Array(0));
137
+ await stream.close();
138
+ } catch {
139
+ try {
140
+ stream.abort(new Error("cohort-topic stream handler error"));
141
+ } catch {
142
+ /* already aborted */
143
+ }
144
+ }
145
+ })();
146
+ });
147
+ }