Quickstart¶
Install¶
cargo add xan-actor
cargo add async-trait
bounded-channel is the default.
To use unbounded mode:
cargo add xan-actor --no-default-features --features unbounded-channel
Minimal Example¶
use xan_actor::prelude::*;
use std::sync::Arc;
#[derive(Debug, Clone)]
enum MsgA {
Ping(String),
}
#[derive(Debug, Clone)]
enum MsgB {
Echo(String),
}
#[derive(thiserror::Error, Debug)]
enum MyError {
#[error(transparent)]
Actor(#[from] ActorError),
}
struct ActorA {
address: String,
}
struct ActorB {
address: String,
}
#[async_trait::async_trait]
impl Actor for ActorA {
type Message = MsgA;
type Result = MsgA;
type Error = MyError;
fn address(&self) -> &str { &self.address }
async fn handle(&mut self, msg: Arc<Self::Message>) -> Result<Self::Result, Self::Error> {
Ok((*msg).clone())
}
}
#[async_trait::async_trait]
impl Actor for ActorB {
type Message = MsgB;
type Result = MsgB;
type Error = MyError;
fn address(&self) -> &str { &self.address }
async fn handle(&mut self, msg: Arc<Self::Message>) -> Result<Self::Result, Self::Error> {
Ok((*msg).clone())
}
}
#[tokio::main]
async fn main() -> Result<(), ActorError> {
let mut system = ActorSystem::new(None);
ActorA { address: "/a/1".into() }
.register(&mut system, ErrorHandling::Stop, Blocking::NonBlocking, None)
.await?;
ActorB { address: "/b/1".into() }
.register(&mut system, ErrorHandling::Stop, Blocking::NonBlocking, None)
.await?;
// compile-time check: only MsgA is accepted here
system.send::<ActorA>("/a/1".into(), MsgA::Ping("hello".into())).await?;
let result = system
.send_and_recv::<ActorB>("/b/1".into(), MsgB::Echo("world".into()))
.await?;
println!("result = {:?}", result);
Ok(())
}
What This Shows¶
- one
ActorSystemhostingActorAandActorB send::<ActorA>accepts onlyMsgAsend_and_recv::<ActorB>returnsActorB::Result
Multi-node Minimal Example¶
cargo add xan-actor --features multi-node
cargo add async-trait
cargo add thiserror
cargo add xancode # needed for `#[derive(Codec)]`; xan-actor doesn't re-export it
cargo add xanq # this example spawns its own in-process broker; skip if connecting to an external one
This example runs an in-process xanq broker so it works as a single binary.
Two ActorSystem instances (node-b and node-a) speak to it; the actor
lives on node-b and node-a calls it via a fully qualified Address.
See Multi-node for the full setup.
use std::sync::Arc;
use xan_actor::prelude::*; // Address, NodeFilter, Topic, ...
use xancode::Codec; // direct dependency on the user side
use xanq::server::Server;
// `Codec` is required so messages and results can cross node boundaries.
#[derive(Debug, Clone, Codec)]
enum Echo {
Ping(String),
Pong(String),
}
#[derive(thiserror::Error, Debug)]
enum E {
#[error(transparent)]
Actor(#[from] ActorError),
}
struct EchoActor {
addr: Address,
}
#[async_trait::async_trait]
impl Actor for EchoActor {
type Message = Echo;
type Result = Echo;
type Error = E;
fn address(&self) -> &Address { &self.addr }
async fn handle(&mut self, msg: Arc<Self::Message>) -> Result<Self::Result, Self::Error> {
match &*msg {
Echo::Ping(s) => Ok(Echo::Pong(format!("pong:{s}"))),
other => Ok(other.clone()),
}
}
}
// Module-scope registration. Wires `EchoActor` into the inter-node decoder/encoder
// registry so a peer node can deserialize messages addressed to it.
xan_actor::register_for_inter_node!(EchoActor);
#[tokio::main(flavor = "multi_thread")]
async fn main() -> Result<(), ActorError> {
// 1. Start an in-process xanq broker. Our `Address` impls `xanq::address::Address`,
// so it doubles as the Server's type parameter — no extra newtype needed.
let (_server, broker_addr) = Server::<Address>::spawn("127.0.0.1:0")
.await
.expect("spawn broker");
let broker = broker_addr.to_string();
// 2. Owner node hosts the actor. The actor's Address.node must match the system's node_name.
let mut node_b = ActorSystem::new(
None, // channel_size
"node-b".into(), // node_name
Some(broker.clone()), // broker_addr
).await?;
EchoActor { addr: Address::new("node-b", "/echo") }
.register(&mut node_b, ErrorHandling::Stop, Blocking::NonBlocking, None)
.await?;
// 3. Caller node.
let mut node_a = ActorSystem::new(None, "node-a".into(), Some(broker)).await?;
// 4. Same API as single-node — the Address.node field decides routing.
let resp = node_a
.send_and_recv::<EchoActor>(
Address::new("node-b", "/echo"),
Echo::Ping("hi".into()),
)
.await?;
println!("got = {resp:?}");
Ok(())
}
What's Different¶
Echoderivesxancode::Codecso the payload can be serialized.register_for_inter_node!(EchoActor)is called once at module scope.- The actor holds an
Address(struct), not aString.Actor::addressreturns&Address. ActorSystem::newisasyncand requiresnode_name;broker_addris optional.send_and_recv::<EchoActor>(Address::new("node-b", "/echo"), ...)— theAddress.nodefield structurally decides whether the call goes local (no broker) or remote (over the broker).