Serviceless is a small async actor library for Rust, inspired by Actix-style APIs but kept
minimal: one mailbox per [Service], fully async handlers, and addresses for typed messaging plus
optional topic notifications.
The implementation of this crate does not use unsafe.
- Async actors — Each service runs a mailbox loop;
started/stoppedhooks andHandler::handleareasync(use theasync_traitcrate). - Typed messages — Implement
MessageandHandlerfor your own types instead of manual routing tables. callandsend—ServiceAddress::callawaitsM::Result;sendenqueues work and drops the handler return value.- Topics (pub/sub-style) —
Topic,RoutedTopic, andTopicEndpointfor one-shot subscribe / publish flows, still serialized through the actor mailbox. - External envelope streams —
Context::with_streammerges another stream ofEnvelopes with the internal mailbox. - Typed narrowing —
ServiceAddress::into_addressbuilds a single-message-typeAddressplus a forwarding future you spawn next to the main run future. - Bring your own runtime — The library returns a
runfuture; you spawn it (examples use Tokio). There are no optional Cargo[features]on this crate: the full API is always available.
- Run
cargo doc --open -p servicelessfor full API reference. - Narrative guide (actor usage, caveats, pub/sub): see the
serviceless::docsmodule in the generated docs (overview, services, messaging, pub/sub, runtime).
A Service is your actor type. You must set the associated Stream type (often
EmptyStream<Self> when
you only use the built-in mailbox).
use async_trait::async_trait;
use serviceless::{Context, EmptyStream, Service};
#[derive(Default)]
pub struct MyActor;
#[async_trait]
impl Service for MyActor {
type Stream = EmptyStream<Self>;
async fn started(&mut self, _ctx: &mut Context<Self, Self::Stream>) {
// runs once before the mailbox loop
}
async fn stopped(&mut self, _ctx: &mut Context<Self, Self::Stream>) {
// runs after the mailbox is closed
}
}Build a Context, start the
service, then spawn the returned run future on your async runtime. Until that future is polled,
mailbox work will not run.
use serviceless::Context;
# use async_trait::async_trait;
# use serviceless::{EmptyStream, Service};
# #[derive(Default)] struct MyActor;
# #[async_trait] impl Service for MyActor { type Stream = EmptyStream<Self>; }
let actor = MyActor::default();
let ctx = Context::new();
let (addr, run) = actor.start_by_context(ctx);
tokio::spawn(run);Stop from inside the actor with Context::stop,
or from outside with
ServiceAddress::close_service.
Declare a Message (with
type Result) and implement Handler
for your service.
use async_trait::async_trait;
use serviceless::{Context, EmptyStream, Handler, Message, Service};
#[derive(Default)]
pub struct Service0;
#[async_trait]
impl Service for Service0 { type Stream = EmptyStream<Self>; }
pub struct U8(pub u8);
impl Message for U8 {
type Result = U8;
}
#[async_trait]
impl Handler<U8> for Service0 {
async fn handle(&mut self, message: U8, _ctx: &mut Context<Self, Self::Stream>) -> U8 {
U8(message.0 + 2)
}
}ServiceAddress is
cloneable and is how other tasks talk to the actor.
call—async; waits forM::Result. If the service has stopped, you getError::ServiceStoped.send— synchronous for the caller; still returnsResultand drops the handler return value.- Preferred dispatch —
callandsenddispatch throughHandler::handle_preferred. The defaulthandle_preferredimplementation callshandle, then usesReplyHandlefor replies. You can overridehandle_preferredto spawn a task and reply later so the current handler path does not block mailbox progress (seeactor/examples/preferred.rs). subscribe— synchronous enqueue with a topic key argument; returnsResultof aFutureyou await for the next matching publication (seeserviceless::docs::pubsubandexamples/topic.rs).
Runnable examples live under actor/examples/ (e.g. topic.rs, single.rs, external_stream.rs,
preferred.rs).
cargo run -p serviceless --example topic