- 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>
6.4 KiB
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.
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 fromM.Transport— WHERE bytes are sent. InMemory (testing), TCP, gRPC, etc.
See transport_routing.svg for the extended routing chain, and 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:
- AddressMap::lookup → local worker delivery (zero-copy, no serialize)
- InboxRegistry::contains → external inbox delivery
- TransportRouter::lookup → codec.encode + transport.send (remote)
- 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
// 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 |