first commit
This commit is contained in:
129
docs/networking/network/network-libp2p-server/README.md
Normal file
129
docs/networking/network/network-libp2p-server/README.md
Normal 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
|
||||
@@ -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.
|
||||
@@ -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.
|
||||
@@ -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.
|
||||
@@ -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.
|
||||
@@ -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.
|
||||
@@ -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).
|
||||
@@ -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`.
|
||||
Reference in New Issue
Block a user