2026-02-09 19:05:37 +00:00
|
|
|
# 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`).
|
|
|
|
|
|
2026-02-12 17:51:36 +00:00
|
|
|
## 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.
|
|
|
|
|
|
2026-02-09 19:05:37 +00:00
|
|
|
## 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` |
|
2026-02-12 17:51:36 +00:00
|
|
|
| Wire encode/decode | `src/transport.rs` |
|
2026-02-09 19:05:37 +00:00
|
|
|
| Integration tests | `tests/transport_api.rs` |
|
|
|
|
|
| TCP example | `examples/tcp_ping_pong.rs` |
|
2026-02-12 17:51:36 +00:00
|
|
|
| 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` |
|