swactor/docs/transport.md
Developer 548396ba4a feat: in-browser runtime, WebSocket gateway, dashboard improvements
- NodeAddr abstraction replacing SocketAddr across distribution crate (~30 files)
- Core wasm32 compatibility: web-time, cfg-gated threads, wire encoding extraction
- WebSocket transport (browser) with GatewayControl protocol
- WebSocket gateway (native) with session routing and control protocol
- BrowserRuntime API: JS actor support, connect/spawn/send/tick
- Dashboard: extracted HTML to static files, gossip protocol panel with
  membership event log, visual fixes, click-to-filter, worker-colored rows

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-02-12 17:51:36 +00:00

154 lines
6.4 KiB
Markdown

# Transport-Agnostic Messaging
Enables actors on different runtimes to communicate transparently via
pluggable codecs (serialization) and transports (delivery protocol).
**Feature-gated:** `#[cfg(feature = "transport")]`. Without the flag, the
binary is identical to the baseline runtime.
```bash
cargo build --features transport
cargo test --features transport
```
## Two Layers of Pluggability
- **`Codec<M>`** — HOW bytes are encoded. gRPC/protobuf, bincode, custom, etc.
No serde bounds — the codec defines what it needs from `M`.
- **`Transport`** — WHERE bytes are sent. InMemory (testing), TCP, gRPC, etc.
See [transport_routing.svg](../crates/runtime-dashboard/docs/transport_routing.svg)
for the extended routing chain, and
[transport_encode_decode.svg](../crates/runtime-dashboard/docs/transport_encode_decode.svg)
for the encode/decode data flow.
## Core Types
| Type | Role |
|------|------|
| `Codec<M>` | User-implemented encode/decode for a message type |
| `NetworkMessage` | Marker trait: adds `fn type_tag() -> &'static str` for wire routing |
| `WireEnvelope` | `{ dest: ActorAddress, type_tag: String, payload: Vec<u8> }` |
| `Transport` | `fn send(WireEnvelope) -> Result<()>` — pluggable delivery |
| `CodecRegistry` | Maps `TypeId → encoder` (send side) and `type_tag → decoder` (receive side) |
| `TransportRouter` | Maps `ActorAddress → Arc<dyn Transport>` for remote addresses |
| `TransportBridge` | Deserializes incoming `WireEnvelope` → `(ActorAddress, Box<dyn Any + Send>)` |
| `InMemoryTransport` | mpsc-backed transport for testing |
## Routing Chain
Without transport, unresolved addresses fall through to `InboxRegistry`.
With transport enabled, a third step is inserted:
1. **AddressMap::lookup** → local worker delivery (zero-copy, no serialize)
2. **InboxRegistry::contains** → external inbox delivery
3. **TransportRouter::lookup** → codec.encode + transport.send (remote)
4. **Fallback** → `InboxRegistry::try_deliver` (Err if not found)
This chain runs in `Runtime::send_to`, `ContextInner for Runtime`, and
`WorkerContext::send_any` — all three follow the same logic.
## Type Erasure Bridge
Messages are `Box<dyn Any + Send>` before routing, but `dyn Any` can't be
serialized. The bridge:
**Send:** `Box<dyn Any>` → `(*msg).type_id()` → `encoders[TypeId]` →
downcast to `M` → `Codec<M>::encode` → `(type_tag, Vec<u8>)` → `WireEnvelope`
**Receive:** `WireEnvelope` → `decoders[type_tag]` → `Codec<M>::decode` →
`Box::new(msg) as Box<dyn Any + Send>` → `runtime.deliver_raw(addr, msg)`
`TypeId` (compiler-assigned, process-local) is used for encoding.
`type_tag` (user-defined, stable) is used on the wire for decoding.
## Setup
```rust
// 1. Register codecs
let mut codecs = CodecRegistry::new();
codecs.register::<Ping, _>(MyCodec);
codecs.register::<Pong, _>(MyCodec);
// 2. Create transport + router
let (transport, rx) = InMemoryTransport::pair();
let router = TransportRouter::new();
router.add_route(remote_addr, transport);
// 3. Attach to runtime
let mut rt = Runtime::new(RuntimeConfig::default());
rt.set_codec_registry(Arc::new(codecs));
rt.set_transport_router(Arc::new(router));
// 4. Send transparently
rt.send_to(remote_addr, Ping { value: 42, reply_to: inbox_addr }).unwrap();
// 5. Receive side: bridge deserializes, deliver_raw injects
let bridge = TransportBridge::new(codecs_arc);
let (addr, msg) = bridge.receive(wire_envelope).unwrap();
rt.deliver_raw(addr, msg).unwrap();
```
## Address Resolution
Addresses are **not automatically discovered**. Each runtime must be told
which remote addresses exist via `router.add_route()`. Since addresses are
32 random bytes, runtimes must exchange them out-of-band (e.g., over the TCP
connection itself — see `examples/tcp_ping_pong.rs`).
## Wire Format
The core transport module provides canonical encode/decode functions for the
binary wire format. These are used by both the TCP transport (distribution
crate) and the WebSocket transport (wasm crate):
```
encode_wire_envelope(envelope) → bytes:
┌──────────────┬───────────────┬──────────────┬──────────┬─────────┐
│ frame_len │ dest address │ tag_len │ type_tag │ payload │
│ 4 bytes BE │ 32 bytes │ 4 bytes BE │ N bytes │ rest │
└──────────────┴───────────────┴──────────────┴──────────┴─────────┘
```
`decode_wire_envelope(frame)` takes the bytes *after* the 4-byte length prefix
and returns a `WireEnvelope`. The caller is responsible for reading the length
prefix and providing exactly `frame_len` bytes.
## WebSocket Transport (Browser)
The `swactor-wasm` crate provides `WsTransport` — a `Transport` impl over
`web_sys::WebSocket`. It uses the same wire format as TCP (binary frames with
length prefix). Messages are buffered until the WebSocket connection opens.
A `GatewayControl` protocol (JSON over reserved null address) handles actor
registration and keepalive between browser and gateway.
See `crates/wasm/src/ws_transport.rs` and `crates/gateway/` for the browser
and native sides respectively.
## Limitations
- **No automatic discovery** — manual address exchange required
- **Transport::send is synchronous** — blocking transports stall the worker
- **One route per address** — no wildcard/prefix routing
- **No ordering guarantees** across transports (depends on transport impl)
- **No back-pressure** from remote — fire-and-forget delivery
- **TypeId is not stable** across compilations (only used process-locally; wire uses type_tag)
## Where Things Live
| Concept | File |
|---------|------|
| All transport types | `src/transport.rs` |
| `InboxRegistry::contains()` | `src/delivery.rs` |
| Transport fields on `TickContext` | `src/delivery.rs` |
| `Runtime::deliver_raw`, setters | `src/runtime.rs` |
| Transport fallback in worker | `src/worker.rs` |
| Wire encode/decode | `src/transport.rs` |
| Integration tests | `tests/transport_api.rs` |
| TCP example | `examples/tcp_ping_pong.rs` |
| WebSocket transport (browser) | `crates/wasm/src/ws_transport.rs` |
| Gateway control protocol | `crates/wasm/src/protocol.rs` |
| WebSocket gateway (native) | `crates/gateway/src/lib.rs` |
| WS acceptor | `crates/gateway/src/ws_acceptor.rs` |
| Gateway example | `examples/ws_gateway.rs` |