Initial Commit
This commit is contained in:
199
docs/network/architecture.md
Normal file
199
docs/network/architecture.md
Normal file
@@ -0,0 +1,199 @@
|
||||
# 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.
|
||||
Reference in New Issue
Block a user