first commit
This commit is contained in:
@@ -0,0 +1,78 @@
|
||||
# connection
|
||||
|
||||
<!-- begin:auto-docs -->
|
||||
_(no submodules)_
|
||||
<!-- end:auto-docs -->
|
||||
## Purpose
|
||||
|
||||
Manages the libp2p node lifecycle on behalf of `Libp2pTransport`. Three small async functions wrap the injected factory: one to construct and start a node, one to dial a peer by multiaddr, and one to stop a node cleanly. All three translate every underlying libp2p surface error into a typed `NetLibp2pError` so callers branch on `error.code` without needing to introspect the libp2p exception hierarchy.
|
||||
|
||||
## Features
|
||||
|
||||
- `createAndStartNode(factory, config, signal?)` — composes factory + `start()`; emits OTel outcome on every path.
|
||||
- `dialPeerAddress(node, address, signal?)` — wraps `node.dial(multiaddr)` with OTel dial-duration tracking.
|
||||
- `stopNode(node)` — translates `node.stop()` failures to `TRANSPORT_CLOSED`.
|
||||
- Honours pre-aborted `AbortSignal` at every step; aborted-between-factory-and-start is treated as a complete failure and the freshly-created node is stopped to avoid leaking native handles.
|
||||
- Records `connections_opened_total{outcome}` and `dial_ms` automatically.
|
||||
|
||||
## Usage: Basic Example
|
||||
|
||||
```ts
|
||||
import {
|
||||
createAndStartNode,
|
||||
dialPeerAddress,
|
||||
stopNode,
|
||||
} from "@suaveplan/network-transport-libp2p";
|
||||
|
||||
const node = await createAndStartNode(libp2pFactory, {
|
||||
subTransport: "websockets",
|
||||
addresses: { listen: ["/ip4/0.0.0.0/tcp/0/ws"] },
|
||||
bootstrap: ["/dns4/bootstrap.example/tcp/443/wss/p2p/Qm…"],
|
||||
maxConnections: 100,
|
||||
dialTimeoutMs: 10_000,
|
||||
protocolId: "/suaveplan/network/1.0.0",
|
||||
});
|
||||
|
||||
await dialPeerAddress(node, "/dns4/peer.example/tcp/443/wss/p2p/Qm…");
|
||||
await stopNode(node);
|
||||
```
|
||||
|
||||
## Usage: Advanced Example
|
||||
|
||||
```ts
|
||||
import {
|
||||
createAndStartNode,
|
||||
dialPeerAddress,
|
||||
NET_LIBP2P_CODES,
|
||||
NetLibp2pError,
|
||||
} from "@suaveplan/network-transport-libp2p";
|
||||
|
||||
const ac = new AbortController();
|
||||
const cancel = setTimeout(() => ac.abort(), 30_000);
|
||||
try {
|
||||
const node = await createAndStartNode(factory, baseConfig, ac.signal);
|
||||
for (const addr of bootstrapAddresses) {
|
||||
try {
|
||||
await dialPeerAddress(node, addr, ac.signal);
|
||||
} catch (err) {
|
||||
if (err instanceof NetLibp2pError && err.code === NET_LIBP2P_CODES.DIAL_FAILED) {
|
||||
console.warn(`bootstrap miss: ${addr}`);
|
||||
continue;
|
||||
}
|
||||
throw err;
|
||||
}
|
||||
}
|
||||
} finally {
|
||||
clearTimeout(cancel);
|
||||
}
|
||||
```
|
||||
|
||||
## API Reference
|
||||
|
||||
- `createAndStartNode(factory, config, signal?): Promise<Libp2pNodeLike>` — throws `BOOTSTRAP_FAILED` on factory or start failure (and on signal pre-abort).
|
||||
- `dialPeerAddress(node, address, signal?): Promise<void>` — throws `DIAL_FAILED` with `context: { address }` on failure.
|
||||
- `stopNode(node): Promise<void>` — throws `TRANSPORT_CLOSED` on stop failure.
|
||||
|
||||
## Implementation Notes
|
||||
|
||||
The factory pattern is the single critical design choice here — the package never imports `libp2p` at module-eval. The "aborted after factory but before start" branch is intentionally treated as a hard failure (rather than letting the node start and trusting the caller to stop it later); leaking a half-started libp2p node into the runtime is the kind of bug that's near-impossible to reproduce in CI.
|
||||
@@ -0,0 +1,63 @@
|
||||
# errors
|
||||
|
||||
<!-- begin:auto-docs -->
|
||||
_(no submodules)_
|
||||
<!-- end:auto-docs -->
|
||||
## Purpose
|
||||
|
||||
Defines the structured-error registry for `@suaveplan/network-transport-libp2p`. Every public boundary in the package raises a `NetLibp2pError` carrying a stable `code` drawn from `NET_LIBP2P_CODES`. Consumers branch on `error.code` rather than parsing message text — this is the recommended pattern across the entire `@suaveplan/*` ecosystem.
|
||||
|
||||
## Features
|
||||
|
||||
- 11-code registry (`BOOTSTRAP_FAILED`, `DIAL_FAILED`, `STREAM_FAILED`, `PROTOCOL_NOT_SUPPORTED`, `IDENTIFY_REJECTED`, `INVALID_OPTIONS`, `DISPOSED`, `NOT_RUNNING`, `PEER_ALREADY_CONNECTED`, `PEER_UNKNOWN`, `TRANSPORT_CLOSED`).
|
||||
- Built via `defineCodes("NET_LIBP2P", [...])` — yields fully qualified codes like `"NET_LIBP2P.DIAL_FAILED"` that don't collide with other packages.
|
||||
- `NetLibp2pError extends SuaveplanError<NetLibp2pErrorCode>` so callers get cause-chain traversal, structured context, redaction, and OTel span-event integration for free.
|
||||
- Frozen registry — adding new codes requires editing this file and bumping the package version.
|
||||
|
||||
## Usage: Basic Example
|
||||
|
||||
```ts
|
||||
import {
|
||||
NET_LIBP2P_CODES,
|
||||
NetLibp2pError,
|
||||
} from "@suaveplan/network-transport-libp2p";
|
||||
|
||||
try {
|
||||
await transport.start();
|
||||
} catch (err) {
|
||||
if (err instanceof NetLibp2pError && err.code === NET_LIBP2P_CODES.BOOTSTRAP_FAILED) {
|
||||
// retry with backoff
|
||||
}
|
||||
throw err;
|
||||
}
|
||||
```
|
||||
|
||||
## Usage: Advanced Example
|
||||
|
||||
```ts
|
||||
import { NET_LIBP2P_CODES, NetLibp2pError } from "@suaveplan/network-transport-libp2p";
|
||||
|
||||
function classify(err: unknown): "retryable" | "fatal" | "config" {
|
||||
if (!(err instanceof NetLibp2pError)) return "fatal";
|
||||
switch (err.code) {
|
||||
case NET_LIBP2P_CODES.INVALID_OPTIONS:
|
||||
return "config";
|
||||
case NET_LIBP2P_CODES.DIAL_FAILED:
|
||||
case NET_LIBP2P_CODES.STREAM_FAILED:
|
||||
case NET_LIBP2P_CODES.BOOTSTRAP_FAILED:
|
||||
return "retryable";
|
||||
default:
|
||||
return "fatal";
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
## API Reference
|
||||
|
||||
- `NET_LIBP2P_CODES`: frozen lookup of code-name → fully qualified code string.
|
||||
- `NetLibp2pErrorCode`: literal union over the registry.
|
||||
- `NetLibp2pError(code, messageKey, options?)`: constructor accepts the standard `SuaveplanErrorOptions` shape, including `cause` (Error chaining) and `context` (structured details).
|
||||
|
||||
## Implementation Notes
|
||||
|
||||
We deliberately avoid a single catch-all code. Hyperswarm's reputation engine taught us that operators need to slice failures by reason to decide what to do — `STREAM_FAILED` warrants reputation tracking; `DIAL_FAILED` warrants retry; `INVALID_OPTIONS` warrants a config-fix; `BOOTSTRAP_FAILED` warrants surfacing to the user. Adding more codes is a non-breaking change as long as nothing existing is removed or repurposed.
|
||||
@@ -0,0 +1,60 @@
|
||||
# frame
|
||||
|
||||
## Purpose
|
||||
|
||||
Implements length-prefixed binary framing for envelopes traversing a libp2p stream. Each frame is a 4-byte big-endian length header followed by an exact-length MessagePack payload. The `FrameDecoder` is stateful — it buffers partial reads across `push()` calls so callers can stream chunks directly from `stream.source` without re-implementing the buffering protocol on every transport.
|
||||
|
||||
## Features
|
||||
|
||||
- `encodeFrame(envelope)` → `Uint8Array` length-prefix + MessagePack payload.
|
||||
- `FrameDecoder.push(chunk)` → `MessageEnvelope[]`, emits zero or more complete envelopes.
|
||||
- `FrameDecoder.bufferedBytes` — observability hook.
|
||||
- `FrameDecoder.reset()` — drops the partial buffer on socket close.
|
||||
- `DEFAULT_MAX_FRAME_SIZE = 1 MiB` plaintext cap (configurable per-decoder).
|
||||
- `STREAM_FAILED` on zero-length headers and oversized frames; `INVALID_OPTIONS` if `maxFrameSize` is non-positive.
|
||||
|
||||
## Usage: Basic Example
|
||||
|
||||
```ts
|
||||
import { encodeFrame, FrameDecoder } from "@suaveplan/network-transport-libp2p";
|
||||
|
||||
const frame = encodeFrame({ id: "m1", type: "ping", payload: { n: 1 } });
|
||||
const decoder = new FrameDecoder();
|
||||
const envelopes = decoder.push(frame);
|
||||
console.log(envelopes[0]?.id); // "m1"
|
||||
```
|
||||
|
||||
## Usage: Advanced Example
|
||||
|
||||
```ts
|
||||
import { FrameDecoder } from "@suaveplan/network-transport-libp2p";
|
||||
|
||||
async function* drainStream(
|
||||
stream: { source: AsyncIterable<{ subarray(): Uint8Array }> },
|
||||
maxFrameSize = 256 * 1024
|
||||
) {
|
||||
const decoder = new FrameDecoder(maxFrameSize);
|
||||
for await (const chunk of stream.source) {
|
||||
for (const envelope of decoder.push(chunk.subarray())) {
|
||||
yield envelope;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
for await (const envelope of drainStream(someStream, 65_536)) {
|
||||
handleEnvelope(envelope);
|
||||
}
|
||||
```
|
||||
|
||||
## API Reference
|
||||
|
||||
- `DEFAULT_MAX_FRAME_SIZE: 1048576`.
|
||||
- `encodeFrame(envelope: MessageEnvelope): Uint8Array`.
|
||||
- `class FrameDecoder` — constructor: `(maxFrameSize?: number)` (default `DEFAULT_MAX_FRAME_SIZE`).
|
||||
- `push(chunk: Uint8Array): MessageEnvelope[]` — appends chunk, returns every complete envelope; throws `NetLibp2pError(STREAM_FAILED)` on zero-length, oversized, or malformed payloads.
|
||||
- `get bufferedBytes(): number`.
|
||||
- `reset(): void`.
|
||||
|
||||
## Implementation Notes
|
||||
|
||||
The decoder's hot path uses a single `Uint8Array.slice(4 + length)` per emitted frame. For workloads dominated by tiny frames this allocates more than a copy-into-ring-buffer design, but the operational simplicity has been worth the trade — the equivalent ring-buffer in the upstream Hyperswarm package was a recurring source of off-by-one bugs. We may revisit this design if a benchmarked workload demands it.
|
||||
@@ -0,0 +1,66 @@
|
||||
# peer-message
|
||||
|
||||
## Purpose
|
||||
|
||||
Defines the wire-format codec for envelopes flowing between libp2p peers. Encoding uses the `@suaveplan/serialize` MessagePack binary profile, which round-trips Date / Map / Set / BigInt / Uint8Array natively — strictly more capable than `JSON.stringify`. Every error surfaces as a typed `NetLibp2pError(STREAM_FAILED)` so the transport layer can route malformed inputs to its reputation engine.
|
||||
|
||||
## Features
|
||||
|
||||
- `encodePeerMessage(envelope)` → `Uint8Array` using the shared MessagePack profile.
|
||||
- `decodePeerMessage(bytes)` → `MessageEnvelope`, throwing `STREAM_FAILED` on malformed input.
|
||||
- `peerIdToString(id)` → `string`, the single point that normalises libp2p's peer-id object to a string `PeerId`.
|
||||
- Zero dependency on the concrete `libp2p` package — types only.
|
||||
|
||||
## Usage: Basic Example
|
||||
|
||||
```ts
|
||||
import {
|
||||
decodePeerMessage,
|
||||
encodePeerMessage,
|
||||
} from "@suaveplan/network-transport-libp2p";
|
||||
|
||||
const bytes = encodePeerMessage({ id: "m1", type: "ping", payload: { n: 1 } });
|
||||
const back = decodePeerMessage(bytes);
|
||||
console.log(back.id); // "m1"
|
||||
```
|
||||
|
||||
## Usage: Advanced Example
|
||||
|
||||
```ts
|
||||
import {
|
||||
decodePeerMessage,
|
||||
encodePeerMessage,
|
||||
NET_LIBP2P_CODES,
|
||||
NetLibp2pError,
|
||||
peerIdToString,
|
||||
} from "@suaveplan/network-transport-libp2p";
|
||||
|
||||
function safeDecode(bytes: Uint8Array): { ok: true; envelope: unknown } | { ok: false; code: string } {
|
||||
try {
|
||||
const envelope = decodePeerMessage(bytes);
|
||||
return { ok: true, envelope };
|
||||
} catch (err) {
|
||||
if (err instanceof NetLibp2pError && err.code === NET_LIBP2P_CODES.STREAM_FAILED) {
|
||||
return { ok: false, code: err.code };
|
||||
}
|
||||
throw err;
|
||||
}
|
||||
}
|
||||
|
||||
const sender = peerIdToString({ toString: () => "Qm-sender" });
|
||||
const encoded = encodePeerMessage({ id: "x", type: "tx", payload: new Map([["k", 1n]]) });
|
||||
```
|
||||
|
||||
## API Reference
|
||||
|
||||
- `encodePeerMessage(envelope: MessageEnvelope): Uint8Array`.
|
||||
- `decodePeerMessage(bytes: Uint8Array): MessageEnvelope` — throws `NetLibp2pError(STREAM_FAILED)` on malformed input.
|
||||
- `peerIdToString(id: { toString(): string }): string`.
|
||||
|
||||
## Implementation Notes
|
||||
|
||||
Choosing `@suaveplan/serialize`'s MessagePack profile over raw JSON gives ≈30 % smaller frames on typical payloads while preserving Map / Set / BigInt / Date / Uint8Array round-trip semantics. The decode path swallows the underlying `msgpack-decode` error and rebrands it as `STREAM_FAILED` because the transport layer cannot distinguish "wire format violation" from "byzantine peer" — both warrant the same reputation impact and stream tear-down.
|
||||
|
||||
The peer-id pass-through is intentionally minimal. libp2p peer-ids are multihash-encoded buffers but their `toString()` returns the stable base58btc form; we treat that string as opaque throughout the transport so the package never needs to depend on `@libp2p/peer-id` or any specific peer-id concrete class. The decoupling pays off in two places: tests can supply a trivial `{ toString }` object as a peer-id without instantiating real cryptographic identities, and downstream consumers can integrate alternative peer-id systems by wrapping their identifier behind the same single-method contract.
|
||||
|
||||
Future work: stream-level codec swapping (caller-injected encode/decode) so the wire format can be replaced with CBOR, Protobuf, or any other byte-exact codec without changing the transport. The structural pass-through cost of MessagePack on every send is negligible compared to the libp2p transport stack itself, so the optimisation has not yet been worth the API surface.
|
||||
@@ -0,0 +1,66 @@
|
||||
# send
|
||||
|
||||
## Purpose
|
||||
|
||||
Outbound stream-based envelope shipping over libp2p. Each `send` opens a new protocol stream, writes a single framed envelope, and closes — modelled after libp2p's standard request/response idiom. The functions thread an `AbortSignal` to cancel the dial and bound the in-flight write with a per-call timeout so a stalled remote cannot pin the calling fiber.
|
||||
|
||||
## Features
|
||||
|
||||
- `sendEnvelopeFrame(node, peerId, protocolId, frame, timeoutMs, signal?)` for callers that pre-encoded a frame (useful for broadcast fan-out).
|
||||
- `sendEnvelope(node, peerId, protocolId, envelope, timeoutMs, signal?)` convenience that encodes inline.
|
||||
- Translates every libp2p surface error into `NetLibp2pError(STREAM_FAILED)` with the cause preserved.
|
||||
- Records `streams_opened_total`, `stream_ttfb_ms`, and `bytes_total{direction=out}` on success.
|
||||
- Honours pre-aborted `AbortSignal` instantly with `STREAM_FAILED`.
|
||||
|
||||
## Usage: Basic Example
|
||||
|
||||
```ts
|
||||
import { sendEnvelope } from "@suaveplan/network-transport-libp2p";
|
||||
|
||||
await sendEnvelope(
|
||||
node,
|
||||
{ toString: () => "Qm-peer-id" },
|
||||
"/suaveplan/network/1.0.0",
|
||||
{ id: "m1", type: "ping", payload: { n: 1 } },
|
||||
5_000
|
||||
);
|
||||
```
|
||||
|
||||
## Usage: Advanced Example
|
||||
|
||||
```ts
|
||||
import {
|
||||
encodeFrame,
|
||||
sendEnvelopeFrame,
|
||||
} from "@suaveplan/network-transport-libp2p";
|
||||
|
||||
async function broadcast(node: Libp2pNodeLike, peers: Libp2pPeerIdLike[]) {
|
||||
const envelope = { id: "bcast", type: "tick", payload: {} } as MessageEnvelope;
|
||||
const frame = encodeFrame(envelope); // encode once, reuse
|
||||
const controller = new AbortController();
|
||||
setTimeout(() => controller.abort(), 1_000);
|
||||
await Promise.allSettled(
|
||||
peers.map((p) =>
|
||||
sendEnvelopeFrame(
|
||||
node,
|
||||
p,
|
||||
"/suaveplan/network/1.0.0",
|
||||
frame,
|
||||
5_000,
|
||||
controller.signal
|
||||
)
|
||||
)
|
||||
);
|
||||
}
|
||||
```
|
||||
|
||||
## API Reference
|
||||
|
||||
- `sendEnvelopeFrame(node, peerId, protocolId, frame, timeoutMs, signal?): Promise<void>`.
|
||||
- `sendEnvelope(node, peerId, protocolId, envelope, timeoutMs, signal?): Promise<void>`.
|
||||
|
||||
Both throw `NetLibp2pError(STREAM_FAILED)` on dialProtocol failure, on `sink`/`close` failure, on timeout, or on a pre-aborted signal.
|
||||
|
||||
## Implementation Notes
|
||||
|
||||
The function builds a manual `Promise` rather than using `async`/`await` because we need an explicit `setTimeout` cancellable from both the success and failure paths. The pattern mirrors the upstream Hyperswarm and WebRTC transport implementations exactly. Reusing a pre-encoded frame across many peers (broadcast) saves N-1 MessagePack encodes — for chatty fan-out workloads this is significant.
|
||||
@@ -0,0 +1,74 @@
|
||||
# otel
|
||||
|
||||
<!-- begin:auto-docs -->
|
||||
_(no submodules)_
|
||||
<!-- end:auto-docs -->
|
||||
## Purpose
|
||||
|
||||
Centralises the OpenTelemetry instrument creation and span helpers for the libp2p transport. Every counter, histogram, and gauge that other modules emit goes through a helper exported here — this keeps OTel boilerplate out of the messaging and transport code and makes the metric surface easy to audit in one place.
|
||||
|
||||
## Features
|
||||
|
||||
- `connections_opened_total{outcome}` counter — total connection-open attempts.
|
||||
- `streams_opened_total` counter — opened libp2p protocol streams.
|
||||
- `bytes_total{direction}` counter — bytes flowing in/out.
|
||||
- `dial_ms` histogram — wall-clock duration of dialing a peer.
|
||||
- `stream_ttfb_ms` histogram — wall-clock time to first byte on outbound streams.
|
||||
- `active_peers` up/down counter (gauge) — currently-connected peer count.
|
||||
- `withLibp2pSpan(name, fn)` — async-span wrapper that always ends the span.
|
||||
- All record helpers clamp non-positive values to zero so out-of-order timestamps don't poison the histograms.
|
||||
|
||||
## Usage: Basic Example
|
||||
|
||||
```ts
|
||||
import { recordDial, recordBytes, withLibp2pSpan } from "@suaveplan/network-transport-libp2p";
|
||||
|
||||
await withLibp2pSpan("custom.op", async () => {
|
||||
const t0 = Date.now();
|
||||
// ...do work
|
||||
recordDial(Date.now() - t0);
|
||||
recordBytes("out", 1024);
|
||||
});
|
||||
```
|
||||
|
||||
## Usage: Advanced Example
|
||||
|
||||
```ts
|
||||
import {
|
||||
adjustActivePeers,
|
||||
recordConnectionOutcome,
|
||||
recordStreamOpened,
|
||||
recordStreamTtfb,
|
||||
} from "@suaveplan/network-transport-libp2p";
|
||||
|
||||
class ObservabilityAdapter {
|
||||
onPeerJoin(peer: string): void {
|
||||
adjustActivePeers(+1);
|
||||
recordConnectionOutcome("ok");
|
||||
}
|
||||
onPeerLeave(peer: string): void {
|
||||
adjustActivePeers(-1);
|
||||
}
|
||||
onStreamOpened(ttfbMs: number): void {
|
||||
recordStreamOpened();
|
||||
recordStreamTtfb(ttfbMs);
|
||||
}
|
||||
onPeerRejected(reason: string): void {
|
||||
recordConnectionOutcome("rejected");
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
## API Reference
|
||||
|
||||
- `withLibp2pSpan<T>(name: string, fn: (span) => Promise<T>): Promise<T>` — wraps `fn` in a span; always ends.
|
||||
- `recordConnectionOutcome(outcome: string): void` — `ok` / `error` / `rejected`.
|
||||
- `recordStreamOpened(): void` — increment streams counter.
|
||||
- `recordBytes(direction: "in" | "out", count: number): void` — count > 0 only.
|
||||
- `recordDial(ms: number): void` — clamped non-negative.
|
||||
- `recordStreamTtfb(ms: number): void` — clamped non-negative.
|
||||
- `adjustActivePeers(delta: number): void` — gauge delta.
|
||||
|
||||
## Implementation Notes
|
||||
|
||||
The tracer and meter are constructed at module-eval time via `trace.getTracer` and `metrics.getMeter`. Both APIs return no-op instances when no SDK is registered, so importing this module never forces an OTel SDK initialisation. The counter/histogram names are stable public API — operators may have dashboards keyed on them — so we never rename without a major-version bump and a deprecation alias.
|
||||
@@ -0,0 +1,77 @@
|
||||
# plugin
|
||||
|
||||
<!-- begin:auto-docs -->
|
||||
_(no submodules)_
|
||||
<!-- end:auto-docs -->
|
||||
## Purpose
|
||||
|
||||
`Libp2pPlugin implements NetworkPlugin` — the canonical wrapper that lets `@suaveplan/network`'s `NetworkNode` install, start, stop, and dispose a `Libp2pTransport` through a uniform plugin contract. Every transport in the `@suaveplan/network-transport-*` family follows this exact shape so the `network-plugin-transport-manager` can swap transports without code changes.
|
||||
|
||||
## Features
|
||||
|
||||
- `install(node)` registers the transport with the network node.
|
||||
- `start()` brings the transport up — composes the libp2p stack, registers the inbound stream handler.
|
||||
- `stop()` closes the transport and unregisters it from the node.
|
||||
- `dispose()` releases the node reference; idempotent across repeated calls.
|
||||
- Exposes the underlying `Libp2pTransport` instance via the `transport` field for tests and tracing hooks.
|
||||
|
||||
## Usage: Basic Example
|
||||
|
||||
```ts
|
||||
import { Libp2pPlugin } from "@suaveplan/network-transport-libp2p";
|
||||
import { NetworkNode } from "@suaveplan/network";
|
||||
|
||||
const node = new NetworkNode({ id: "node-1" });
|
||||
const plugin = new Libp2pPlugin({
|
||||
libp2pFactory: realLibp2pFactory,
|
||||
bootstrap: ["/dns4/bootstrap.example/tcp/443/wss/p2p/Qm…"],
|
||||
});
|
||||
|
||||
plugin.install(node);
|
||||
await plugin.start();
|
||||
// ... node is now reachable over libp2p
|
||||
await plugin.stop();
|
||||
plugin.dispose();
|
||||
```
|
||||
|
||||
## Usage: Advanced Example
|
||||
|
||||
```ts
|
||||
import { Libp2pPlugin } from "@suaveplan/network-transport-libp2p";
|
||||
import { NetworkNode } from "@suaveplan/network";
|
||||
|
||||
const wsPlugin = new Libp2pPlugin({
|
||||
libp2pFactory: wsFactory,
|
||||
bootstrap: bootstrapList,
|
||||
subTransport: "websockets",
|
||||
});
|
||||
const rtcPlugin = new Libp2pPlugin({
|
||||
libp2pFactory: rtcFactory,
|
||||
bootstrap: bootstrapList,
|
||||
subTransport: "webrtc",
|
||||
});
|
||||
|
||||
const node = new NetworkNode({ id: "dual" });
|
||||
node.use(wsPlugin);
|
||||
node.use(rtcPlugin);
|
||||
await node.start();
|
||||
|
||||
// Later:
|
||||
await node.stop();
|
||||
wsPlugin.dispose();
|
||||
rtcPlugin.dispose();
|
||||
```
|
||||
|
||||
## API Reference
|
||||
|
||||
- `new Libp2pPlugin(config: Libp2pConfig)` — constructs the transport eagerly so `config` validation runs at plugin instantiation, not at first `start()`.
|
||||
- `name: "libp2p"` (readonly).
|
||||
- `transport: Libp2pTransport` (readonly) — exposed for tracing/tests.
|
||||
- `install(node: NetworkNode): void`.
|
||||
- `start(): Promise<void>`.
|
||||
- `stop(): Promise<void>`.
|
||||
- `dispose(): void` — idempotent.
|
||||
|
||||
## Implementation Notes
|
||||
|
||||
Like the other transport plugins, this class is a deliberately thin wrapper — the heavy lifting lives in `Libp2pTransport`. The split exists so that `network-plugin-transport-manager` can introspect and swap plugins uniformly without knowing transport-specific configuration shapes. `dispose()` only clears the node reference; the transport's `close()` runs on `stop()` so a caller may safely re-install the same plugin instance against a different node after `stop()`.
|
||||
@@ -0,0 +1,66 @@
|
||||
# streams
|
||||
|
||||
<!-- begin:auto-docs -->
|
||||
_(no submodules)_
|
||||
<!-- end:auto-docs -->
|
||||
## Purpose
|
||||
|
||||
Inbound protocol-handler registration and stream draining. `registerStreamHandler` binds the application protocol id to a callback that ingests each inbound stream through a `FrameDecoder` and emits one `MessageEnvelope` per complete frame. `consumeInboundStream` is the underlying coroutine — exposed separately so callers can re-use it without registering a handler.
|
||||
|
||||
## Features
|
||||
|
||||
- `registerStreamHandler(node, protocolId, maxFrameSize, onEnvelope, signal?)` — single call to wire the inbound handler.
|
||||
- `consumeInboundStream(stream, maxFrameSize, onEnvelope, signal?)` — drains a single stream, returns the count of envelopes delivered.
|
||||
- Records `bytes_total{direction=in}` on every chunk.
|
||||
- Honours pre-aborted `AbortSignal` instantly and cuts the loop on mid-iteration abort.
|
||||
- Always closes the stream in a `finally` block, swallowing any close failure (stream is already torn down).
|
||||
|
||||
## Usage: Basic Example
|
||||
|
||||
```ts
|
||||
import { registerStreamHandler } from "@suaveplan/network-transport-libp2p";
|
||||
|
||||
await registerStreamHandler(
|
||||
node,
|
||||
"/suaveplan/network/1.0.0",
|
||||
1024 * 1024,
|
||||
(peerId, envelope) => {
|
||||
console.log(`Received ${envelope.id} from ${peerId}`);
|
||||
}
|
||||
);
|
||||
```
|
||||
|
||||
## Usage: Advanced Example
|
||||
|
||||
```ts
|
||||
import {
|
||||
consumeInboundStream,
|
||||
NET_LIBP2P_CODES,
|
||||
NetLibp2pError,
|
||||
} from "@suaveplan/network-transport-libp2p";
|
||||
|
||||
async function drainWithReputation(stream: Libp2pStreamLike, peer: string) {
|
||||
try {
|
||||
const count = await consumeInboundStream(
|
||||
stream,
|
||||
64 * 1024,
|
||||
(envelope) => routeEnvelope(peer, envelope)
|
||||
);
|
||||
recordPeerHealthy(peer, count);
|
||||
} catch (err) {
|
||||
if (err instanceof NetLibp2pError && err.code === NET_LIBP2P_CODES.STREAM_FAILED) {
|
||||
recordPeerByzantine(peer);
|
||||
}
|
||||
throw err;
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
## API Reference
|
||||
|
||||
- `registerStreamHandler(node, protocolId, maxFrameSize, onEnvelope, signal?): Promise<void>` — throws `NetLibp2pError(STREAM_FAILED)` if `signal` is pre-aborted.
|
||||
- `consumeInboundStream(stream, maxFrameSize, onEnvelope, signal?): Promise<number>` — throws `NetLibp2pError(STREAM_FAILED)` on any framing/decoding error.
|
||||
|
||||
## Implementation Notes
|
||||
|
||||
The handler returns synchronously after firing the consume coroutine (`void` return) — libp2p's handler API does not await its callback, so we never block its event loop. Errors raised during decoding propagate up through the `consumeInboundStream` promise, but because the transport never awaits that promise (it's a fire-and-forget on each new stream), errors surface in OTel counters rather than as uncaught exceptions. Future work: explicit reputation hook to tear down the offending peer's other streams when a single stream signals byzantine behaviour.
|
||||
@@ -0,0 +1,95 @@
|
||||
# transport
|
||||
|
||||
<!-- begin:auto-docs -->
|
||||
_(no submodules)_
|
||||
<!-- end:auto-docs -->
|
||||
## Purpose
|
||||
|
||||
`Libp2pTransport implements Transport` from `@suaveplan/network`. The class composes a libp2p stack (production or fake) through an injectable factory and wires the universal `Transport` contract: `start` / `connect` / `disconnect` / `send` / `onMessage` / `onPeerEvent` / `getConnectedPeers` / `close`. It owns the lifecycle of the libp2p node, registers the inbound stream handler, and converts libp2p's `peer:connect` / `peer:disconnect` events into the `Transport.onPeerEvent` shape.
|
||||
|
||||
## Features
|
||||
|
||||
- Universal — zero direct dependency on the concrete `libp2p` package at module-eval; production wiring lives in caller code.
|
||||
- Mandatory `libp2pFactory` and non-empty `bootstrap` enforced by `Libp2pConfigSchema` at construction.
|
||||
- Sub-transport selection (`websockets` | `webrtc`); TCP / mDNS deferred to a server-tier sibling.
|
||||
- Idempotent `start()` and `close()`; `start()` after `close()` rejects with `DISPOSED`.
|
||||
- Translates every internal failure into a typed `NetLibp2pError`.
|
||||
- Records `connections_opened_total`, `active_peers`, and (via `send` / `streams` modules) `bytes_total` + `stream_ttfb_ms`.
|
||||
- Honours pre-aborted `AbortSignal` on `start()`.
|
||||
|
||||
## Usage: Basic Example
|
||||
|
||||
```ts
|
||||
import { Libp2pTransport } from "@suaveplan/network-transport-libp2p";
|
||||
|
||||
const transport = new Libp2pTransport({
|
||||
libp2pFactory: myLibp2pFactory,
|
||||
bootstrap: ["/dns4/bootstrap.example/tcp/443/wss/p2p/Qm…"],
|
||||
});
|
||||
|
||||
await transport.start();
|
||||
transport.onMessage((peerId, envelope) => {
|
||||
console.log(`recv ${envelope.id} from ${peerId}`);
|
||||
});
|
||||
await transport.connect("Qm-remote", "/dns4/remote/tcp/443/wss/p2p/Qm-remote");
|
||||
await transport.send("Qm-remote", { id: "m1", type: "ping", payload: {} });
|
||||
await transport.close();
|
||||
```
|
||||
|
||||
## Usage: Advanced Example
|
||||
|
||||
```ts
|
||||
import {
|
||||
Libp2pTransport,
|
||||
NET_LIBP2P_CODES,
|
||||
NetLibp2pError,
|
||||
} from "@suaveplan/network-transport-libp2p";
|
||||
|
||||
const transport = new Libp2pTransport({
|
||||
libp2pFactory,
|
||||
bootstrap: bootstrapList,
|
||||
subTransport: "webrtc",
|
||||
addresses: { listen: ["/webrtc"] },
|
||||
protocolId: "/my-app/2.0.0",
|
||||
maxConnections: 50,
|
||||
dialTimeoutMs: 5_000,
|
||||
maxFrameSize: 256 * 1024,
|
||||
messageTimeoutMs: 3_000,
|
||||
});
|
||||
|
||||
const startController = new AbortController();
|
||||
setTimeout(() => startController.abort(), 15_000);
|
||||
|
||||
try {
|
||||
await transport.start(startController.signal);
|
||||
} catch (err) {
|
||||
if (err instanceof NetLibp2pError && err.code === NET_LIBP2P_CODES.BOOTSTRAP_FAILED) {
|
||||
// surface to user / retry / etc.
|
||||
}
|
||||
throw err;
|
||||
}
|
||||
|
||||
const off = transport.onPeerEvent((peer, event) => {
|
||||
metrics.gauge("peers", transport.getConnectedPeers().length, { peer, event });
|
||||
});
|
||||
// later: off();
|
||||
```
|
||||
|
||||
## API Reference
|
||||
|
||||
- `new Libp2pTransport(config: Libp2pConfig)` — throws `INVALID_OPTIONS` if `libp2pFactory` missing or `bootstrap` empty.
|
||||
- `id: "libp2p"` (readonly).
|
||||
- `subTransport: Libp2pSubTransport` (readonly).
|
||||
- `protocolId: string` (readonly).
|
||||
- `start(signal?: AbortSignal): Promise<void>` — idempotent; throws `DISPOSED` after `close()`.
|
||||
- `connect(peerId, address): Promise<void>` — throws `NOT_RUNNING` before `start()`, `DISPOSED` after `close()`, `DIAL_FAILED` on dial failure.
|
||||
- `disconnect(peerId): Promise<void>` — idempotent.
|
||||
- `send(peerId, envelope): Promise<void>` — throws `PEER_UNKNOWN` for unknown peer, `STREAM_FAILED` on protocol failure.
|
||||
- `onMessage(handler): () => void` — returns disposer.
|
||||
- `onPeerEvent(handler): () => void` — returns disposer.
|
||||
- `getConnectedPeers(): readonly PeerId[]` — snapshot.
|
||||
- `close(): Promise<void>` — idempotent.
|
||||
|
||||
## Implementation Notes
|
||||
|
||||
The transport stores peer ids as plain strings to keep `getConnectedPeers()` O(1) on the hot path. The `peer:connect` / `peer:disconnect` event listeners use ordinary functions held in instance fields so `close()` can pass exact references to `removeEventListener` — anonymous arrow handlers would silently leak. Sub-transport defaults to `websockets` because it is the universally-supported wire on both server and browser; selecting `webrtc` is a deliberate browser-tier opt-in. All error translation runs at the boundary; the internal `connection` / `streams` / `send` modules already produce typed errors, so the transport class itself contains essentially zero error-translation logic.
|
||||
@@ -0,0 +1,91 @@
|
||||
# types
|
||||
|
||||
<!-- begin:auto-docs -->
|
||||
_(no submodules)_
|
||||
<!-- end:auto-docs -->
|
||||
## Purpose
|
||||
|
||||
Defines the universal structural types consumed by every other module in `@suaveplan/network-transport-libp2p`. The package never imports the concrete `libp2p` package at module-eval time — instead, callers supply a `Libp2pFactory` and the package operates on structural `Libp2pNodeLike` / `Libp2pStreamLike` / `Libp2pPeerIdLike` interfaces. This keeps the package portable across browsers and Node, makes tests trivially injectable, and limits the bundle surface to the abstractions the transport actually exercises.
|
||||
|
||||
## Features
|
||||
|
||||
- `Libp2pSubTransport` discriminated union (`websockets` | `webrtc`).
|
||||
- `Libp2pNodeLike` structural interface mirroring the subset of `libp2p`'s `Libp2p` instance that this package uses (`start`/`stop`/`dial`/`dialProtocol`/`handle`/`unhandle`/`getPeers`/`addEventListener`/`removeEventListener`).
|
||||
- `Libp2pStreamLike` structural duplex stream with async-iterable `source` and async `sink`.
|
||||
- `Libp2pPeerIdLike` opaque peer-id with stable `toString()`.
|
||||
- `Libp2pStreamHandler` callback signature accepted by `handle(protocolId, handler)`.
|
||||
- `Libp2pFactory` async factory function — sole production wiring boundary.
|
||||
- `Libp2pFactoryConfig` configuration passed from the transport to the factory.
|
||||
- `Libp2pConfig` public boundary type validated by `Libp2pConfigSchema`.
|
||||
- `MessageHandler` / `PeerEventHandler` matching the `Transport` contract.
|
||||
|
||||
## Usage: Basic Example
|
||||
|
||||
```ts
|
||||
import type {
|
||||
Libp2pConfig,
|
||||
Libp2pFactory,
|
||||
} from "@suaveplan/network-transport-libp2p";
|
||||
|
||||
const factory: Libp2pFactory = async (cfg) => {
|
||||
const { createLibp2p } = await import("libp2p");
|
||||
return createLibp2p({
|
||||
addresses: { listen: cfg.addresses.listen },
|
||||
// ...wire bootstrap, identify, ping, etc.
|
||||
}) as unknown as import("@suaveplan/network-transport-libp2p").Libp2pNodeLike;
|
||||
};
|
||||
|
||||
const config: Libp2pConfig = {
|
||||
libp2pFactory: factory,
|
||||
bootstrap: ["/dns4/bootstrap.example/tcp/443/wss/p2p/Qm…"],
|
||||
subTransport: "websockets",
|
||||
};
|
||||
```
|
||||
|
||||
## Usage: Advanced Example
|
||||
|
||||
```ts
|
||||
import type {
|
||||
Libp2pConfig,
|
||||
Libp2pNodeLike,
|
||||
} from "@suaveplan/network-transport-libp2p";
|
||||
|
||||
function wrapForObservability(
|
||||
next: Libp2pNodeLike,
|
||||
metricsHook: (event: string) => void
|
||||
): Libp2pNodeLike {
|
||||
return new Proxy(next, {
|
||||
get(target, prop, recv) {
|
||||
metricsHook(String(prop));
|
||||
return Reflect.get(target, prop, recv);
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
const config: Libp2pConfig = {
|
||||
libp2pFactory: async (cfg) => wrapForObservability(await realFactory(cfg), () => {}),
|
||||
bootstrap: ["/dns4/example/tcp/443/wss/p2p/Qm…"],
|
||||
subTransport: "webrtc",
|
||||
addresses: { listen: ["/webrtc"] },
|
||||
maxConnections: 50,
|
||||
dialTimeoutMs: 5_000,
|
||||
maxFrameSize: 256 * 1024,
|
||||
};
|
||||
```
|
||||
|
||||
## API Reference
|
||||
|
||||
- `Libp2pSubTransport`: `"websockets" | "webrtc"`.
|
||||
- `Libp2pNodeLike`: structural libp2p node surface.
|
||||
- `Libp2pStreamLike`: structural duplex stream.
|
||||
- `Libp2pPeerIdLike`: structural peer-id.
|
||||
- `Libp2pStreamHandler`: `(args: { stream; connection }) => void`.
|
||||
- `Libp2pFactory`: `(config: Libp2pFactoryConfig) => Promise<Libp2pNodeLike>`.
|
||||
- `Libp2pFactoryConfig`: subset of `Libp2pConfig` re-shaped for the factory call.
|
||||
- `Libp2pConfig`: validated public config; `libp2pFactory` and non-empty `bootstrap` are mandatory.
|
||||
- `MessageHandler`: `(peerId, envelope) => void`.
|
||||
- `PeerEventHandler`: `(peerId, "connected" | "disconnected") => void`.
|
||||
|
||||
## Implementation Notes
|
||||
|
||||
The structural-type pattern is the single most load-bearing design choice in the package. By never importing `libp2p` at module-eval, we avoid pulling in transitive Node-only dependencies into browser bundles and give tests a clean injection seam (`FakeLibp2pNode`). Tradeoff: callers must take responsibility for wiring `createLibp2p` correctly — including Noise, Yamux, identify, ping, and bootstrap services. The Wave-7 demo package documents the canonical wiring.
|
||||
Reference in New Issue
Block a user