first commit
This commit is contained in:
140
docs/networking/network/network-trystero-server/README.md
Normal file
140
docs/networking/network/network-trystero-server/README.md
Normal file
@@ -0,0 +1,140 @@
|
||||
# @suaveplan/network-trystero-server - Server-tier Trystero signaling daemon — hosts an MQTT broker shim or Nostr relay shim over plain WebSocket for browser-tier @suaveplan/network-discovery-trystero clients. Construction-time mode selection, injectable broker factory, AbortSignal lifecycle, health/ready/live endpoints, OTel observability, real-fake integration tests.
|
||||
|
||||
## Summary
|
||||
|
||||
Server-tier Trystero signaling daemon. Hosts an MQTT-style or
|
||||
Nostr-style WebSocket broker shim plus standard `/health` / `/ready` /
|
||||
`/live` probes on a single Node `http.Server`. Browser-tier clients
|
||||
running `@suaveplan/network-discovery-trystero` connect to the
|
||||
daemon's published `relayUrl` for relay-backed P2P signaling.
|
||||
|
||||
<!-- 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 -->
|
||||
- [broker-select](./src/server/broker-select.md) — The broker-select module resolves which broker factory the daemon hosts and bundles the matching mode-specific options…
|
||||
- [health-listener](./src/server/health-listener.md) — The health-listener helper extracts the `http.Server.listen` promise adapter out of the main daemon module so the daemo…
|
||||
- [peer-events](./src/server/peer-events.md) — The peer-events module owns the daemon-side wiring of broker lifecycle callbacks into the daemon's connection table, OT…
|
||||
- [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)_- [broker-select](./src/server/broker-select.md) — The broker-select module resolves which broker factory the daemon hosts and bundles the matching mode-specific options…
|
||||
- [health-listener](./src/server/health-listener.md) — The health-listener helper extracts the `http.Server.listen` promise adapter out of the main daemon module so the daemo…
|
||||
- [peer-events](./src/server/peer-events.md) — The peer-events module owns the daemon-side wiring of broker lifecycle callbacks into the daemon's connection table, OT…
|
||||
- [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)_- [broker-select](./src/server/broker-select.md) — The broker-select module resolves which broker factory the daemon hosts and bundles the matching mode-specific options…
|
||||
- [health-listener](./src/server/health-listener.md) — The health-listener helper extracts the `http.Server.listen` promise adapter out of the main daemon module so the daemo…
|
||||
- [peer-events](./src/server/peer-events.md) — The peer-events module owns the daemon-side wiring of broker lifecycle callbacks into the daemon's connection table, OT…
|
||||
- [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)_- [broker-select](./src/server/broker-select.md) — The broker-select module resolves which broker factory the daemon hosts and bundles the matching mode-specific options…
|
||||
- [health-listener](./src/server/health-listener.md) — The health-listener helper extracts the `http.Server.listen` promise adapter out of the main daemon module so the daemo…
|
||||
- [peer-events](./src/server/peer-events.md) — The peer-events module owns the daemon-side wiring of broker lifecycle callbacks into the daemon's connection table, OT…
|
||||
- [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)_ - [broker-select](./src/server/broker-select.md) — The broker-select module resolves which broker factory the daemon hosts and bundles the matching mode-specific options…
|
||||
- [health-listener](./src/server/health-listener.md) — The health-listener helper extracts the `http.Server.listen` promise adapter out of the main daemon module so the daemo…
|
||||
- [peer-events](./src/server/peer-events.md) — The peer-events module owns the daemon-side wiring of broker lifecycle callbacks into the daemon's connection table, OT…
|
||||
- [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)_
|
||||
## Why this package
|
||||
|
||||
- **One daemon, one mode.** Construction-time selection between
|
||||
`"mqtt"` and `"nostr"`. No runtime branching past the broker
|
||||
factory, so dashboards split cleanly by `mode` label.
|
||||
- **Defensive by default.** Malformed frames flow through
|
||||
`onMalformedFrame` and increment
|
||||
`trystero_server.messages_relayed_total{outcome="error"}` without
|
||||
ever crashing the relay.
|
||||
- **Injection-friendly.** The broker shim is a peer dependency on
|
||||
intent — production wires a real WebSocket broker, tests bind a
|
||||
deterministic in-memory `FakeBroker`.
|
||||
- **Lifecycle-correct.** `start({ signal? })` resolves with
|
||||
`{ address, port, mode, relayUrl }`. `stop()` is idempotent and
|
||||
drains in-flight peers.
|
||||
|
||||
## Run the daemon
|
||||
|
||||
```ts
|
||||
import {
|
||||
TrysteroServer,
|
||||
type BrokerFactory,
|
||||
} from "@suaveplan/network-trystero-server";
|
||||
|
||||
const mqttBrokerFactory: BrokerFactory = (deps) => {
|
||||
// attach to the daemon's http.Server, handle WS upgrade, etc.
|
||||
return {
|
||||
mode: deps.mode,
|
||||
activeConnections: 0,
|
||||
async start({ httpServer, callbacks }) {
|
||||
// real broker wiring
|
||||
},
|
||||
async stop() {
|
||||
// drain sockets, close listeners
|
||||
},
|
||||
};
|
||||
};
|
||||
|
||||
const server = new TrysteroServer({
|
||||
mode: "mqtt",
|
||||
port: 8765,
|
||||
host: "0.0.0.0",
|
||||
maxConnections: 256,
|
||||
mqttBrokerFactory,
|
||||
});
|
||||
|
||||
const { relayUrl } = await server.start();
|
||||
console.log(`trystero relay listening on ${relayUrl}`);
|
||||
|
||||
process.on("SIGTERM", async () => {
|
||||
await server.stop();
|
||||
process.exit(0);
|
||||
});
|
||||
```
|
||||
|
||||
## Modes
|
||||
|
||||
| Mode | Wire path | Mode-specific options |
|
||||
|---------|------------|------------------------------------|
|
||||
| `mqtt` | `/mqtt` | `{ password? }` |
|
||||
| `nostr` | `/nostr` | `{ relayName? }` |
|
||||
|
||||
Both modes follow the same wire-formatting contract from the
|
||||
browser-tier companion package
|
||||
[`@suaveplan/network-discovery-trystero`](../network-discovery-trystero) —
|
||||
the daemon publishes the matching `relayUrl` and the discovery client
|
||||
treats it as a relay endpoint.
|
||||
|
||||
## Configuration reference
|
||||
|
||||
See [`src/types/types.md`](./src/types/types.md) for the full strict
|
||||
`TrysteroServerConfig`. The only required fields are `mode`, `port`,
|
||||
and the per-mode broker factory. Every other field is optional with
|
||||
a spec-documented default.
|
||||
|
||||
## Health endpoints
|
||||
|
||||
| Verb | 200 OK when | 503 when |
|
||||
|----------|--------------------------------------|--------------------------------------|
|
||||
| `/live` | process alive (always, until stop) | after `stop()` |
|
||||
| `/ready` | started AND below `maxConnections` | starting, at-cap, or stopped |
|
||||
| `/health`| started AND below `maxConnections` | starting, at-cap, or stopped |
|
||||
|
||||
See [`src/health/health.md`](./src/health/health.md).
|
||||
|
||||
## Observability
|
||||
|
||||
OpenTelemetry instrument names emitted by the daemon:
|
||||
|
||||
- `trystero_server.start` / `trystero_server.stop` — spans.
|
||||
- `trystero_server.connections_total{mode,outcome}` — counter.
|
||||
- `trystero_server.messages_relayed_total{mode,outcome}` — counter.
|
||||
- `trystero_server.active_connections{mode}` — up-down gauge.
|
||||
- `trystero_server.start_duration_ms{mode}` — histogram.
|
||||
|
||||
See [`src/otel/otel.md`](./src/otel/otel.md).
|
||||
|
||||
## Companion package
|
||||
|
||||
[`@suaveplan/network-discovery-trystero`](../network-discovery-trystero) is
|
||||
the browser-tier client this daemon serves.
|
||||
@@ -0,0 +1,90 @@
|
||||
# `errors`
|
||||
|
||||
<!-- begin:auto-docs -->
|
||||
_(no submodules)_
|
||||
<!-- end:auto-docs -->
|
||||
## Purpose
|
||||
|
||||
The errors module owns the structured-error contract for
|
||||
`@suaveplan/network-trystero-server`. Every public boundary on the
|
||||
daemon — the constructor, `start()`, `stop()`, the broker factory
|
||||
adapter, and the inline HTTP listener — throws a
|
||||
`NetTrysteroServerError` carrying a stable code from the
|
||||
`NET_TRYSTERO_SERVER_CODES` registry. Callers branch on `error.code`
|
||||
to recover (port collision, factory failure, abort mid-startup) rather
|
||||
than parsing message strings, so the registry doubles as the
|
||||
package's runtime stability contract.
|
||||
|
||||
## Features
|
||||
|
||||
- Frozen `NET_TRYSTERO_SERVER_CODES` registry built via the shared
|
||||
`defineCodes` helper from `@suaveplan/error`.
|
||||
- `NetTrysteroServerError` extends `SuaveplanError` and ships with the
|
||||
standard cause-chain, context bag, and i18n `messageKey` slot.
|
||||
- Six discriminants covering Zod validation, mode resolution,
|
||||
start-up failure, listen failure, disposed state, and not-running.
|
||||
|
||||
## Usage: Basic Example
|
||||
|
||||
```ts
|
||||
import {
|
||||
NET_TRYSTERO_SERVER_CODES,
|
||||
NetTrysteroServerError,
|
||||
} from "@suaveplan/network-trystero-server";
|
||||
|
||||
throw new NetTrysteroServerError(
|
||||
NET_TRYSTERO_SERVER_CODES.INVALID_OPTIONS,
|
||||
"trystero_server.invalid-config",
|
||||
{ context: { issues: [] } }
|
||||
);
|
||||
```
|
||||
|
||||
## Usage: Advanced Example
|
||||
|
||||
```ts
|
||||
import {
|
||||
NET_TRYSTERO_SERVER_CODES,
|
||||
NetTrysteroServerError,
|
||||
} from "@suaveplan/network-trystero-server";
|
||||
|
||||
try {
|
||||
await server.start();
|
||||
} catch (err) {
|
||||
if (err instanceof NetTrysteroServerError) {
|
||||
switch (err.code) {
|
||||
case NET_TRYSTERO_SERVER_CODES.LISTEN_FAILED:
|
||||
// port collision — retry with a new port
|
||||
break;
|
||||
case NET_TRYSTERO_SERVER_CODES.MODE_UNSUPPORTED:
|
||||
// caller asked for ipfs/torrent — not hosted server-side
|
||||
break;
|
||||
default:
|
||||
throw err;
|
||||
}
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
## API Reference
|
||||
|
||||
- `NET_TRYSTERO_SERVER_CODES` — frozen object literal mapping
|
||||
identifier → uppercase code string. Six members: `INVALID_OPTIONS`,
|
||||
`MODE_UNSUPPORTED`, `START_FAILED`, `LISTEN_FAILED`, `DISPOSED`,
|
||||
`NOT_RUNNING`.
|
||||
- `NetTrysteroServerErrorCode` — string-literal union of every code
|
||||
value in the registry.
|
||||
- `NetTrysteroServerError extends SuaveplanError<NetTrysteroServerErrorCode>`
|
||||
— constructor takes the code, an i18n message key, and optional
|
||||
`{ cause?, context? }`.
|
||||
|
||||
## Implementation Notes
|
||||
|
||||
- Codes are append-only across the package's lifetime. Renaming a
|
||||
code is a breaking change because consumer error-handling code
|
||||
branches on the literal value.
|
||||
- The class sets `name = "NetTrysteroServerError"` so
|
||||
`instanceof`-guarded handlers in cross-package transport layers can
|
||||
identify the source without importing the constructor.
|
||||
- The errors module has zero runtime dependencies on the rest of the
|
||||
package — every other module imports from here. That keeps the
|
||||
dependency graph acyclic by construction.
|
||||
@@ -0,0 +1,91 @@
|
||||
# `health`
|
||||
|
||||
<!-- begin:auto-docs -->
|
||||
_(no submodules)_
|
||||
<!-- end:auto-docs -->
|
||||
## Purpose
|
||||
|
||||
The health module is the daemon's probe surface. It owns the
|
||||
`/health` / `/ready` / `/live` verb routing, the per-verb status
|
||||
matrix, and the JSON body shape every probe receives. The daemon
|
||||
passes a `HealthSnapshot` on every request so responses always
|
||||
reflect current truth — never a stale cached view.
|
||||
|
||||
## Features
|
||||
|
||||
- Three verbs routed via `HEALTH_PATHS`: `/health`, `/ready`, `/live`.
|
||||
- `classifyHealthRequest(url)` accepts the raw `req.url`, strips
|
||||
querystring + trailing slash, returns the matching kind or `null`
|
||||
for non-health routes.
|
||||
- `buildHealthResponse(verb, snapshot)` returns the matching HTTP
|
||||
status code (200 / 503) and JSON body for the verb.
|
||||
- `handleHealth(req, res, snapshot)` writes the response directly and
|
||||
returns `true` so the daemon's request handler can early-return.
|
||||
|
||||
## Usage: Basic Example
|
||||
|
||||
```ts
|
||||
import {
|
||||
handleHealth,
|
||||
type HealthSnapshot,
|
||||
} from "@suaveplan/network-trystero-server";
|
||||
import { createServer } from "node:http";
|
||||
|
||||
const snap: HealthSnapshot = {
|
||||
running: true,
|
||||
disposed: false,
|
||||
activeConnections: 0,
|
||||
maxConnections: 100,
|
||||
startedAt: Date.now(),
|
||||
mode: "mqtt",
|
||||
relayUrl: "ws://0.0.0.0:8765/mqtt",
|
||||
};
|
||||
createServer((req, res) => {
|
||||
if (handleHealth(req, res, snap)) return;
|
||||
res.writeHead(404).end();
|
||||
}).listen(8765);
|
||||
```
|
||||
|
||||
## Usage: Advanced Example
|
||||
|
||||
```ts
|
||||
import {
|
||||
buildHealthResponse,
|
||||
classifyHealthRequest,
|
||||
} from "@suaveplan/network-trystero-server";
|
||||
|
||||
const verb = classifyHealthRequest("/ready?delay=1");
|
||||
if (verb !== null) {
|
||||
const { status, body } = buildHealthResponse(verb, mySnapshot());
|
||||
console.log(status, body.status, body.mode);
|
||||
}
|
||||
```
|
||||
|
||||
## API Reference
|
||||
|
||||
- `HEALTH_PATHS` — `{ health: "/health", ready: "/ready", live:
|
||||
"/live" }`.
|
||||
- `HealthPath` — literal union of the three values.
|
||||
- `HealthKind` — `"health" | "ready" | "live" | null`.
|
||||
- `HealthSnapshot` — runtime-state struct the daemon supplies.
|
||||
- `HealthResponse` — JSON body shape, includes the matching `verb`.
|
||||
- `classifyHealthRequest(url)` — null-safe URL → kind classifier.
|
||||
- `buildHealthResponse(verb, snap)` — pure function returning
|
||||
`{ status, body }`.
|
||||
- `handleHealth(req, res, snap)` — combined classifier + writer;
|
||||
returns `true` when the request was handled.
|
||||
|
||||
## Implementation Notes
|
||||
|
||||
- The matrix:
|
||||
- `/live` returns 200 unless `stop()` cleared state (then 503).
|
||||
- `/ready` returns 200 only when `running` AND below
|
||||
`maxConnections`.
|
||||
- `/health` matches `/ready` for 200; degrades to 503 with status
|
||||
`degraded` when at-cap, `starting` when not yet started, or
|
||||
`stopped` after stop.
|
||||
- `mode` and `relayUrl` are echoed on every response so a probe
|
||||
client can dual-purpose the health verb as a discovery endpoint.
|
||||
- The `handleHealth` writer signature accepts `Pick<...>` so unit
|
||||
tests can pass cheap stand-ins instead of a real
|
||||
`IncomingMessage`/`ServerResponse`.
|
||||
@@ -0,0 +1,85 @@
|
||||
# `otel`
|
||||
|
||||
<!-- begin:auto-docs -->
|
||||
_(no submodules)_
|
||||
<!-- end:auto-docs -->
|
||||
## Purpose
|
||||
|
||||
The OTel module owns every observability emission for
|
||||
`@suaveplan/network-trystero-server`. The daemon exposes spans for
|
||||
lifecycle methods (`start` / `stop`), counters for connection
|
||||
lifecycle and frame relay, an up-down gauge for live client-socket
|
||||
count, and a histogram for `start()` duration. Every metric carries
|
||||
the `mode` attribute so dashboards split mqtt vs nostr without
|
||||
client-side filtering.
|
||||
|
||||
## Features
|
||||
|
||||
- Two spans — `trystero_server.start` and `trystero_server.stop` —
|
||||
produced via `withStartSpan` / `withStopSpan`.
|
||||
- Counter `trystero_server.connections_total{mode,outcome}` —
|
||||
outcome ∈ `connected | disconnected | rejected`.
|
||||
- Counter `trystero_server.messages_relayed_total{mode,outcome}` —
|
||||
outcome ∈ `ok | error`.
|
||||
- Up-down counter `trystero_server.active_connections{mode}` — driven
|
||||
by `adjustActiveConnections`.
|
||||
- Histogram `trystero_server.start_duration_ms{mode}` — populated
|
||||
from the wall-clock delta between method entry and the bound listen
|
||||
resolution.
|
||||
|
||||
## Usage: Basic Example
|
||||
|
||||
```ts
|
||||
import {
|
||||
recordConnection,
|
||||
withStartSpan,
|
||||
} from "@suaveplan/network-trystero-server";
|
||||
|
||||
await withStartSpan(async () => {
|
||||
// ... start the broker
|
||||
});
|
||||
recordConnection("mqtt", "connected");
|
||||
```
|
||||
|
||||
## Usage: Advanced Example
|
||||
|
||||
```ts
|
||||
import {
|
||||
adjustActiveConnections,
|
||||
recordMessageRelayed,
|
||||
recordStartDuration,
|
||||
} from "@suaveplan/network-trystero-server";
|
||||
|
||||
adjustActiveConnections("nostr", +1);
|
||||
recordMessageRelayed("nostr", "ok");
|
||||
recordStartDuration("nostr", performance.now() - t0);
|
||||
```
|
||||
|
||||
## API Reference
|
||||
|
||||
- `withStartSpan(fn)` / `withStopSpan(fn)` — wrap the body of the
|
||||
matching lifecycle method in an active span; the span ends in a
|
||||
`finally` so errors are observable.
|
||||
- `recordConnection(mode, outcome)` — add 1 to the connections
|
||||
counter with the matching labels.
|
||||
- `recordMessageRelayed(mode, outcome)` — add 1 to the relayed
|
||||
messages counter.
|
||||
- `adjustActiveConnections(mode, delta)` — apply ±1 to the gauge.
|
||||
Zero deltas short-circuit so callers can wire unconditional
|
||||
`adjust(±delta)` calls without polluting cardinality.
|
||||
- `recordStartDuration(mode, ms)` — record one observation; negative
|
||||
inputs are clamped to zero so a malformed `Date.now()` skew can
|
||||
never poison the histogram.
|
||||
- `ConnectionOutcome` / `MessageRelayOutcome` — exported string
|
||||
unions consumers can use to type their own outcome branches.
|
||||
|
||||
## Implementation Notes
|
||||
|
||||
- The tracer and meter are resolved at module-evaluation time against
|
||||
the global OTel API. Production wires a real SDK via
|
||||
`@suaveplan/opentelemetry`; tests run with the default no-op
|
||||
provider so assertions focus on behaviour, not metric collection.
|
||||
- Every metric name is namespaced with `trystero_server.` so
|
||||
multi-package dashboards can filter the entire family in one query.
|
||||
- `mode` is always present on every emission so `outcome`-only
|
||||
rollups can be pre-computed without losing the mode dimension.
|
||||
@@ -0,0 +1,74 @@
|
||||
# `broker-select`
|
||||
|
||||
## Purpose
|
||||
|
||||
The broker-select module resolves which broker factory the daemon
|
||||
hosts and bundles the matching mode-specific options into a single
|
||||
struct. The daemon constructor calls this exactly once and freezes
|
||||
the result into its resolved config so the rest of the lifetime is
|
||||
mode-stable — no runtime branching, no factory swaps.
|
||||
|
||||
## Features
|
||||
|
||||
- Construction-time mode → factory dispatch covering `"mqtt"` and
|
||||
`"nostr"`.
|
||||
- Throws `INVALID_OPTIONS` with a discriminating `messageKey` when
|
||||
the matching factory is missing.
|
||||
- Throws `MODE_UNSUPPORTED` when callers widen the mode type past the
|
||||
Zod enum (defensive — Zod normally catches this first).
|
||||
- Returns the per-mode options struct with `{}` substituted when the
|
||||
caller omitted it, so downstream consumers can always read an
|
||||
object.
|
||||
|
||||
## Usage: Basic Example
|
||||
|
||||
```ts
|
||||
import { selectBrokerFactory } from "@suaveplan/network-trystero-server";
|
||||
|
||||
const { factory, mode, options } = selectBrokerFactory({
|
||||
mode: "mqtt",
|
||||
port: 0,
|
||||
mqttBrokerFactory: realMqttFactory,
|
||||
});
|
||||
```
|
||||
|
||||
## Usage: Advanced Example
|
||||
|
||||
```ts
|
||||
import {
|
||||
NET_TRYSTERO_SERVER_CODES,
|
||||
NetTrysteroServerError,
|
||||
selectBrokerFactory,
|
||||
} from "@suaveplan/network-trystero-server";
|
||||
|
||||
try {
|
||||
selectBrokerFactory({ mode: "nostr", port: 0 } as never);
|
||||
} catch (err) {
|
||||
if (
|
||||
err instanceof NetTrysteroServerError &&
|
||||
err.code === NET_TRYSTERO_SERVER_CODES.INVALID_OPTIONS
|
||||
) {
|
||||
// missing nostrRelayFactory
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
## API Reference
|
||||
|
||||
- `selectBrokerFactory(config)` — returns `{ factory, mode, options
|
||||
}`. `factory` is the caller-supplied `BrokerFactory`, `mode` is the
|
||||
string literal, `options` is the mode-specific opts struct
|
||||
defaulted to `{}`.
|
||||
|
||||
## Implementation Notes
|
||||
|
||||
- The function is pure and synchronous; it does not invoke the
|
||||
factory. That keeps construction cheap and the broker startup
|
||||
observable inside the `start()` span.
|
||||
- The `MODE_UNSUPPORTED` branch is defensive: Zod's
|
||||
`TrysteroServerConfigSchema` constrains `mode` to the two-member
|
||||
enum, so this branch can only fire when consumers cast past the
|
||||
schema. It still exists so the daemon emits a stable code instead
|
||||
of a generic type error.
|
||||
- Returning the struct keeps the surface explicit — the daemon does
|
||||
not need to remember which property the factory came from.
|
||||
@@ -0,0 +1,71 @@
|
||||
# `health-listener`
|
||||
|
||||
## Purpose
|
||||
|
||||
The health-listener helper extracts the `http.Server.listen` promise
|
||||
adapter out of the main daemon module so the daemon stays under the
|
||||
300-line discipline cap. It binds an existing `node:http` server to
|
||||
`(port, host)`, resolves on the `listening` event, and maps every
|
||||
known failure mode to a typed `NetTrysteroServerError`. It also wires
|
||||
an `AbortSignal` so a slow bind can be cancelled mid-flight.
|
||||
|
||||
## Features
|
||||
|
||||
- Promise-based listen wrapper over `node:http.Server.listen`.
|
||||
- `EADDRINUSE` → `LISTEN_FAILED` with `messageKey =
|
||||
"trystero_server.port-in-use"` and `context: { port }`.
|
||||
- Any other errno → `LISTEN_FAILED` with `messageKey =
|
||||
"trystero_server.listen-failed"` and the original errno preserved
|
||||
as `cause`.
|
||||
- `AbortSignal` fire → `START_FAILED` with `messageKey =
|
||||
"trystero_server.aborted"` plus a best-effort `http.close()` so the
|
||||
socket does not leak.
|
||||
- Symmetric listener cleanup — `error` / `listening` / `abort`
|
||||
handlers are removed on every termination path so a single
|
||||
`listenHttp` call cannot accumulate stale listeners.
|
||||
|
||||
## Usage: Basic Example
|
||||
|
||||
```ts
|
||||
import { listenHttp } from "@suaveplan/network-trystero-server";
|
||||
import { createServer } from "node:http";
|
||||
|
||||
const http = createServer();
|
||||
await listenHttp(http, 0, "127.0.0.1", undefined);
|
||||
const addr = http.address() as { address: string; port: number };
|
||||
```
|
||||
|
||||
## Usage: Advanced Example
|
||||
|
||||
```ts
|
||||
import { listenHttp } from "@suaveplan/network-trystero-server";
|
||||
import { createServer } from "node:http";
|
||||
|
||||
const ctrl = new AbortController();
|
||||
const http = createServer();
|
||||
setTimeout(() => ctrl.abort(), 100);
|
||||
try {
|
||||
await listenHttp(http, 0, "127.0.0.1", ctrl.signal);
|
||||
} catch (err) {
|
||||
// err.code === "NET_TRYSTERO_SERVER.START_FAILED"
|
||||
}
|
||||
```
|
||||
|
||||
## API Reference
|
||||
|
||||
- `listenHttp(http, port, host, signal)` — returns `Promise<void>`
|
||||
resolved once the server is listening, rejected with a
|
||||
`NetTrysteroServerError` on any failure or abort.
|
||||
|
||||
## Implementation Notes
|
||||
|
||||
- The handlers are wired with `once()` so a single resolution path
|
||||
always wins. Combined with the symmetric removal logic, there is no
|
||||
way for both `onError` and `onListening` to fire.
|
||||
- `onAbort` calls `http.close(noop)` as a best-effort cleanup; if the
|
||||
underlying socket has not yet entered the listening state the close
|
||||
is a noop, otherwise the half-bound socket is freed before the
|
||||
caller sees the rejection.
|
||||
- The function is intentionally narrow — it does not own the
|
||||
`http.Server` lifecycle, only its binding. The daemon retains the
|
||||
reference and closes it on `stop()`.
|
||||
@@ -0,0 +1,86 @@
|
||||
# `peer-events`
|
||||
|
||||
## Purpose
|
||||
|
||||
The peer-events module owns the daemon-side wiring of broker
|
||||
lifecycle callbacks into the daemon's connection table, OTel
|
||||
emissions, and typed event bus. It exists as a sibling of `server.ts`
|
||||
so that file stays under the 300-line cap and so callbacks can be
|
||||
unit-tested without spinning up a full HTTP server.
|
||||
|
||||
## Features
|
||||
|
||||
- `buildPeerCallbacks(ctx)` returns a `BrokerCallbacks` struct bound
|
||||
to the supplied context.
|
||||
- Connection-cap enforcement on `onConnect` — sockets over capacity
|
||||
are rejected with the `rejected` counter outcome and closed via
|
||||
`socket.close()`.
|
||||
- `onDisconnect` only fires the gauge/counter when the socket was
|
||||
actually `connected` so double-disconnect from a misbehaving
|
||||
broker does not drive the gauge negative.
|
||||
- `onFrame` and `onMalformedFrame` update `lastSeen` /
|
||||
`messagesRelayed` and emit the matching OTel counter outcome.
|
||||
- `relay-error` event emitted on every malformed frame so consumers
|
||||
with structured logging can alert on a misbehaving peer.
|
||||
|
||||
## Usage: Basic Example
|
||||
|
||||
```ts
|
||||
import {
|
||||
buildPeerCallbacks,
|
||||
type TrysteroServerPeerInfo,
|
||||
} from "@suaveplan/network-trystero-server";
|
||||
|
||||
const peers = new Map<string, TrysteroServerPeerInfo>();
|
||||
const cb = buildPeerCallbacks({
|
||||
peers,
|
||||
mode: "mqtt",
|
||||
maxConnections: 100,
|
||||
activeCount: () =>
|
||||
Array.from(peers.values()).filter((p) => p.connected).length,
|
||||
emit: () => {},
|
||||
});
|
||||
```
|
||||
|
||||
## Usage: Advanced Example
|
||||
|
||||
```ts
|
||||
import {
|
||||
buildPeerCallbacks,
|
||||
type PeerEventsContext,
|
||||
} from "@suaveplan/network-trystero-server";
|
||||
|
||||
const ctx: PeerEventsContext = {
|
||||
peers: new Map(),
|
||||
mode: "nostr",
|
||||
maxConnections: 1,
|
||||
activeCount: () => 0,
|
||||
emit: () => {},
|
||||
};
|
||||
const cb = buildPeerCallbacks(ctx);
|
||||
cb.onConnect({ id: "a", send: () => {}, close: () => {} });
|
||||
cb.onFrame("a");
|
||||
cb.onMalformedFrame("a");
|
||||
cb.onDisconnect("a");
|
||||
```
|
||||
|
||||
## API Reference
|
||||
|
||||
- `PeerEventsContext` — `{ peers, mode, maxConnections, activeCount,
|
||||
emit }`.
|
||||
- `buildPeerCallbacks(ctx)` — returns a `BrokerCallbacks` struct with
|
||||
four bound handlers.
|
||||
|
||||
## Implementation Notes
|
||||
|
||||
- Every handler reads `ctx.activeCount()` on demand rather than
|
||||
caching it in a local variable, so the cap check always sees fresh
|
||||
truth across interleaved connects/disconnects.
|
||||
- The malformed-frame handler updates `lastSeen` for a known peer
|
||||
even though it does not increment `messagesRelayed`. This keeps the
|
||||
liveness timestamp accurate while denying the offending socket
|
||||
relay-rate credit.
|
||||
- The bus emit is wrapped through the daemon-side `emit` shim and so
|
||||
is already exception-safe by the time it lands here. The module
|
||||
itself never catches — failures propagate to the surrounding broker
|
||||
callback, which is the broker's contract to honour.
|
||||
@@ -0,0 +1,119 @@
|
||||
# `server`
|
||||
|
||||
<!-- begin:auto-docs -->
|
||||
_(no submodules)_
|
||||
<!-- end:auto-docs -->
|
||||
## Purpose
|
||||
|
||||
The `TrysteroServer` class is the public daemon entry point. It owns
|
||||
the inline `node:http` listener, the broker shim attached to it, the
|
||||
peer connection table, OTel observability, and the typed lifecycle
|
||||
event bus. One daemon hosts exactly one mode (`mqtt` or `nostr`)
|
||||
selected at construction time; runtime mode-branching is restricted
|
||||
to the broker factory itself.
|
||||
|
||||
## Features
|
||||
|
||||
- Construction-time mode selection via injected
|
||||
`mqttBrokerFactory` / `nostrRelayFactory` — no implicit defaults.
|
||||
- Inline HTTP `/health` / `/ready` / `/live` on the same port the
|
||||
broker shim attaches to.
|
||||
- Idempotent `stop()` and single-shot `start()` with `AbortSignal`
|
||||
threaded throughout.
|
||||
- `relayUrl` of the form `ws://<host>:<port>/<mode>` published on
|
||||
every started event so browser-tier clients consume the daemon
|
||||
output without manual stitching.
|
||||
- Optional `EventBusLike` injection for typed `started` / `stopped` /
|
||||
`peer-connected` / `peer-disconnected` / `relay-error` emissions.
|
||||
- OTel spans for both lifecycle methods plus the matrix of counters
|
||||
/ gauge / histogram emitted via the package's `otel` module.
|
||||
|
||||
## Usage: Basic Example
|
||||
|
||||
```ts
|
||||
import {
|
||||
TrysteroServer,
|
||||
type BrokerFactory,
|
||||
} from "@suaveplan/network-trystero-server";
|
||||
|
||||
const mqttFactory: BrokerFactory = (deps) => ({
|
||||
mode: deps.mode,
|
||||
activeConnections: 0,
|
||||
async start() {
|
||||
/* real broker wiring */
|
||||
},
|
||||
async stop() {},
|
||||
});
|
||||
|
||||
const server = new TrysteroServer({
|
||||
mode: "mqtt",
|
||||
port: 0,
|
||||
host: "127.0.0.1",
|
||||
mqttBrokerFactory: mqttFactory,
|
||||
});
|
||||
const { relayUrl } = await server.start();
|
||||
// → publish `relayUrl` to discovery-trystero clients
|
||||
await server.stop();
|
||||
```
|
||||
|
||||
## Usage: Advanced Example
|
||||
|
||||
```ts
|
||||
import {
|
||||
TrysteroServer,
|
||||
type TrysteroServerEventMap,
|
||||
} from "@suaveplan/network-trystero-server";
|
||||
import type { EventBusLike } from "@suaveplan/types";
|
||||
|
||||
const events: Array<keyof TrysteroServerEventMap> = [];
|
||||
const bus: EventBusLike<TrysteroServerEventMap> = {
|
||||
on: () => () => {},
|
||||
emit: (topic) => {
|
||||
events.push(topic);
|
||||
},
|
||||
};
|
||||
|
||||
const server = new TrysteroServer({
|
||||
mode: "nostr",
|
||||
port: 0,
|
||||
host: "127.0.0.1",
|
||||
nostrRelayFactory: myNostrFactory,
|
||||
nostr: { relayName: "edge-1" },
|
||||
maxConnections: 256,
|
||||
bus,
|
||||
});
|
||||
|
||||
const ctrl = new AbortController();
|
||||
await server.start({ signal: ctrl.signal });
|
||||
```
|
||||
|
||||
## API Reference
|
||||
|
||||
- `new TrysteroServer(config)` — throws `INVALID_OPTIONS` on Zod
|
||||
failure or missing per-mode factory.
|
||||
- `start(options?)` — `Promise<TrysteroServerStartResult>`.
|
||||
- `stop()` — idempotent, drains peers, closes HTTP, returns
|
||||
`Promise<void>`.
|
||||
- `config` — read-only `ResolvedTrysteroServerConfig`.
|
||||
- `mode` — `TrysteroServerMode`.
|
||||
- `activeConnections` — live peer count.
|
||||
- `peers()` — snapshot array of every observed peer record.
|
||||
- `snapshot()` — `HealthSnapshot` for probe responses.
|
||||
- `relayUrl` / `boundAddress` / `boundPort` — populated after
|
||||
`start()` resolves.
|
||||
|
||||
## Implementation Notes
|
||||
|
||||
- The broker factory is invoked once per `start()` call. On failure
|
||||
the HTTP listener is closed and `START_FAILED` is thrown with the
|
||||
original cause preserved.
|
||||
- Peer state changes flow through `buildPeerCallbacks` (sibling
|
||||
module) so the daemon stays under the 300-line discipline cap. The
|
||||
factory observes the daemon only through the wired callbacks —
|
||||
it never reaches into the peer table directly.
|
||||
- `host === "0.0.0.0"` / `::` is rewritten to `127.0.0.1` in the
|
||||
published `relayUrl` so clients dialling the loopback consumer get
|
||||
a working address out of the box. Operators binding to an
|
||||
externally-routable host receive that host unchanged.
|
||||
- Bus emissions are wrapped in `try/catch` — a misbehaving bus must
|
||||
never crash the relay.
|
||||
@@ -0,0 +1,103 @@
|
||||
# `types`
|
||||
|
||||
<!-- begin:auto-docs -->
|
||||
_(no submodules)_
|
||||
<!-- end:auto-docs -->
|
||||
## Purpose
|
||||
|
||||
The types module owns every public structural interface and union
|
||||
emitted by `@suaveplan/network-trystero-server`. The daemon's
|
||||
`TrysteroServerConfig` discriminates on `mode` at construction time —
|
||||
mqtt and nostr broker shims are hosted via the same `BrokerLike`
|
||||
contract — so consumers wire a single factory per mode and the daemon
|
||||
itself contains no runtime mode-branching beyond construction.
|
||||
|
||||
## Features
|
||||
|
||||
- Discriminated `TrysteroServerMode = "mqtt" | "nostr"` union.
|
||||
- Structural `BrokerLike` surface every hosted broker conforms to,
|
||||
letting tests bind in-memory implementations without pulling a real
|
||||
WebSocket runtime.
|
||||
- Mode-specific options (`TrysteroServerMqttOptions`,
|
||||
`TrysteroServerNostrOptions`) the daemon forwards verbatim to the
|
||||
matching factory.
|
||||
- Lifecycle event map (`TrysteroServerEventMap`) used with the
|
||||
optional `EventBusLike` injection.
|
||||
- `ResolvedTrysteroServerConfig` — fully-defaulted snapshot held by
|
||||
the daemon for the lifetime of the process.
|
||||
|
||||
## Usage: Basic Example
|
||||
|
||||
```ts
|
||||
import type {
|
||||
BrokerFactory,
|
||||
TrysteroServerConfig,
|
||||
} from "@suaveplan/network-trystero-server";
|
||||
|
||||
const cfg: TrysteroServerConfig = {
|
||||
mode: "mqtt",
|
||||
port: 8765,
|
||||
host: "0.0.0.0",
|
||||
mqttBrokerFactory: ((deps) =>
|
||||
myRealMqttBroker(deps)) as BrokerFactory,
|
||||
};
|
||||
```
|
||||
|
||||
## Usage: Advanced Example
|
||||
|
||||
```ts
|
||||
import type {
|
||||
BrokerCallbacks,
|
||||
BrokerLike,
|
||||
BrokerSocketLike,
|
||||
} from "@suaveplan/network-trystero-server";
|
||||
|
||||
class MyBroker implements BrokerLike {
|
||||
public readonly mode = "nostr" as const;
|
||||
public activeConnections = 0;
|
||||
async start(_opts: {
|
||||
readonly callbacks: BrokerCallbacks;
|
||||
readonly maxConnections: number;
|
||||
}): Promise<void> {
|
||||
// attach to the daemon's http.Server, handle WS upgrade, etc.
|
||||
}
|
||||
async stop(): Promise<void> {
|
||||
// drain sockets, close listeners
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
## API Reference
|
||||
|
||||
- `TrysteroServerMode` — `"mqtt" | "nostr"` discriminator.
|
||||
- `BrokerSocketLike` — `{ id, send, close }`.
|
||||
- `BrokerLike` — `{ mode, start, stop, activeConnections }`.
|
||||
- `BrokerCallbacks` — `onConnect`, `onDisconnect`, `onFrame`,
|
||||
`onMalformedFrame`.
|
||||
- `BrokerFactory` — `(deps: BrokerFactoryDeps) => BrokerLike |
|
||||
Promise<BrokerLike>`.
|
||||
- `BrokerFactoryDeps` — `{ mode, options }`.
|
||||
- `TrysteroServerMqttOptions` — `{ password? }`.
|
||||
- `TrysteroServerNostrOptions` — `{ relayName? }`.
|
||||
- `TrysteroServerConfig` — strict caller-facing surface.
|
||||
- `ResolvedTrysteroServerConfig` — fully-defaulted snapshot.
|
||||
- `TrysteroServerStartResult` — `{ address, port, mode, relayUrl }`.
|
||||
- `TrysteroServerPeerInfo` — `{ id, connected, lastSeen,
|
||||
messagesRelayed }`.
|
||||
- Lifecycle events — `TrysteroServerStartedEvent`,
|
||||
`TrysteroServerStoppedEvent`, `TrysteroServerPeerConnectedEvent`,
|
||||
`TrysteroServerPeerDisconnectedEvent`,
|
||||
`TrysteroServerRelayErrorEvent`.
|
||||
|
||||
## Implementation Notes
|
||||
|
||||
- `BrokerLike` only exposes lifecycle methods — the broker is
|
||||
observation-only from the daemon's perspective. Every state change
|
||||
flows through the wired callbacks. This keeps the trust boundary
|
||||
one-way: the broker never reaches into the daemon's connection
|
||||
table directly.
|
||||
- `mode` lives on `BrokerLike` so multi-broker test rigs can be
|
||||
asserted positionally without rebuilding the daemon.
|
||||
- The two mode-options structs are independent rather than a single
|
||||
discriminated union so callers don't have to repeat `mode` twice in
|
||||
config payloads.
|
||||
Reference in New Issue
Block a user