first commit

This commit is contained in:
2026-05-29 08:20:56 +01:00
commit 949299f58d
6368 changed files with 527176 additions and 0 deletions

View File

@@ -0,0 +1,129 @@
# @suaveplan/network-libp2p-server - Server-tier libp2p relay daemon (bootstrap + circuit-relay-v2) paired with @suaveplan/network-transport-libp2p — Node-only HTTP/libp2p host with structural Zod option validation, AbortSignal lifecycle, health/ready/live endpoints, OTel observability, and injectable libp2p factory for real-fake integration tests.
## Summary
Server-tier libp2p relay daemon paired with
[`@suaveplan/network-transport-libp2p`](../network-transport-libp2p).
Hosts a libp2p node configured as a bootstrap + circuit-relay-v2 server
alongside an inline HTTP listener exposing `/health`, `/ready`, `/live`
verbs.
<!-- begin:auto-docs -->
## Documentation tree
- [errors](./src/errors/errors.md) — <!-- begin:auto-docs --> _(no submodules)_ <!-- end:auto-docs -->
- [errors](./src/errors/errors.md) — <!-- begin:auto-docs --> _(no submodules)_ <!-- end:auto-docs -->
- [health](./src/health/health.md) — <!-- begin:auto-docs --> _(no submodules)_ <!-- end:auto-docs -->
- [health](./src/health/health.md) — <!-- begin:auto-docs --> _(no submodules)_ <!-- end:auto-docs -->
- [otel](./src/otel/otel.md) — <!-- begin:auto-docs --> _(no submodules)_ <!-- end:auto-docs -->
- [otel](./src/otel/otel.md) — <!-- begin:auto-docs --> _(no submodules)_ <!-- end:auto-docs -->
- [server](./src/server/server.md) — <!-- begin:auto-docs --> _(no submodules)_ <!-- end:auto-docs -->
- [health-listener](./src/server/health-listener.md) — Wraps the asynchronous `http.Server.listen(port, host)` flow with the listener-attach / `EADDRINUSE` classification / `…
- [peer-events](./src/server/peer-events.md) — Wires the `peer:connect` and `peer:disconnect` event listeners onto a `Libp2pServerNodeLike` and bridges them onto the…
- [server](./src/server/server.md) — <!-- begin:auto-docs --> _(no submodules)_ <!-- end:auto-docs -->
- [types](./src/types/types.md) — <!-- begin:auto-docs --> _(no submodules)_ <!-- end:auto-docs -->
- [types](./src/types/types.md) — <!-- begin:auto-docs --> _(no submodules)_ <!-- end:auto-docs -->
<!-- end:auto-docs --> - [errors](./src/errors/errors.md) — <!-- begin:auto-docs --> _(no submodules)_- [health](./src/health/health.md) — <!-- begin:auto-docs --> _(no submodules)_- [health](./src/health/health.md) — <!-- begin:auto-docs --> _(no submodules)_- [otel](./src/otel/otel.md) — <!-- begin:auto-docs --> _(no submodules)_- [otel](./src/otel/otel.md) — <!-- begin:auto-docs --> _(no submodules)_- [server](./src/server/server.md) — <!-- begin:auto-docs --> _(no submodules)_- [health-listener](./src/server/health-listener.md) — Wraps the asynchronous `http.Server.listen(port, host)` flow with the listener-attach / `EADDRINUSE` classification / `…
- [peer-events](./src/server/peer-events.md) — Wires the `peer:connect` and `peer:disconnect` event listeners onto a `Libp2pServerNodeLike` and bridges them onto the…
- [server](./src/server/server.md) — <!-- begin:auto-docs --> _(no submodules)_- [types](./src/types/types.md) — <!-- begin:auto-docs --> _(no submodules)_- [types](./src/types/types.md) — <!-- begin:auto-docs --> _(no submodules)_- [errors](./src/errors/errors.md) — <!-- begin:auto-docs --> _(no submodules)_- [health](./src/health/health.md) — <!-- begin:auto-docs --> _(no submodules)_- [health](./src/health/health.md) — <!-- begin:auto-docs --> _(no submodules)_- [otel](./src/otel/otel.md) — <!-- begin:auto-docs --> _(no submodules)_- [otel](./src/otel/otel.md) — <!-- begin:auto-docs --> _(no submodules)_- [server](./src/server/server.md) — <!-- begin:auto-docs --> _(no submodules)_- [health-listener](./src/server/health-listener.md) — Wraps the asynchronous `http.Server.listen(port, host)` flow with the listener-attach / `EADDRINUSE` classification / `…
- [peer-events](./src/server/peer-events.md) — Wires the `peer:connect` and `peer:disconnect` event listeners onto a `Libp2pServerNodeLike` and bridges them onto the…
- [server](./src/server/server.md) — <!-- begin:auto-docs --> _(no submodules)_- [types](./src/types/types.md) — <!-- begin:auto-docs --> _(no submodules)_- [types](./src/types/types.md) — <!-- begin:auto-docs --> _(no submodules)_- [errors](./src/errors/errors.md) — <!-- begin:auto-docs --> _(no submodules)_- [health](./src/health/health.md) — <!-- begin:auto-docs --> _(no submodules)_- [health](./src/health/health.md) — <!-- begin:auto-docs --> _(no submodules)_- [otel](./src/otel/otel.md) — <!-- begin:auto-docs --> _(no submodules)_- [otel](./src/otel/otel.md) — <!-- begin:auto-docs --> _(no submodules)_- [server](./src/server/server.md) — <!-- begin:auto-docs --> _(no submodules)_- [health-listener](./src/server/health-listener.md) — Wraps the asynchronous `http.Server.listen(port, host)` flow with the listener-attach / `EADDRINUSE` classification / `…
- [peer-events](./src/server/peer-events.md) — Wires the `peer:connect` and `peer:disconnect` event listeners onto a `Libp2pServerNodeLike` and bridges them onto the…
- [server](./src/server/server.md) — <!-- begin:auto-docs --> _(no submodules)_- [types](./src/types/types.md) — <!-- begin:auto-docs --> _(no submodules)_- [types](./src/types/types.md) — <!-- begin:auto-docs --> _(no submodules)_- [errors](./src/errors/errors.md) — <!-- begin:auto-docs --> _(no submodules)_- [health](./src/health/health.md) — <!-- begin:auto-docs --> _(no submodules)_ - [health](./src/health/health.md) — <!-- begin:auto-docs --> _(no submodules)_- [otel](./src/otel/otel.md) — <!-- begin:auto-docs --> _(no submodules)_ - [otel](./src/otel/otel.md) — <!-- begin:auto-docs --> _(no submodules)_- [server](./src/server/server.md) — <!-- begin:auto-docs --> _(no submodules)_ - [health-listener](./src/server/health-listener.md) — Wraps the asynchronous `http.Server.listen(port, host)` flow with the listener-attach / `EADDRINUSE` classification / `…
- [peer-events](./src/server/peer-events.md) — Wires the `peer:connect` and `peer:disconnect` event listeners onto a `Libp2pServerNodeLike` and bridges them onto the…
- [server](./src/server/server.md) — <!-- begin:auto-docs --> _(no submodules)_- [types](./src/types/types.md) — <!-- begin:auto-docs --> _(no submodules)_ - [types](./src/types/types.md) — <!-- begin:auto-docs --> _(no submodules)_
## Run the daemon
```ts
import { createLibp2p } from "libp2p";
import { tcp } from "@libp2p/tcp";
import { webSockets } from "@libp2p/websockets";
import { noise } from "@chainsafe/libp2p-noise";
import { yamux } from "@chainsafe/libp2p-yamux";
import { identify } from "@libp2p/identify";
import { circuitRelayServer } from "@libp2p/circuit-relay-v2";
import { bootstrap } from "@libp2p/bootstrap";
import { Libp2pServer } from "@suaveplan/network-libp2p-server";
const server = new Libp2pServer({
listen: ["/ip4/0.0.0.0/tcp/9090", "/ip4/0.0.0.0/tcp/9091/ws"],
bootstrap: ["/dnsaddr/bootstrap.libp2p.io/p2p/QmNnoo..."],
autoRelay: { enabled: true, maxConnections: 500 },
healthPort: 9099,
libp2pFactory: async (cfg) =>
createLibp2p({
addresses: { listen: [...cfg.listen] },
transports: [tcp(), webSockets()],
connectionEncrypters: [noise()],
streamMuxers: [yamux()],
peerDiscovery:
cfg.bootstrap.length > 0
? [bootstrap({ list: [...cfg.bootstrap] })]
: [],
services: {
identify: identify(),
relay: circuitRelayServer({
reservations: {
maxReservations: cfg.autoRelay.maxConnections,
},
}),
},
connectionManager: { maxConnections: cfg.maxConnections },
}),
});
const { peerId, multiaddrs } = await server.start();
console.log(`libp2p relay ${peerId} on:`);
for (const a of multiaddrs) console.log(` ${a}`);
// Graceful shutdown
process.on("SIGTERM", () => server.stop());
```
## Test environment
Tests inject a deterministic `FakeLibp2pServerNode` so the native
transitive deps of the real `libp2p` package never enter the test
surface — the daemon never resolves `libp2p` at module evaluation time
and the integration suite boots a real `Libp2pServer` inside the test
process.
```ts
import { Libp2pServer } from "@suaveplan/network-libp2p-server";
import {
FakeLibp2pHub,
makeFakeServerFactory,
} from "./tests/setup/fake-libp2p-server.js";
const hub = new FakeLibp2pHub();
const { factory } = makeFakeServerFactory(hub);
const server = new Libp2pServer({
libp2pFactory: factory,
listen: ["/ip4/0.0.0.0/tcp/0"],
healthPort: 0,
});
await server.start();
// ... drive the daemon, then:
await server.stop();
```
## Acceptance gates
- `bunx turbo lint typecheck test --filter=@suaveplan/network-libp2p-server`
- `bunx turbo test:coverage --filter=@suaveplan/network-libp2p-server` → 100/100/100/100
- `bun run docs:check --filter=@suaveplan/network-libp2p-server`
## API
See per-module `.md` docs:
- [`src/server/server.md`](src/server/server.md) — `Libp2pServer` daemon
- [`src/server/peer-events.md`](src/server/peer-events.md) — peer
connect/disconnect bridging
- [`src/server/health-listener.md`](src/server/health-listener.md) —
inline-HTTP `listenHttp` helper
- [`src/health/health.md`](src/health/health.md) — `/health` / `/ready`
/ `/live` verb pipeline
- [`src/otel/otel.md`](src/otel/otel.md) — OTel instrumentation surface
- [`src/errors/errors.md`](src/errors/errors.md) — error registry
- [`src/types/types.md`](src/types/types.md) — public type contracts

View File

@@ -0,0 +1,89 @@
# `errors/errors.ts` — error registry
<!-- begin:auto-docs -->
_(no submodules)_
<!-- end:auto-docs -->
## Purpose
Defines the `NET_LIBP2P_SERVER` code registry plus `NetLibp2pServerError`
class used throughout the libp2p relay daemon. Every public boundary in
`@suaveplan/network-libp2p-server` throws this typed error on failure so
callers can branch deterministically on `error.code` without parsing
messages or stack traces.
## Features
- Frozen `NET_LIBP2P_SERVER_CODES` registry — six codes covering option
validation, lifecycle, listener bind failures, and circuit-relay-v2
setup.
- `NetLibp2pServerError` extends `SuaveplanError<NetLibp2pServerErrorCode>`
so the cause-chain traversal, structured logging adapters, and OTel
span-event integrations already wired into the suaveplan error
framework all apply automatically.
- `NetLibp2pServerErrorCode` derived type exposed for exhaustive
switches in callers.
## Usage: basic example
```ts
import {
NET_LIBP2P_SERVER_CODES,
NetLibp2pServerError,
} from "@suaveplan/network-libp2p-server";
throw new NetLibp2pServerError(
NET_LIBP2P_SERVER_CODES.INVALID_OPTIONS,
"libp2p_server.invalid-config"
);
```
## Usage: advanced example
```ts
try {
await server.start();
} catch (e) {
if (e instanceof NetLibp2pServerError) {
switch (e.code) {
case NET_LIBP2P_SERVER_CODES.LISTEN_FAILED:
// health port in use — retry with another port
break;
case NET_LIBP2P_SERVER_CODES.START_FAILED:
// libp2p node failed to start
break;
default:
throw e;
}
}
}
```
## API reference
- `NET_LIBP2P_SERVER_CODES` — `Readonly<{ INVALID_OPTIONS, START_FAILED,
LISTEN_FAILED, DISPOSED, NOT_RUNNING, RELAY_FAILED }>`.
- `NetLibp2pServerError(code, messageKey, options?)` — constructor.
Sets `name = "NetLibp2pServerError"`. Forwards `options` (cause,
context) to `SuaveplanError`.
## Implementation notes
The registry is intentionally narrow. The relay daemon is not in the
business of forwarding application-level libp2p stream errors — those
belong to the consumer's protocol-level error model. The codes here
cover only daemon-lifecycle failures so dashboards and SLO panels can
key on a tight, stable surface.
`INVALID_OPTIONS` is the only code thrown synchronously from the
constructor; the remaining codes surface from `start()` and `stop()`.
`LISTEN_FAILED` covers every bind failure on the inline health HTTP
server, including `EADDRINUSE`. `START_FAILED` covers libp2p factory
failures, `node.start()` rejections, double-start attempts, and signal
aborts; aborts deliberately reuse the same code so dashboards see a
single bucket. `DISPOSED` is the post-`stop()` state guard; `NOT_RUNNING`
is reserved for future operations that are only valid mid-lifecycle.
`RELAY_FAILED` is reserved for the circuit-relay-v2 stream-setup
failure path and is exported now so consumers' switch-statements stay
exhaustive when the relay-stream-wiring story expands. All codes are
frozen at definition time by `defineCodes` so accidental mutation at
runtime is impossible.

View File

@@ -0,0 +1,72 @@
# `health/health.ts` — inline health endpoints
<!-- begin:auto-docs -->
_(no submodules)_
<!-- end:auto-docs -->
## Purpose
Exposes the `/health`, `/ready`, `/live` verb pipeline used by the
libp2p relay daemon's inline HTTP listener. Allows orchestrators
(Kubernetes, ECS, Nomad, docker-compose healthchecks) to probe the
daemon without pulling in `@suaveplan/health-check` as a hard dep.
Returns deterministic JSON bodies so dashboards can key on the
`status` field as well as inspect `activePeers` / `peerId` for at-a
glance triage.
## Features
- Three pure verbs: `live` (process alive, hasn't `stop()`'d), `ready`
(running with spare capacity), `health` (overall posture).
- Pure `classifyHealthRequest(url)` returning `"health" | "ready" |
"live" | null` — strips querystrings and trailing slashes.
- Pure `buildHealthResponse(verb, snap)` returning `{ status, body }`
for unit testability and reuse outside the inline HTTP server.
- `handleHealth(req, res, snap)` dispatcher returning a boolean so the
daemon can fall through to non-health routes deterministically.
## Usage: basic example
```ts
import { handleHealth } from "@suaveplan/network-libp2p-server/health";
import { createServer } from "node:http";
createServer((req, res) => {
if (handleHealth(req, res, server.snapshot())) return;
res.writeHead(404).end();
}).listen(9091);
```
## Usage: advanced example
```ts
import {
buildHealthResponse,
classifyHealthRequest,
HEALTH_PATHS,
} from "@suaveplan/network-libp2p-server/health";
// Custom routing — wire health into an existing fastify/express tree
const verb = classifyHealthRequest(req.url);
if (verb !== null) {
const { status, body } = buildHealthResponse(verb, snapshot);
reply.code(status).send(body);
}
```
## API reference
- `HEALTH_PATHS` — `Readonly<{ health: "/health", ready: "/ready",
live: "/live" }>`.
- `classifyHealthRequest(url) => HealthKind`.
- `buildHealthResponse(verb, snap) => { status, body }`.
- `handleHealth(req, res, snap) => boolean` — returns `true` if the
request was handled and the response was written.
## Implementation notes
The `live` verb intentionally returns 200 even when the libp2p node has
not yet finished `start()` — process liveness is independent of node
readiness so orchestrators don't churn restarts during slow bootstrap.
`ready` returns 503 with `status: "starting"` until `start()` resolves.
`health` returns 503 with `status: "degraded"` when at-cap so load
balancers shed traffic before connection rejections occur.

View File

@@ -0,0 +1,75 @@
# `otel/otel.ts` — OTel instrumentation surface
<!-- begin:auto-docs -->
_(no submodules)_
<!-- end:auto-docs -->
## Purpose
Centralises every OpenTelemetry instrument the libp2p relay daemon
emits. The daemon never touches `@opentelemetry/api` directly — it
always calls through the helpers in this module so the instrument names
are owned in exactly one place and dashboards stay stable across
refactors. Operators wire an OTel SDK once at process start; the
suaveplan no-op default applies otherwise so the package stays
zero-overhead by default.
## Features
- Two named spans: `libp2p_server.start`, `libp2p_server.stop`.
- One counter `libp2p_server.connections_total{outcome}` covering
`connected`/`disconnected`/`rejected` per-peer events.
- One counter `libp2p_server.relayed_streams_total` for circuit-relay-v2
stream brokering events.
- One up-down counter `libp2p_server.active_peers` tracking live peer
count.
- One histogram `libp2p_server.start_duration_ms` recording `start()`
wall-clock duration.
## Usage: basic example
```ts
import {
recordConnection,
adjustActivePeers,
} from "@suaveplan/network-libp2p-server/otel";
recordConnection("connected");
adjustActivePeers(+1);
```
## Usage: advanced example
```ts
import {
withStartSpan,
recordStartDuration,
recordRelayedStream,
} from "@suaveplan/network-libp2p-server/otel";
await withStartSpan(async (span) => {
span.setAttribute("listen", listen.join(","));
const t0 = Date.now();
await node.start();
recordStartDuration(Date.now() - t0);
});
// On a relayed stream:
recordRelayedStream();
```
## API reference
- `withStartSpan<T>(fn) => Promise<T>` — wraps `fn` in an active span.
- `withStopSpan<T>(fn) => Promise<T>` — wraps `fn` in an active span.
- `recordConnection(outcome)` — `connected | disconnected | rejected`.
- `recordRelayedStream()` — incremented per brokered relay stream.
- `adjustActivePeers(delta)` — `+1` or `-1` on connection events.
- `recordStartDuration(ms)` — non-negative ms.
## Implementation notes
The `recordStartDuration` helper clamps negative values to zero so
caller-side `Date.now()` clock skew never corrupts the histogram. All
labels are low-cardinality enums; no peer-id or multiaddr appears in
any metric attribute so we don't blow up the metrics backend cardinality
budget when the relay is busy.

View File

@@ -0,0 +1,62 @@
# `server/health-listener.ts` — inline health HTTP bind helper
## Purpose
Wraps the asynchronous `http.Server.listen(port, host)` flow with the
listener-attach / `EADDRINUSE` classification / `AbortSignal` plumbing
the libp2p relay daemon needs around its inline health HTTP listener.
Extracted as a sibling module to keep `server.ts` under the 300-line
hard cap mandated by CLAUDE.md while preserving the deterministic
error-code mapping callers depend on.
## Features
- Resolves once the `listening` event fires.
- Rejects with `NET_LIBP2P_SERVER.LISTEN_FAILED` + `messageKey =
"libp2p_server.port-in-use"` when the bind fails with `EADDRINUSE`.
- Rejects with `NET_LIBP2P_SERVER.LISTEN_FAILED` + `messageKey =
"libp2p_server.listen-failed"` for every other errno.
- Rejects with `NET_LIBP2P_SERVER.START_FAILED` + `messageKey =
"libp2p_server.aborted"` when a supplied `AbortSignal` fires before
the `listening` event, after closing the half-bound socket
best-effort.
- Cleans up its own listeners on every exit path so the `http.Server`
doesn't leak handlers when the daemon retries with a different port.
## Usage: basic example
```ts
import { listenHttp } from "@suaveplan/network-libp2p-server";
import { createServer } from "node:http";
const http = createServer();
await listenHttp(http, 9091, "127.0.0.1", undefined);
```
## Usage: advanced example
```ts
const ctrl = new AbortController();
queueMicrotask(() => ctrl.abort());
try {
await listenHttp(http, 0, "0.0.0.0", ctrl.signal);
} catch (e) {
if (e instanceof NetLibp2pServerError) {
// Match `code` to discriminate listen vs abort failure paths.
}
}
```
## API reference
- `listenHttp(http, port, host, signal?) => Promise<void>` — resolves
on `listening`, rejects per the mapping above.
## Implementation notes
The helper is responsible only for the listen lifecycle of the inline
health HTTP server — the libp2p node lifecycle is owned by the daemon
itself, so caller-side rollback (stopping the libp2p node on a failed
health-port bind) lives in `server.ts`. All listener attachments use
`once` so reject and resolve paths cannot fire twice and the `http`
instance never retains stale callbacks after this Promise settles.

View File

@@ -0,0 +1,70 @@
# `server/peer-events.ts` — peer connect/disconnect bridging
## Purpose
Wires the `peer:connect` and `peer:disconnect` event listeners onto a
`Libp2pServerNodeLike` and bridges them onto the daemon's internal peer
set, the OTel `libp2p_server.connections_total` counter / `active_peers`
gauge, and the optional `EventBusLike` lifecycle surface. Extracted from
`server.ts` to keep that module under the 300-line hard cap mandated by
CLAUDE.md.
## Features
- `wirePeerEvents(node, ctx)` — attaches both listeners and returns the
bound functions for later removal.
- `unwirePeerEvents(node, binding)` — removes previously-bound listeners
on graceful stop. Tolerates `undefined` for both arguments so the
daemon can call it unconditionally on `stop()`.
- Enforces `maxConnections` at the wire boundary — when a connect event
would push the peer set past the cap, the listener increments the
`connections_total{outcome="rejected"}` counter and bails without
recording the peer.
## Usage: basic example
```ts
import {
wirePeerEvents,
unwirePeerEvents,
} from "@suaveplan/network-libp2p-server";
const binding = wirePeerEvents(node, {
peers,
maxConnections: 1000,
emit: (topic, payload) => bus.emit(topic, payload),
});
// ... later, on shutdown
unwirePeerEvents(node, binding);
```
## Usage: advanced example
```ts
const ctx: PeerEventsContext = {
peers: new Set<string>(),
maxConnections: 100,
emit: (topic, payload) => {
if (topic === "libp2p_server:peer-connected") {
console.log("peer joined", (payload as { peerId: string }).peerId);
}
bus.emit(topic, payload);
},
};
const binding = wirePeerEvents(node, ctx);
```
## API reference
- `wirePeerEvents(node, ctx) => PeerEventsBinding` — `ctx` is `{ peers,
maxConnections, emit }`. Returns `{ onConnect, onDisconnect }`.
- `unwirePeerEvents(node?, binding?) => void` — idempotent.
## Implementation notes
The connect handler is a no-op when the peer id is already tracked so
duplicate `peer:connect` events from libp2p's internal dial fan-out
don't double-count. The disconnect handler is symmetric — only known
peers cause counter decrement. Both listeners derive the peer id via
`String(event.detail.toString())` so any structurally compatible peer-id
object satisfies the binding without leaking the concrete `PeerId` type.

View File

@@ -0,0 +1,105 @@
# `server/server.ts` — `Libp2pServer` daemon
<!-- begin:auto-docs -->
_(no submodules)_
<!-- end:auto-docs -->
## Purpose
Hosts a libp2p server node — typically configured as a bootstrap +
circuit-relay-v2 relay — paired with an inline HTTP listener exposing
the `/health` / `/ready` / `/live` verbs. Browser-tier clients dial the
libp2p node via `@suaveplan/network-transport-libp2p`. The daemon is
designed for in-process integration testing as well as production
deployment: every dependency on the real `libp2p` runtime flows through
the injected `libp2pFactory`, so test environments can wire a
deterministic `FakeLibp2pServerNode` without pulling native transitive
deps into the test surface.
## Features
- Idempotent lifecycle: `start({ signal? })` resolves with
`{ peerId, multiaddrs, healthAddress, healthPort }`; `stop()` is safe
to call repeatedly and drains tracked peers via the OTel surface.
- Pre-aborted signal: `start({ signal })` with an already-aborted
signal short-circuits to `START_FAILED` before any side effects.
- Required injectable `libp2pFactory` — the package never resolves the
`libp2p` module at evaluation time.
- Inline health HTTP listener (`/health`, `/ready`, `/live`) bound to
`healthHost:healthPort`. Falls through to a plaintext `"libp2p relay"`
marker on non-health routes.
- Auto-relay configuration forwarded to the factory via
`Libp2pServerFactoryConfig.autoRelay`.
- Strict Zod validation of `Libp2pServerConfig` at the constructor
boundary — invalid options throw `INVALID_OPTIONS` with the Zod
issue list attached for diagnostics.
- Peer-event bridging (`peer:connect` / `peer:disconnect`) into the
internal peer set, OTel counters/gauge, and optional `EventBusLike`.
- Hand-off helper `observeRelayedStream(peerId)` so consumers wiring a
custom circuit-relay-v2 stream handler in the factory body keep the
`libp2p_server.relayed_streams_total` counter accurate.
## Usage: basic example
```ts
import { Libp2pServer } from "@suaveplan/network-libp2p-server";
const server = new Libp2pServer({
libp2pFactory: async (cfg) => createLibp2p({
addresses: { listen: cfg.listen },
// ... compose transports/encrypters/muxers/services here
}),
listen: ["/ip4/0.0.0.0/tcp/0", "/ip4/0.0.0.0/tcp/0/ws"],
healthPort: 9091,
});
const { peerId, multiaddrs } = await server.start();
console.log(`relay ${peerId} on ${multiaddrs.join(", ")}`);
// later
await server.stop();
```
## Usage: advanced example
```ts
const ctrl = new AbortController();
const server = new Libp2pServer({
libp2pFactory: myFactory,
listen: ["/ip4/0.0.0.0/tcp/9090"],
bootstrap: ["/dnsaddr/bootstrap.libp2p.io"],
autoRelay: { enabled: true, maxConnections: 500 },
maxConnections: 5000,
protocolId: "/myapp/1.0.0",
healthPort: 9091,
bus,
});
await server.start({ signal: ctrl.signal });
// On a circuit-relay-v2 stream brokered between two clients:
server.observeRelayedStream("12D3KooW...client");
```
## API reference
- `new Libp2pServer(config)` — validates `config`; throws
`INVALID_OPTIONS` on Zod failure or missing `libp2pFactory`.
- `server.start(options?) => Promise<Libp2pServerStartResult>` — throws
`DISPOSED`, `START_FAILED`, or `LISTEN_FAILED`.
- `server.stop() => Promise<void>` — idempotent.
- `server.observeRelayedStream(peerId)` — increments OTel counter and
emits `libp2p_server:relay-stream`.
- `server.snapshot() => HealthSnapshot` — for health-probe responses.
- `server.peers()` / `server.activePeers` — observed peers.
- `server.config` — resolved config snapshot.
- `server.healthAddress` / `server.healthPort` — bound HTTP listener
inspectors.
## Implementation notes
The libp2p node lifecycle and the inline-HTTP lifecycle are wound
independently: a successful libp2p start followed by a failed HTTP bind
rolls the libp2p node back via `stop()` so the daemon never leaks a
half-running node. `stop()` reverses the wiring in the opposite order
(unwire listeners, stop node, drain peers, close HTTP) and is wrapped
in a try-around-`node.stop()` so half-started instances cannot poison
the disposed state. The `#runStart` helper exists purely to keep the
public `start()` method short enough for the discipline-metrics cap on
this file (≤300 LOC).

View File

@@ -0,0 +1,75 @@
# `types/types.ts` — public type contracts
<!-- begin:auto-docs -->
_(no submodules)_
<!-- end:auto-docs -->
## Purpose
Defines every TypeScript contract `@suaveplan/network-libp2p-server`
exposes at its public boundary. The daemon never depends on the concrete
`Libp2p` interface from the upstream `libp2p` package — only on the
structural `Libp2pServerNodeLike` shape declared here. This keeps the
relay's runtime cost zero on the test surface (no native transitive
deps pulled in) while still preserving full parity with the production
libp2p hosting story.
## Features
- `Libp2pServerNodeLike` structural interface — lifecycle + peer enum +
event listeners.
- `Libp2pServerFactory` / `Libp2pServerFactoryConfig` for injectable
factory wiring (production binds `createLibp2p({...})`, tests bind a
deterministic fake).
- Typed event map (`Libp2pServerEventMap`) covering start, stop,
peer-connect, peer-disconnect, and circuit-relay-v2 stream emissions.
- `Libp2pServerConfig` — strict callable boundary; required fields are
declared non-optional so consumers cannot silently fall through to an
implicit default.
- `ResolvedLibp2pServerConfig` — fully-defaulted snapshot returned via
the daemon's `config` getter for introspection.
## Usage: basic example
```ts
import type { Libp2pServerConfig } from "@suaveplan/network-libp2p-server";
const config: Libp2pServerConfig = {
libp2pFactory: async (cfg) => createLibp2p({ /* compose */ }),
listen: ["/ip4/0.0.0.0/tcp/0", "/ip4/0.0.0.0/tcp/0/ws"],
healthPort: 0,
};
```
## Usage: advanced example
```ts
const config: Libp2pServerConfig = {
libp2pFactory: myFactory,
listen: ["/ip4/0.0.0.0/tcp/9090"],
bootstrap: ["/dnsaddr/bootstrap.libp2p.io"],
autoRelay: { enabled: true, maxConnections: 200 },
maxConnections: 5000,
protocolId: "/myapp/1.0.0",
healthPort: 9091,
healthHost: "127.0.0.1",
bus: eventBus,
};
```
## API reference
- `Libp2pServerNodeLike` — structural libp2p node surface.
- `Libp2pServerFactory(config) => Promise<Libp2pServerNodeLike>` —
factory callable.
- `Libp2pServerConfig` — required `libp2pFactory`, `listen`,
`healthPort`; optional `bootstrap`, `autoRelay`, `maxConnections`,
`protocolId`, `healthHost`, `bus`.
- `Libp2pServerStartResult` — `{ peerId, multiaddrs, healthAddress,
healthPort }`.
## Implementation notes
Types-only module (excluded from coverage via `types.ts` filename). The
sibling `types.schemas.ts` carries the Zod boundary schemas; that file
is also excluded from coverage. Concrete enforcement (factory presence,
listen non-empty) lives in the constructor in `server/server.ts`.