@optimystic/db-p2p 0.24.0 → 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.
- package/dist/src/cluster/service.d.ts +8 -0
- package/dist/src/cluster/service.d.ts.map +1 -1
- package/dist/src/cluster/service.js +16 -4
- package/dist/src/cluster/service.js.map +1 -1
- package/dist/src/cohort-topic/host.js +34 -11
- package/dist/src/cohort-topic/host.js.map +1 -1
- package/dist/src/cohort-topic/stream-util.d.ts +25 -11
- package/dist/src/cohort-topic/stream-util.d.ts.map +1 -1
- package/dist/src/cohort-topic/stream-util.js +31 -19
- package/dist/src/cohort-topic/stream-util.js.map +1 -1
- package/dist/src/libp2p-key-network.d.ts +68 -0
- package/dist/src/libp2p-key-network.d.ts.map +1 -1
- package/dist/src/libp2p-key-network.js +123 -14
- package/dist/src/libp2p-key-network.js.map +1 -1
- package/dist/src/libp2p-node-base.d.ts.map +1 -1
- package/dist/src/libp2p-node-base.js +8 -5
- package/dist/src/libp2p-node-base.js.map +1 -1
- package/dist/src/logger.d.ts +2 -2
- package/dist/src/logger.js +2 -2
- package/dist/src/matchmaking/query-transport.js +3 -3
- package/dist/src/matchmaking/query-transport.js.map +1 -1
- package/dist/src/peer-address-book.d.ts +69 -0
- package/dist/src/peer-address-book.d.ts.map +1 -1
- package/dist/src/peer-address-book.js +110 -15
- package/dist/src/peer-address-book.js.map +1 -1
- package/dist/src/reactivity/notify-transport.d.ts +4 -4
- package/dist/src/reactivity/notify-transport.js +6 -6
- package/dist/src/reactivity/notify-transport.js.map +1 -1
- package/dist/src/reactivity/push-state-gossip.js +2 -2
- package/dist/src/reactivity/push-state-gossip.js.map +1 -1
- package/dist/src/reactivity/recover-transport.d.ts +6 -2
- package/dist/src/reactivity/recover-transport.d.ts.map +1 -1
- package/dist/src/reactivity/recover-transport.js +7 -3
- package/dist/src/reactivity/recover-transport.js.map +1 -1
- package/dist/src/repo/service.d.ts +6 -0
- package/dist/src/repo/service.d.ts.map +1 -1
- package/dist/src/repo/service.js +12 -2
- package/dist/src/repo/service.js.map +1 -1
- package/dist/src/routing/libp2p-known-peers.d.ts.map +1 -1
- package/dist/src/routing/libp2p-known-peers.js +5 -0
- package/dist/src/routing/libp2p-known-peers.js.map +1 -1
- package/dist/src/testing/cohort-topic-mesh-harness.d.ts +13 -6
- package/dist/src/testing/cohort-topic-mesh-harness.d.ts.map +1 -1
- package/dist/src/testing/cohort-topic-mesh-harness.js +15 -6
- package/dist/src/testing/cohort-topic-mesh-harness.js.map +1 -1
- package/package.json +3 -3
- package/src/cluster/service.ts +305 -293
- package/src/cohort-topic/host.ts +2932 -2901
- package/src/cohort-topic/stream-util.ts +147 -135
- package/src/libp2p-key-network.ts +1235 -1120
- package/src/libp2p-node-base.ts +1678 -1675
- package/src/logger.ts +27 -27
- package/src/matchmaking/query-transport.ts +492 -492
- package/src/peer-address-book.ts +266 -149
- package/src/reactivity/notify-transport.ts +144 -144
- package/src/reactivity/push-state-gossip.ts +291 -291
- package/src/reactivity/recover-transport.ts +412 -408
- package/src/repo/service.ts +323 -313
- package/src/routing/libp2p-known-peers.ts +31 -26
- package/src/testing/cohort-topic-mesh-harness.ts +673 -663
|
@@ -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})
|
|
11
|
-
*
|
|
12
|
-
*
|
|
13
|
-
*
|
|
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 {
|
|
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
|
|
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
|
+
}
|