first commit
This commit is contained in:
26
docs/networking/network/network-plugin-relay/README.md
Normal file
26
docs/networking/network/network-plugin-relay/README.md
Normal file
@@ -0,0 +1,26 @@
|
||||
# @suaveplan/network-plugin-relay - Cross-mesh relay: relay router, host-backbone transport, cross-mesh envelope schema, mesh directory, loop-prevention guard, manager
|
||||
|
||||
## Summary
|
||||
|
||||
Cross-mesh relay — the federation layer that lets Tier 0 hosts of different meshes route messages to each other. Composes `RelayRouter`, `HostBackboneTransport` (Tier 0 ↔ Tier 0), `CrossMeshEnvelope` (Zod-validated), `MeshDirectory` (host lookup), `LoopGuard` (hopCount/ttl/visitedNodes), and `RelayManager` (plugin entry point).
|
||||
|
||||
<!-- begin:auto-docs -->
|
||||
## Documentation tree
|
||||
|
||||
- [backbone](./src/backbone/host-backbone-transport.md) — <!-- begin:auto-docs --> _(no submodules)_ <!-- end:auto-docs -->
|
||||
- [host-backbone-transport](./src/backbone/host-backbone-transport.md) — <!-- begin:auto-docs --> _(no submodules)_ <!-- end:auto-docs -->
|
||||
- [directory](./src/directory/mesh-directory.md) — <!-- begin:auto-docs --> _(no submodules)_ <!-- end:auto-docs -->
|
||||
- [mesh-directory](./src/directory/mesh-directory.md) — <!-- begin:auto-docs --> _(no submodules)_ <!-- end:auto-docs -->
|
||||
- [envelope](./src/envelope/cross-mesh-envelope.md) — <!-- begin:auto-docs --> _(no submodules)_ <!-- end:auto-docs -->
|
||||
- [cross-mesh-envelope](./src/envelope/cross-mesh-envelope.md) — <!-- begin:auto-docs --> _(no submodules)_ <!-- end:auto-docs -->
|
||||
- [loop-prevention](./src/loop-prevention/loop-guard.md) — <!-- begin:auto-docs --> _(no submodules)_ <!-- end:auto-docs -->
|
||||
- [loop-guard](./src/loop-prevention/loop-guard.md) — <!-- begin:auto-docs --> _(no submodules)_ <!-- end:auto-docs -->
|
||||
- [manager](./src/manager/relay-manager.md) — <!-- begin:auto-docs --> _(no submodules)_ <!-- end:auto-docs -->
|
||||
- [relay-manager](./src/manager/relay-manager.md) — <!-- begin:auto-docs --> _(no submodules)_ <!-- end:auto-docs -->
|
||||
- [router](./src/router/relay-router.md) — <!-- begin:auto-docs --> _(no submodules)_ <!-- end:auto-docs -->
|
||||
- [relay-router](./src/router/relay-router.md) — <!-- begin:auto-docs --> _(no submodules)_ <!-- end:auto-docs -->
|
||||
- [error-codes](./src/error-codes.md) — Frozen registry of error codes for the network-plugin-relay plugin. Five codes cover the full surface: `ROUTE_NOT_FOUND…
|
||||
<!-- end:auto-docs --> - [host-backbone-transport](./src/backbone/host-backbone-transport.md) — <!-- begin:auto-docs --> _(no submodules)_- [directory](./src/directory/mesh-directory.md) — <!-- begin:auto-docs --> _(no submodules)_- [mesh-directory](./src/directory/mesh-directory.md) — <!-- begin:auto-docs --> _(no submodules)_- [envelope](./src/envelope/cross-mesh-envelope.md) — <!-- begin:auto-docs --> _(no submodules)_- [cross-mesh-envelope](./src/envelope/cross-mesh-envelope.md) — <!-- begin:auto-docs --> _(no submodules)_- [loop-prevention](./src/loop-prevention/loop-guard.md) — <!-- begin:auto-docs --> _(no submodules)_- [loop-guard](./src/loop-prevention/loop-guard.md) — <!-- begin:auto-docs --> _(no submodules)_- [manager](./src/manager/relay-manager.md) — <!-- begin:auto-docs --> _(no submodules)_- [relay-manager](./src/manager/relay-manager.md) — <!-- begin:auto-docs --> _(no submodules)_- [router](./src/router/relay-router.md) — <!-- begin:auto-docs --> _(no submodules)_- [relay-router](./src/router/relay-router.md) — <!-- begin:auto-docs --> _(no submodules)_- [error-codes](./src/error-codes.md) — Frozen registry of error codes for the network-plugin-relay plugin. Five codes cover the full surface: `ROUTE_NOT_FOUND…
|
||||
- [host-backbone-transport](./src/backbone/host-backbone-transport.md) — <!-- begin:auto-docs --> _(no submodules)_- [directory](./src/directory/mesh-directory.md) — <!-- begin:auto-docs --> _(no submodules)_- [mesh-directory](./src/directory/mesh-directory.md) — <!-- begin:auto-docs --> _(no submodules)_- [envelope](./src/envelope/cross-mesh-envelope.md) — <!-- begin:auto-docs --> _(no submodules)_- [cross-mesh-envelope](./src/envelope/cross-mesh-envelope.md) — <!-- begin:auto-docs --> _(no submodules)_- [loop-prevention](./src/loop-prevention/loop-guard.md) — <!-- begin:auto-docs --> _(no submodules)_- [loop-guard](./src/loop-prevention/loop-guard.md) — <!-- begin:auto-docs --> _(no submodules)_- [manager](./src/manager/relay-manager.md) — <!-- begin:auto-docs --> _(no submodules)_- [relay-manager](./src/manager/relay-manager.md) — <!-- begin:auto-docs --> _(no submodules)_- [router](./src/router/relay-router.md) — <!-- begin:auto-docs --> _(no submodules)_- [relay-router](./src/router/relay-router.md) — <!-- begin:auto-docs --> _(no submodules)_- [error-codes](./src/error-codes.md) — Frozen registry of error codes for the network-plugin-relay plugin. Five codes cover the full surface: `ROUTE_NOT_FOUND…
|
||||
- [host-backbone-transport](./src/backbone/host-backbone-transport.md) — <!-- begin:auto-docs --> _(no submodules)_- [directory](./src/directory/mesh-directory.md) — <!-- begin:auto-docs --> _(no submodules)_- [mesh-directory](./src/directory/mesh-directory.md) — <!-- begin:auto-docs --> _(no submodules)_- [envelope](./src/envelope/cross-mesh-envelope.md) — <!-- begin:auto-docs --> _(no submodules)_- [cross-mesh-envelope](./src/envelope/cross-mesh-envelope.md) — <!-- begin:auto-docs --> _(no submodules)_- [loop-prevention](./src/loop-prevention/loop-guard.md) — <!-- begin:auto-docs --> _(no submodules)_- [loop-guard](./src/loop-prevention/loop-guard.md) — <!-- begin:auto-docs --> _(no submodules)_- [manager](./src/manager/relay-manager.md) — <!-- begin:auto-docs --> _(no submodules)_- [relay-manager](./src/manager/relay-manager.md) — <!-- begin:auto-docs --> _(no submodules)_- [router](./src/router/relay-router.md) — <!-- begin:auto-docs --> _(no submodules)_- [relay-router](./src/router/relay-router.md) — <!-- begin:auto-docs --> _(no submodules)_- [error-codes](./src/error-codes.md) — Frozen registry of error codes for the network-plugin-relay plugin. Five codes cover the full surface: `ROUTE_NOT_FOUND…
|
||||
- [host-backbone-transport](./src/backbone/host-backbone-transport.md) — <!-- begin:auto-docs --> _(no submodules)_- [directory](./src/directory/mesh-directory.md) — <!-- begin:auto-docs --> _(no submodules)_ - [mesh-directory](./src/directory/mesh-directory.md) — <!-- begin:auto-docs --> _(no submodules)_- [envelope](./src/envelope/cross-mesh-envelope.md) — <!-- begin:auto-docs --> _(no submodules)_ - [cross-mesh-envelope](./src/envelope/cross-mesh-envelope.md) — <!-- begin:auto-docs --> _(no submodules)_- [loop-prevention](./src/loop-prevention/loop-guard.md) — <!-- begin:auto-docs --> _(no submodules)_ - [loop-guard](./src/loop-prevention/loop-guard.md) — <!-- begin:auto-docs --> _(no submodules)_- [manager](./src/manager/relay-manager.md) — <!-- begin:auto-docs --> _(no submodules)_ - [relay-manager](./src/manager/relay-manager.md) — <!-- begin:auto-docs --> _(no submodules)_- [router](./src/router/relay-router.md) — <!-- begin:auto-docs --> _(no submodules)_ - [relay-router](./src/router/relay-router.md) — <!-- begin:auto-docs --> _(no submodules)_- [error-codes](./src/error-codes.md) — Frozen registry of error codes for the network-plugin-relay plugin. Five codes cover the full surface: `ROUTE_NOT_FOUND…
|
||||
@@ -0,0 +1,85 @@
|
||||
# HostBackboneTransport
|
||||
|
||||
<!-- begin:auto-docs -->
|
||||
_(no submodules)_
|
||||
<!-- end:auto-docs -->
|
||||
## Purpose
|
||||
|
||||
Cross-mesh transport for Tier 0 ↔ Tier 0 traffic. Runs over an injected `BackboneWire` (WebSocket, RTCDataChannel, libp2p, anything) and adds the `CrossMeshEnvelope` encode/decode plus a single-handler `onMessage` subscriber that the relay manager hooks into. Decoupling the wire from the envelope shape means a swap from WebSocket to QUIC doesn't touch the manager.
|
||||
|
||||
## Features
|
||||
|
||||
- Pluggable `BackboneWire` interface (`open` / `close` / `write` / `onPayload` / `isConnected`).
|
||||
- Pluggable serializer; default uses JSON over UTF-8 bytes.
|
||||
- `start()` wires the wire's `onPayload` into the local handler set; `stop()` unwires it.
|
||||
- `send` throws `BACKBONE_DISCONNECTED` when the wire reports the host isn't connected.
|
||||
- `onMessage(handler)` returns an unsubscribe function so subscribers don't leak.
|
||||
- `isConnected(hostId)` exposed for observability.
|
||||
|
||||
## Usage: Basic Example
|
||||
|
||||
```ts
|
||||
import {
|
||||
type BackboneWire,
|
||||
HostBackboneTransport,
|
||||
} from "@suaveplan/network-plugin-relay";
|
||||
|
||||
declare const wire: BackboneWire;
|
||||
|
||||
const backbone = new HostBackboneTransport({ wire });
|
||||
backbone.start();
|
||||
|
||||
await backbone.connect("host-X", "wss://x");
|
||||
await backbone.send("host-X", envelope);
|
||||
```
|
||||
|
||||
## Usage: Advanced Example
|
||||
|
||||
```ts
|
||||
import {
|
||||
HostBackboneTransport,
|
||||
RELAY_CODES,
|
||||
RelayError,
|
||||
} from "@suaveplan/network-plugin-relay";
|
||||
|
||||
const backbone = new HostBackboneTransport({
|
||||
wire,
|
||||
serializer: {
|
||||
encode: (env) => msgpack.encode(env),
|
||||
decode: (b) => msgpack.decode(b) as never,
|
||||
},
|
||||
});
|
||||
backbone.start();
|
||||
|
||||
backbone.onMessage(async (env, fromHostId) => {
|
||||
await manager.deliverInbound(env, fromHostId);
|
||||
});
|
||||
|
||||
try {
|
||||
await backbone.send("host-X", envelope);
|
||||
} catch (err) {
|
||||
if (
|
||||
err instanceof RelayError &&
|
||||
err.code === RELAY_CODES.BACKBONE_DISCONNECTED
|
||||
) {
|
||||
await backbone.connect("host-X", "wss://x");
|
||||
await backbone.send("host-X", envelope);
|
||||
} else {
|
||||
throw err;
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
## API Reference
|
||||
|
||||
- `interface BackboneWire { open, close, write, onPayload, isConnected }`.
|
||||
- `interface HostBackboneTransportConfig { wire, serializer? }`.
|
||||
- `start()` / `stop()` — bind / unbind the inbound payload subscriber.
|
||||
- `connect(hostId, address)` / `disconnect(hostId)`.
|
||||
- `send(hostId, envelope): Promise<void>` — throws `BACKBONE_DISCONNECTED` if the wire is not open.
|
||||
- `onMessage(handler) → () => void`.
|
||||
- `isConnected(hostId): boolean`.
|
||||
|
||||
## Implementation Notes
|
||||
|
||||
Inbound handlers are dispatched eagerly with `void h(...)` so an async handler that throws does not block subsequent handlers — this matches the observer-not-subscriber semantics other transports in the network suite use. Encoding via JSON is the default because it's debuggable and small enough for federation traffic; consumers that need binary framing should pass a custom `serializer`. The `onPayload` callback is captured and the unsubscribe stored in `offWire` so `stop()` can reverse the wiring cleanly without leaking listeners on the underlying wire.
|
||||
@@ -0,0 +1,73 @@
|
||||
# MeshDirectory
|
||||
|
||||
<!-- begin:auto-docs -->
|
||||
_(no submodules)_
|
||||
<!-- end:auto-docs -->
|
||||
## Purpose
|
||||
|
||||
Tracks which Tier 0 host advertises which mesh. Hosts query this when they need "who is the gateway for mesh-B?". A separate registry from `RelayRouter` because the directory is the _source_ of routing information — the router is the per-peer cache derived from it. Decoupling lets directory sync run on its own cadence without churning the router.
|
||||
|
||||
## Features
|
||||
|
||||
- `register(reg)` — insert or update a `MeshRegistration` (mesh id, host id, address, optional expiry).
|
||||
- `deregister(meshId, hostId)` — explicit removal.
|
||||
- `lookup(meshId): readonly MeshRegistration[]` — every non-expired host advertising this mesh.
|
||||
- `list()` — every non-expired registration.
|
||||
- Pluggable `clock` for deterministic tests.
|
||||
- Expired entries are filtered lazily on every lookup, never stored after expiry.
|
||||
|
||||
## Usage: Basic Example
|
||||
|
||||
```ts
|
||||
import { MeshDirectory } from "@suaveplan/network-plugin-relay";
|
||||
|
||||
const directory = new MeshDirectory();
|
||||
directory.register({
|
||||
meshId: "mesh-B",
|
||||
hostId: "host-X",
|
||||
address: "wss://x.example.com",
|
||||
registeredAt: Date.now(),
|
||||
});
|
||||
|
||||
const hosts = directory.lookup("mesh-B");
|
||||
```
|
||||
|
||||
## Usage: Advanced Example
|
||||
|
||||
```ts
|
||||
import { MeshDirectory } from "@suaveplan/network-plugin-relay";
|
||||
|
||||
let now = 1_000;
|
||||
const directory = new MeshDirectory({ clock: () => now });
|
||||
|
||||
directory.register({
|
||||
meshId: "mesh-B",
|
||||
hostId: "host-X",
|
||||
address: "wss://x",
|
||||
registeredAt: 1_000,
|
||||
expiresAt: 2_000,
|
||||
});
|
||||
directory.register({
|
||||
meshId: "mesh-B",
|
||||
hostId: "host-Y",
|
||||
address: "wss://y",
|
||||
registeredAt: 1_000,
|
||||
});
|
||||
|
||||
now = 5_000;
|
||||
const live = directory.lookup("mesh-B");
|
||||
// live === [{ hostId: "host-Y", ... }] — host-X expired
|
||||
```
|
||||
|
||||
## API Reference
|
||||
|
||||
- `interface MeshRegistration { meshId, hostId, address, registeredAt, expiresAt? }`.
|
||||
- `interface MeshDirectoryConfig { clock? }`.
|
||||
- `register(reg)` / `deregister(meshId, hostId): boolean`.
|
||||
- `lookup(meshId): readonly MeshRegistration[]`.
|
||||
- `list(): readonly MeshRegistration[]`.
|
||||
- `size(): number` / `clear()`.
|
||||
|
||||
## Implementation Notes
|
||||
|
||||
Expiry is enforced at read time rather than via a background sweep timer, which keeps the directory free of leaked timers across test/server boundaries. The cost is one extra `Date.now()` (or injected clock) call per lookup; the benefit is that the directory has no lifecycle of its own — it can be created and discarded on demand. `register` of the same `(meshId, hostId)` pair is an upsert by design so directory sync can refresh expiry without explicit deregister-then-register.
|
||||
@@ -0,0 +1,68 @@
|
||||
# CrossMeshEnvelope
|
||||
|
||||
<!-- begin:auto-docs -->
|
||||
_(no submodules)_
|
||||
<!-- end:auto-docs -->
|
||||
## Purpose
|
||||
|
||||
Zod-validated envelope shape for cross-mesh relay traffic. Extends the intra-mesh `MessageEnvelope` with `senderMeshId` / `targetMeshId` for federation routing, plus `hopCount` / `maxHops` / `visitedNodes` for loop prevention. Centralising the schema means every relay component (router, manager, backbone transport) parses the same shape and rejects malformed wire frames at the boundary.
|
||||
|
||||
## Features
|
||||
|
||||
- Zod schema with bounded `maxHops` (`1..255`) and non-negative `hopCount`.
|
||||
- Boundary validators: `validateCrossMeshEnvelope(value)` (throws `ENVELOPE_INVALID` on failure) and `createCrossMeshEnvelope(partial)` (defaults headers / hop count / visited nodes).
|
||||
- Headers are string-only (cross-mesh framing is intentionally simpler than intra-mesh).
|
||||
- `visitedNodes` is an ordered array, not a set, so loop detection can preserve traversal history for diagnostics.
|
||||
|
||||
## Usage: Basic Example
|
||||
|
||||
```ts
|
||||
import { createCrossMeshEnvelope } from "@suaveplan/network-plugin-relay";
|
||||
|
||||
const envelope = createCrossMeshEnvelope({
|
||||
id: "msg-1",
|
||||
type: "chat",
|
||||
senderId: "alice",
|
||||
senderMeshId: "mesh-A",
|
||||
targetId: "bob",
|
||||
targetMeshId: "mesh-B",
|
||||
payload: { text: "hi" },
|
||||
timestamp: Date.now(),
|
||||
});
|
||||
```
|
||||
|
||||
## Usage: Advanced Example
|
||||
|
||||
```ts
|
||||
import {
|
||||
createCrossMeshEnvelope,
|
||||
RELAY_CODES,
|
||||
RelayError,
|
||||
validateCrossMeshEnvelope,
|
||||
} from "@suaveplan/network-plugin-relay";
|
||||
|
||||
try {
|
||||
const decoded = validateCrossMeshEnvelope(JSON.parse(wireBytes));
|
||||
await router.route(decoded);
|
||||
} catch (err) {
|
||||
if (
|
||||
err instanceof RelayError &&
|
||||
err.code === RELAY_CODES.ENVELOPE_INVALID
|
||||
) {
|
||||
metrics.counter("relay.malformed").increment();
|
||||
return;
|
||||
}
|
||||
throw err;
|
||||
}
|
||||
```
|
||||
|
||||
## API Reference
|
||||
|
||||
- `interface CrossMeshEnvelope` — Zod-inferred shape with id, type, senderId, senderMeshId, targetId, targetMeshId, payload, timestamp, headers, hopCount, maxHops, visitedNodes.
|
||||
- `CrossMeshEnvelopeSchema` — the Zod object schema.
|
||||
- `validateCrossMeshEnvelope(value): CrossMeshEnvelope` — throws `ENVELOPE_INVALID`.
|
||||
- `createCrossMeshEnvelope(partial): CrossMeshEnvelope` — default-injecting constructor.
|
||||
|
||||
## Implementation Notes
|
||||
|
||||
`payload` is typed `unknown` because the federation layer doesn't constrain payload shape — that's the recipient's domain. Consumers that want a typed payload must layer their own Zod schema on top. We deliberately reject `maxHops > 255` to keep the on-wire counter byte-aligned for any future binary frame variant. The `visitedNodes` cap is enforced by the loop guard, not the schema, so callers that want to inspect a longer trace can extend the schema without coupling to the guard's policy.
|
||||
@@ -0,0 +1,61 @@
|
||||
# RELAY_CODES
|
||||
|
||||
## Purpose
|
||||
|
||||
Frozen registry of error codes for the network-plugin-relay plugin. Five codes cover the full surface: `ROUTE_NOT_FOUND` when the router cannot resolve a `(meshId, peerId)` pair, `LOOP_DETECTED` when the loop-guard sees the local host in `visitedNodes`, `TTL_EXHAUSTED` when an envelope's hop budget is spent, `BACKBONE_DISCONNECTED` when the host-to-host backbone has lost the next-hop link, and `ENVELOPE_INVALID` when a Zod check on the wire frame fails. The companion `RelayError` extends `SuaveplanError<RelayErrorCode>` so consumers pattern-match on `code` rather than parse messages.
|
||||
|
||||
## Features
|
||||
|
||||
- Five frozen codes registered under the `NET_RELAY` namespace.
|
||||
- `RelayError extends SuaveplanError<RelayErrorCode>`.
|
||||
- `name: "RelayError"` on every instance — useful for log filters and dashboard alerts.
|
||||
- `cause` and `context` propagation via `SuaveplanErrorOptions`.
|
||||
- Codes are stable across versions; the registry only ever grows, never shrinks.
|
||||
|
||||
## Usage: Basic Example
|
||||
|
||||
```ts
|
||||
import {
|
||||
RELAY_CODES,
|
||||
RelayError,
|
||||
} from "@suaveplan/network-plugin-relay";
|
||||
|
||||
throw new RelayError(RELAY_CODES.ROUTE_NOT_FOUND, "relay.route-not-found", {
|
||||
context: { targetMeshId: "mesh-B", targetPeerId: "bob" },
|
||||
});
|
||||
```
|
||||
|
||||
## Usage: Advanced Example
|
||||
|
||||
```ts
|
||||
import {
|
||||
RELAY_CODES,
|
||||
RelayError,
|
||||
} from "@suaveplan/network-plugin-relay";
|
||||
|
||||
try {
|
||||
await manager.route(envelope);
|
||||
} catch (err) {
|
||||
if (err instanceof RelayError) {
|
||||
if (err.code === RELAY_CODES.LOOP_DETECTED) {
|
||||
metrics.counter("relay.loops").increment();
|
||||
return;
|
||||
}
|
||||
if (err.code === RELAY_CODES.TTL_EXHAUSTED) {
|
||||
metrics.counter("relay.ttl-exhausted").increment();
|
||||
return;
|
||||
}
|
||||
}
|
||||
throw err;
|
||||
}
|
||||
```
|
||||
|
||||
## API Reference
|
||||
|
||||
- `RELAY_CODES` — frozen `{ ROUTE_NOT_FOUND, LOOP_DETECTED, TTL_EXHAUSTED, BACKBONE_DISCONNECTED, ENVELOPE_INVALID }`.
|
||||
- `type RelayErrorCode` — union of registered code values.
|
||||
- `class RelayError extends SuaveplanError<RelayErrorCode>` — typed throwable.
|
||||
|
||||
## Implementation Notes
|
||||
|
||||
`LOOP_DETECTED` and `TTL_EXHAUSTED` are intentionally distinct codes even though they both indicate "stop relaying this envelope." Loops mean the topology is misconfigured (or the visitedNodes list lost an entry); TTL exhaustion is a normal end-of-life signal for over-aged envelopes. Conflating them would hide topology bugs behind expected drops. The package barrel side-effect-imports `./error-codes.js` so subpath consumers register the codes too.
|
||||
@@ -0,0 +1,67 @@
|
||||
# LoopGuard
|
||||
|
||||
<!-- begin:auto-docs -->
|
||||
_(no submodules)_
|
||||
<!-- end:auto-docs -->
|
||||
## Purpose
|
||||
|
||||
Verifies a cross-mesh envelope can safely be forwarded. Two checks: TTL (hopCount + 1 ≤ maxHops) and loop detection (local host id not in visitedNodes). On success, returns a new envelope with `localHostId` appended to `visitedNodes` and `hopCount` incremented. Centralising this in one class means every relay component (manager, backbone, integration tests) enforces the same invariant.
|
||||
|
||||
## Features
|
||||
|
||||
- Two failure modes: `TTL_EXHAUSTED` and `LOOP_DETECTED` (kept distinct on purpose).
|
||||
- `assertCanForward(envelope)` — pure check, no mutation.
|
||||
- `stamp(envelope): CrossMeshEnvelope` — checks then returns a stamped copy.
|
||||
- Configurable `maxVisitedHistory` (default 64) — caps memory on long routes.
|
||||
- Truncates `visitedNodes` from the head when history exceeds the cap, preserving the most recent hops.
|
||||
- Read-only `localHostId` — guard instance is per-host and does not mutate over time.
|
||||
|
||||
## Usage: Basic Example
|
||||
|
||||
```ts
|
||||
import { LoopGuard } from "@suaveplan/network-plugin-relay";
|
||||
|
||||
const guard = new LoopGuard({ localHostId: "host-X" });
|
||||
const stamped = guard.stamp(envelope);
|
||||
await backbone.send(nextHopId, stamped);
|
||||
```
|
||||
|
||||
## Usage: Advanced Example
|
||||
|
||||
```ts
|
||||
import {
|
||||
LoopGuard,
|
||||
RELAY_CODES,
|
||||
RelayError,
|
||||
} from "@suaveplan/network-plugin-relay";
|
||||
|
||||
const guard = new LoopGuard({
|
||||
localHostId: "host-X",
|
||||
maxVisitedHistory: 16,
|
||||
});
|
||||
|
||||
try {
|
||||
const stamped = guard.stamp(envelope);
|
||||
await forward(stamped);
|
||||
} catch (err) {
|
||||
if (err instanceof RelayError) {
|
||||
if (err.code === RELAY_CODES.LOOP_DETECTED) {
|
||||
metrics.counter("relay.loops").increment();
|
||||
} else if (err.code === RELAY_CODES.TTL_EXHAUSTED) {
|
||||
metrics.counter("relay.ttl-exhausted").increment();
|
||||
}
|
||||
return;
|
||||
}
|
||||
throw err;
|
||||
}
|
||||
```
|
||||
|
||||
## API Reference
|
||||
|
||||
- `interface LoopGuardConfig { localHostId, maxVisitedHistory? }`.
|
||||
- `assertCanForward(envelope)` — throws `TTL_EXHAUSTED` / `LOOP_DETECTED`.
|
||||
- `stamp(envelope): CrossMeshEnvelope` — returns a fresh stamped envelope.
|
||||
|
||||
## Implementation Notes
|
||||
|
||||
The check ordering (TTL first, loop second) is deliberate: an over-aged envelope is dropped before we even consider whether it has visited the local host, since TTL exhaustion is a routine end-of-life signal whereas a loop is a topology bug. `stamp` returns a new object rather than mutating the input so callers can safely reuse the original envelope (e.g. for diagnostic logging) without worrying about iteration-order surprises.
|
||||
@@ -0,0 +1,84 @@
|
||||
# RelayManager
|
||||
|
||||
<!-- begin:auto-docs -->
|
||||
_(no submodules)_
|
||||
<!-- end:auto-docs -->
|
||||
## Purpose
|
||||
|
||||
Plugin entry point that orchestrates the relay router, host-backbone transport, mesh directory, and loop guard into a single API. Two flows: `route(envelope)` for outbound traffic the local node originates, and `deliverInbound(envelope, fromHostId)` for envelopes arriving from another Tier 0 host. Both share the same `handle(envelope)` core so loop and TTL semantics are identical regardless of direction.
|
||||
|
||||
## Features
|
||||
|
||||
- Local-mesh short-circuit: if `targetMeshId === localMeshId`, calls the `deliverLocally` handler directly.
|
||||
- Remote routing: stamps via loop guard, resolves the next hop, hands off to the backbone.
|
||||
- `attachToBackbone()` — wires the backbone's onMessage into `deliverInbound`; returns an unsubscribe function for clean teardown.
|
||||
- `ROUTE_NOT_FOUND` raised when the router cannot resolve `(meshId, peerId)`.
|
||||
- Read-only `getLocalMeshId()`, `getLocalHostId()`, `getDirectory()` for downstream observers.
|
||||
|
||||
## Usage: Basic Example
|
||||
|
||||
```ts
|
||||
import {
|
||||
HostBackboneTransport,
|
||||
LoopGuard,
|
||||
MeshDirectory,
|
||||
RelayManager,
|
||||
RelayRouter,
|
||||
} from "@suaveplan/network-plugin-relay";
|
||||
|
||||
const manager = new RelayManager({
|
||||
localMeshId: "mesh-A",
|
||||
localHostId: "host-A",
|
||||
router: new RelayRouter(),
|
||||
backbone: new HostBackboneTransport({ wire }),
|
||||
directory: new MeshDirectory(),
|
||||
loopGuard: new LoopGuard({ localHostId: "host-A" }),
|
||||
deliverLocally: async (env) => {
|
||||
await intraMeshTransport.send(env.targetId, env);
|
||||
},
|
||||
});
|
||||
|
||||
const off = manager.attachToBackbone();
|
||||
await manager.route(crossMeshEnvelope);
|
||||
off();
|
||||
```
|
||||
|
||||
## Usage: Advanced Example
|
||||
|
||||
```ts
|
||||
import {
|
||||
RELAY_CODES,
|
||||
RelayError,
|
||||
RelayManager,
|
||||
} from "@suaveplan/network-plugin-relay";
|
||||
|
||||
try {
|
||||
await manager.route(envelope);
|
||||
} catch (err) {
|
||||
if (err instanceof RelayError) {
|
||||
switch (err.code) {
|
||||
case RELAY_CODES.ROUTE_NOT_FOUND:
|
||||
await rebuildRoutesFromDirectory(manager.getDirectory());
|
||||
return manager.route(envelope);
|
||||
case RELAY_CODES.LOOP_DETECTED:
|
||||
case RELAY_CODES.TTL_EXHAUSTED:
|
||||
metrics.counter("relay.dropped").increment();
|
||||
return;
|
||||
}
|
||||
}
|
||||
throw err;
|
||||
}
|
||||
```
|
||||
|
||||
## API Reference
|
||||
|
||||
- `type LocalDeliveryHandler = (envelope) => void | Promise<void>`.
|
||||
- `interface RelayManagerConfig { localMeshId, localHostId, router, backbone, directory, loopGuard, deliverLocally }`.
|
||||
- `route(envelope): Promise<void>`.
|
||||
- `deliverInbound(envelope, fromHostId): Promise<void>`.
|
||||
- `attachToBackbone(): () => void`.
|
||||
- `getLocalMeshId() / getLocalHostId() / getDirectory()`.
|
||||
|
||||
## Implementation Notes
|
||||
|
||||
Outbound and inbound paths share `handle(envelope)` deliberately — a relayed envelope arriving from a peer host is in every meaningful sense a fresh outbound from the relay's perspective, except for the visited-nodes history (which the loop guard consults via the envelope itself, not the call site). This keeps loop and TTL semantics identical regardless of direction. `deliverLocally` is a callback rather than a built-in transport because the manager has no opinion about how local delivery happens — that's the host's responsibility.
|
||||
@@ -0,0 +1,58 @@
|
||||
# RelayRouter
|
||||
|
||||
<!-- begin:auto-docs -->
|
||||
_(no submodules)_
|
||||
<!-- end:auto-docs -->
|
||||
## Purpose
|
||||
|
||||
Cross-mesh routing table. Maps `(destMeshId, destPeerId)` to a next-hop host id. The relay manager consults this on every outbound cross-mesh send to decide whether to forward to a backbone peer or drop with `ROUTE_NOT_FOUND`. Centralising the table here means directory sync, manual ops tooling, and the manager all read/write the same source of truth.
|
||||
|
||||
## Features
|
||||
|
||||
- `addRoute(destMeshId, destPeerId, nextHopHostId)` — inserts or updates.
|
||||
- `removeRoute(destMeshId, destPeerId)` — returns `true` if a route was removed.
|
||||
- `resolve(targetMeshId, targetPeerId): string | null` — null on miss.
|
||||
- `getRoutesForMesh(meshId)` — view of every per-peer entry inside one destination mesh.
|
||||
- `size()` and `clear()` for observability and teardown.
|
||||
|
||||
## Usage: Basic Example
|
||||
|
||||
```ts
|
||||
import { RelayRouter } from "@suaveplan/network-plugin-relay";
|
||||
|
||||
const router = new RelayRouter();
|
||||
router.addRoute("mesh-B", "bob", "host-X");
|
||||
|
||||
const nextHop = router.resolve("mesh-B", "bob");
|
||||
```
|
||||
|
||||
## Usage: Advanced Example
|
||||
|
||||
```ts
|
||||
import { RelayRouter } from "@suaveplan/network-plugin-relay";
|
||||
|
||||
const router = new RelayRouter();
|
||||
|
||||
// Bulk-load from a directory snapshot.
|
||||
for (const peer of meshDirectorySnapshot) {
|
||||
router.addRoute(peer.meshId, peer.peerId, peer.nextHopHostId);
|
||||
}
|
||||
|
||||
// Inspect every route into mesh-B.
|
||||
const meshBRoutes = router.getRoutesForMesh("mesh-B");
|
||||
for (const [peerId, nextHop] of meshBRoutes) {
|
||||
console.log(peerId, "→", nextHop);
|
||||
}
|
||||
```
|
||||
|
||||
## API Reference
|
||||
|
||||
- `addRoute(destMeshId, destPeerId, nextHopHostId)`.
|
||||
- `removeRoute(destMeshId, destPeerId): boolean`.
|
||||
- `resolve(targetMeshId, targetPeerId): string | null`.
|
||||
- `getRoutesForMesh(meshId): ReadonlyMap<string, string>`.
|
||||
- `size(): number` / `clear()`.
|
||||
|
||||
## Implementation Notes
|
||||
|
||||
The internal key is `"${meshId}::${peerId}"` rather than nested `Map<Map>` so existence checks are a single hashmap lookup. We use `::` as a separator on the assumption that mesh and peer ids never embed it themselves; consumers that need to support arbitrary ids should normalise before passing in. `getRoutesForMesh` walks the full set on demand rather than keeping a per-mesh index because the target use-case (occasional ops tooling and snapshot exports) is much less hot than the per-call `resolve`.
|
||||
Reference in New Issue
Block a user