//! Transport-agnostic messaging framework. //! //! Enables actors on different runtimes to communicate transparently via //! pluggable codecs (serialization) and transports (delivery protocol). //! //! # Two layers of pluggability //! //! - **[`Codec`]**: HOW bytes are encoded — gRPC/protobuf, bincode, custom, etc. //! - **[`Transport`]**: WHERE bytes are sent — in-memory, gRPC channel, TCP, etc. use std::any::{Any, TypeId}; use std::collections::HashMap; use std::sync::{Arc, RwLock}; use crate::actor::{ActorAddress, Message}; use crate::Error; // ─── Codec ────────────────────────────────────────────────────────────────── /// User-implemented codec for a specific message type. /// /// This is where serialization logic lives — gRPC/protobuf, bincode, /// msgpack, or any custom format. The framework imposes no serialization /// constraints on message types; the codec defines what it needs from `M`. pub trait Codec: Send + Sync + 'static { fn encode(&self, msg: &M) -> Result, Error>; fn decode(&self, bytes: &[u8]) -> Result; } // ─── NetworkMessage ───────────────────────────────────────────────────────── /// Marker for messages that can cross runtime boundaries. /// /// The only requirement is a stable `type_tag` string used for deserialization /// routing on the receiving side. No serialization bounds — the [`Codec`] /// handles that separately. pub trait NetworkMessage: Message { /// Stable identifier for this message type, used for deserialization routing. /// Must be unique per type and stable across compilations. /// Convention: `"crate_name::TypeName"`. fn type_tag() -> &'static str; } // ─── WireEnvelope ─────────────────────────────────────────────────────────── /// Serialized message ready for transport across runtime boundaries. /// /// A plain struct — the [`Transport`] implementation decides how to put it /// on the wire (protobuf, raw bytes, etc.). #[derive(Debug, Clone)] pub struct WireEnvelope { pub dest: ActorAddress, pub type_tag: String, pub payload: Vec, } // ─── Transport ────────────────────────────────────────────────────────────── /// Pluggable transport protocol. /// /// Implementations queue or send the envelope to a remote runtime. /// `send` should not block the calling thread. pub trait Transport: Send + Sync { fn send(&self, envelope: WireEnvelope) -> Result<(), Error>; } // ─── CodecRegistry ────────────────────────────────────────────────────────── type EncodeFn = Box) -> Result<(String, Vec), Error> + Send + Sync>; type DecodeFn = Box Result, Error> + Send + Sync>; /// Unified registry for encoding (`TypeId` → encoder) and decoding /// (`type_tag` → decoder). /// /// Built at setup time via [`register`](Self::register), then shared /// read-only via `Arc`. pub struct CodecRegistry { encoders: HashMap, decoders: HashMap, } impl CodecRegistry { pub fn new() -> Self { Self { encoders: HashMap::new(), decoders: HashMap::new(), } } /// Register a message type with its codec. /// /// Both encoding and decoding are handled by the same codec instance. pub fn register>(&mut self, codec: C) { let codec = Arc::new(codec); // Encoder side — closure downcasts Any → M, encodes, returns (tag, bytes) let encode_codec = codec.clone(); let encode_fn: EncodeFn = Box::new(move |msg: Box| { let typed = msg .downcast::() .map_err(|_| Error::from("Transport: type downcast failed during encode"))?; let bytes = encode_codec.encode(&*typed)?; Ok((M::type_tag().to_string(), bytes)) }); self.encoders.insert(TypeId::of::(), encode_fn); // Decoder side — closure captures Arc let decode_fn: DecodeFn = Box::new(move |bytes: &[u8]| { let msg: M = codec.decode(bytes)?; Ok(Box::new(msg) as Box) }); self.decoders.insert(M::type_tag().to_string(), decode_fn); } /// Encode a type-erased message. Returns `(type_tag, payload_bytes)`. pub fn encode( &self, type_id: TypeId, msg: Box, ) -> Result<(String, Vec), Error> { let encoder = self.encoders.get(&type_id).ok_or_else(|| { Error::from("Transport: message type not registered for remote transport") })?; encoder(msg) } /// Decode bytes back to a type-erased message using the `type_tag` key. pub fn decode( &self, type_tag: &str, bytes: &[u8], ) -> Result, Error> { let decoder = self.decoders.get(type_tag).ok_or_else(|| { Error::from(format!("Transport: unknown type_tag '{type_tag}'")) })?; decoder(bytes) } /// Deserialize a [`WireEnvelope`] into an address and type-erased message. pub fn receive( &self, envelope: WireEnvelope, ) -> Result<(ActorAddress, Box), Error> { let payload = self.decode(&envelope.type_tag, &envelope.payload)?; Ok((envelope.dest, payload)) } } // ─── TransportRouter ──────────────────────────────────────────────────────── /// Maps remote actor addresses to their [`Transport`]. pub struct TransportRouter { routes: RwLock>>, } impl TransportRouter { pub fn new() -> Self { Self { routes: RwLock::new(HashMap::new()), } } /// Register a remote address as reachable via the given transport. pub fn add_route(&self, addr: ActorAddress, transport: Arc) { self.routes.write().unwrap().insert(addr, transport); } /// Look up which transport handles a given address. pub(crate) fn lookup(&self, addr: &ActorAddress) -> Option> { self.routes.read().unwrap().get(addr).cloned() } } // ─── InMemoryTransport ────────────────────────────────────────────────────── /// In-process transport connecting two runtimes via an `mpsc` channel. /// /// Use [`pair`](Self::pair) to create a linked transport + receiver. pub struct InMemoryTransport { tx: std::sync::Mutex>, } impl InMemoryTransport { /// Create a linked pair: the transport sends to the returned receiver. pub fn pair() -> (Arc, std::sync::mpsc::Receiver) { let (tx, rx) = std::sync::mpsc::channel(); let transport = Arc::new(InMemoryTransport { tx: std::sync::Mutex::new(tx), }); (transport, rx) } } impl Transport for InMemoryTransport { fn send(&self, envelope: WireEnvelope) -> Result<(), Error> { self.tx .lock() .unwrap() .send(envelope) .map_err(|_| Error::from("InMemoryTransport: receiver dropped")) } } // ─── send_via_transport (crate-internal helper) ───────────────────────────── /// Attempt to serialize and send a message via the transport router. /// /// Called by the send paths in `Runtime` and `WorkerContext` when an address /// is not found locally or in the inbox registry. pub(crate) fn send_via_transport( addr: ActorAddress, msg: Box, codec_registry: &CodecRegistry, transport_router: &TransportRouter, ) -> Result<(), Error> { let transport = transport_router .lookup(&addr) .ok_or_else(|| Error::from("Address not found"))?; let type_id = (*msg).type_id(); let (type_tag, payload) = codec_registry.encode(type_id, msg)?; let wire = WireEnvelope { dest: addr, type_tag, payload, }; transport.send(wire) }