200 lines
8.3 KiB
Markdown
200 lines
8.3 KiB
Markdown
# Network Architecture
|
|
|
|
Canonical capability spec: [`openspec/specs/network/spec.md`](../../openspec/specs/network/spec.md) (`NET-01..NET-125`).
|
|
|
|
## The five core abstractions
|
|
|
|
| Abstraction | Role | Spec |
|
|
| --- | --- | --- |
|
|
| `NetworkPlugin` | The universal extension contract every plugin implements | NET-01 |
|
|
| `NetworkNode` | The single entry-point — owns lifecycle, bus, router, transports | NET-02 |
|
|
| `MessageEnvelope` | Typed wire format for every message | NET-03 |
|
|
| `Transport` | The contract every transport plugin satisfies | NET-04 |
|
|
| `NetworkBus` | The cross-plugin event broker (no plugin imports another plugin) | NET-05 |
|
|
|
|
These are the only five things `network-core` ships. Everything else — even WebRTC — is a plugin.
|
|
|
|
## `NetworkPlugin` (NET-01)
|
|
|
|
```ts
|
|
interface NetworkPlugin {
|
|
readonly name: string;
|
|
install(node: NetworkNode): Promise<void> | void;
|
|
start(): Promise<void> | void;
|
|
stop(): Promise<void> | void;
|
|
dispose?(): Promise<void> | void;
|
|
}
|
|
```
|
|
|
|
Lifecycle is strict: `install → start → ... → stop → dispose`. The core calls them in that order; plugins must be idempotent across `start`/`stop` (you can restart without disposing). **Plugins do not import each other.** Cross-plugin communication goes through `NetworkBus`.
|
|
|
|
## `NetworkNode` (NET-02)
|
|
|
|
```ts
|
|
class NetworkNode {
|
|
use(plugin: NetworkPlugin): this;
|
|
start(): Promise<void>;
|
|
stop(): Promise<void>;
|
|
|
|
send(targetId: PeerId, envelope: MessageEnvelope): Promise<void>;
|
|
broadcast(envelope: MessageEnvelope): Promise<void>;
|
|
on<E extends NetworkEvents>(event: E, handler: Handler<E>): () => void;
|
|
|
|
readonly bus: NetworkBus;
|
|
readonly router: MessageRouter;
|
|
|
|
bootstrap(options: BootstrapOptions): Promise<void>; // NET-113
|
|
}
|
|
```
|
|
|
|
The node owns plugin lifecycle, transport pool, and message routing. Sending is transport-agnostic — the node hands the envelope to whichever active transport can reach the target peer.
|
|
|
|
## `MessageEnvelope` (NET-03)
|
|
|
|
```ts
|
|
interface MessageEnvelope {
|
|
id: string; // unique
|
|
type: string; // e.g. 'cd:request', 'election:vote'
|
|
senderId: PeerId;
|
|
targetId: PeerId | "*"; // "*" = broadcast
|
|
payload: Uint8Array; // already-encoded; protocol plugin handles serialisation
|
|
timestamp: number; // injectable Clock
|
|
headers: Record<string, string>;
|
|
}
|
|
```
|
|
|
|
Validated via Zod before routing. The 13-byte binary header from `network-plugin-protocol` (`NET-15`) prefixes this when serialising for the wire: `version(1) | flags(1) | messageType(1) | timestamp(8) | payloadLength(2)`.
|
|
|
|
## `Transport` (NET-04)
|
|
|
|
```ts
|
|
interface Transport {
|
|
connect(peerId: PeerId, address: string): Promise<void>;
|
|
disconnect(peerId: PeerId): Promise<void>;
|
|
send(peerId: PeerId, envelope: MessageEnvelope): Promise<void>;
|
|
onMessage(handler: (env: MessageEnvelope) => void): () => void;
|
|
getConnectedPeers(): PeerId[];
|
|
}
|
|
```
|
|
|
|
Concrete transports: WebRTC (`network-plugin-webrtc`), WebSocket (`network-websockets`), HTTPS (planned), libp2p (`network-libp2p` — pending), Hyperswarm (`network-hyperswarm` — pending), Trystero (`network-discovery-trystero` — pending), GunDB (`network-discovery-gundb` — pending), MemoryTransport (test-only, in `network-testing`).
|
|
|
|
`network-plugin-transport-manager` (NET-23..NET-26) maintains multiple active transports per peer simultaneously, exchanges capability bitmaps during handshake (NET-24), and routes messages by priority across them (NET-25). CRITICAL messages may go redundantly across two transports; BULK uses backpressure-aware selection.
|
|
|
|
## `NetworkBus` (NET-05)
|
|
|
|
Typed pub/sub with FIFO delivery, bounded-queue backpressure, and interceptor support. Topics are constants from the frozen `NetworkEvents` enum (NET-55):
|
|
|
|
```text
|
|
node:created, node:connected, node:disconnected, node:updated, node:removed,
|
|
message:sent, message:received, message:routed, message:dropped,
|
|
hierarchy:changed, hierarchy:parent-changed, hierarchy:child-added, ...,
|
|
plugin:registered, plugin:unregistered,
|
|
peer:joined, peer:left, peer:updated, peer:latency,
|
|
transport:connected, transport:disconnected,
|
|
election:vote, election:completed, election:cancelled,
|
|
failover:triggered, failover:completed,
|
|
topology:changed, latency:rebalance,
|
|
cd:advertise, cd:request, cd:response,
|
|
partition:detected, partition:healed,
|
|
qos:congestion, monitor:alert, system:error
|
|
```
|
|
|
|
Plugins subscribe; plugins publish; the bus enforces ordering and backpressure. **No plugin ever imports another plugin's code.** That is the wire that lets `network-suite` compose 21 plugins without circular deps.
|
|
|
|
## `MessageRouter`
|
|
|
|
Owns:
|
|
|
|
- Per-message type → handler dispatch.
|
|
- TTL decrement (`NET-58`); drop when `ttl <= 0`.
|
|
- Loop detection via `visitedNodes` header (`NET-58`).
|
|
- Broadcast deduplication via `messageId` bloom filter.
|
|
- Route caching for frequently-used `(source, target) → nextHop` mappings (`NET-59`); invalidated on `topology:changed`.
|
|
|
|
## How a plugin actually talks to another plugin
|
|
|
|
Concrete example — `drive-mesh` wants a chunk from a peer:
|
|
|
|
```text
|
|
drive-mesh.fetchChunk(fileId, chunkId)
|
|
↓
|
|
networkNode.bus.publish('cd:request', { fileId, chunkId, requesterId })
|
|
↓
|
|
network-plugin-content-delivery.onCdRequest(...)
|
|
↓
|
|
resolves a peer holding the chunk via gossip index
|
|
↓
|
|
networkNode.send(holderPeerId, { type: 'cd:fetch', payload: { fileId, chunkId } })
|
|
↓
|
|
(transport-manager picks WebRTC, WebSocket, or HTTPS based on capability negotiation)
|
|
↓
|
|
holder runs network-plugin-content-delivery.onCdFetch and sends back chunk bytes
|
|
↓
|
|
networkNode.bus.publish('cd:response', { fileId, chunkId, bytes })
|
|
↓
|
|
drive-mesh.onCdResponse(...) → verify sig + AEAD via chunks plugin → return to caller
|
|
```
|
|
|
|
Three plugins coordinated; none imported any other; all communication via bus topics. Replace `network-plugin-content-delivery` with a different chunk-distribution implementation and the rest of the chain doesn't notice.
|
|
|
|
## `BootstrapOptions` (NET-113, NET-114)
|
|
|
|
```ts
|
|
interface BootstrapOptions {
|
|
mode: 'cold' | 'warm';
|
|
initialPeers?: PeerAddress[];
|
|
bootstrapServers?: string[];
|
|
connectionConcurrency?: number;
|
|
discoveryTimeout?: number;
|
|
prioritizeLocalNetwork?: boolean;
|
|
}
|
|
```
|
|
|
|
`NetworkNode.bootstrap(options)` brings up the node from a clean state — does discovery, opens initial connections, and (with `ConnectionPreWarmer`) pre-opens transports to known peers before they are needed.
|
|
|
|
## `PluginRegistry` and `PluginDiscovery` (NET-116)
|
|
|
|
```ts
|
|
class PluginRegistry {
|
|
register(plugin: NetworkPlugin): void;
|
|
unregister(name: string): void;
|
|
has(name: string): boolean;
|
|
get(name: string): NetworkPlugin | undefined;
|
|
list(): NetworkPlugin[];
|
|
}
|
|
|
|
class PluginDiscovery {
|
|
findPluginsByCapability(capability: string): NetworkPlugin[];
|
|
}
|
|
```
|
|
|
|
Runtime plugin discovery and registration — the same pattern the rest of the monorepo uses (storage factory, db factory, etc.).
|
|
|
|
## `TransportTester` PDK (NET-115)
|
|
|
|
Third-party transport plugins are validated by a conformance harness:
|
|
|
|
```ts
|
|
class TransportTester {
|
|
testBasicFunctionality(transport: Transport): Promise<TestReport>;
|
|
testReliability(transport: Transport): Promise<TestReport>;
|
|
testBackpressure(transport: Transport): Promise<TestReport>;
|
|
testReconnection(transport: Transport): Promise<TestReport>;
|
|
}
|
|
```
|
|
|
|
Anyone shipping a custom transport (e.g., a corporate VPN tunnel transport) runs `TransportTester` against it before registering with `network-plugin-transport-manager`. This is what makes "swap WebRTC for $YOUR_TRANSPORT" a one-line change.
|
|
|
|
## Why this shape
|
|
|
|
The core's job is to do **as little as possible**. It owns plugin lifecycle, the bus, the router, the envelope shape, and the transport contract — and stops. Every protocol decision (binary header layout, encryption, signing, election algorithm, partition recovery) lives in a plugin where it can be replaced.
|
|
|
|
This is the lesson the old planning corpus was groping toward but never quite landed: a "core + plugins" model where the core is genuinely small and the plugins genuinely don't depend on each other. The `network-suite` change folder finally delivers it; the canonical spec encodes it; this is the design genesis ships.
|
|
|
|
## Reading further
|
|
|
|
- [`topology-and-failover.md`](./topology-and-failover.md) — the resilience model.
|
|
- [`plugins-catalog.md`](./plugins-catalog.md) — what each plugin does.
|
|
- [`security-and-tokens.md`](./security-and-tokens.md) — the trust model.
|