feat: types for request/response handling

Introduce a typed `ActorRequestSender` connection handle for actors and harden the `Error` type.

- src/lib.rs: add `ActorRequestSender<A>` wrapping `mpsc::Sender<ActorRequest<A>>` with async `send`, `Clone`, and `From` impls; expose it via new `Handle::get_connection()` so callers hold a lightweight standalone connection to an actor
- src/lib.rs: route `Handle::send` through the new sender and store `tx` as an `ActorRequestSender`; drop the `Unpin` supertrait bound from the `Actor` trait
- src/error.rs: turn the `Error` type alias into a newtype struct, gate `convert_err` as `pub(crate)`, and add `From<T: AsRef<str>>` plus `ToString` impls
- examples/hello.rs: switch `Greeter::spawn` to method-call syntax (`Greeter.spawn(&rt)`) to match the updated API
- README.md: rename the project heading from "about" to "swactor"

Signed-off-by: Zachery Aaron Shores-Chmielewski <zacheryasc@gmail.com>
This commit is contained in:
Zachery Aaron Shores-Chmielewski 2025-11-25 20:51:13 -05:00
parent 88121780ce
commit 759acc622d
4 changed files with 57 additions and 17 deletions

View file

@ -1,2 +1,2 @@
# about # swactor
Small wasm-compatible actor library Small wasm-compatible actor library

View file

@ -30,7 +30,7 @@ fn main() {
let rt = tokio::runtime::Builder::new_current_thread() let rt = tokio::runtime::Builder::new_current_thread()
.build() .build()
.expect("failed to build runtime"); .expect("failed to build runtime");
let greeter = Greeter::spawn(Greeter, &rt); let greeter = Greeter.spawn(&rt);
let response = rt let response = rt
.block_on(async move { .block_on(async move {

View file

@ -1,5 +1,18 @@
pub type Error = Box<dyn std::error::Error + Send + Sync + 'static>; #[derive(Debug)]
pub struct Error(Box<dyn std::error::Error + Send + Sync + 'static>);
pub type Result<T> = std::result::Result<T, Error>; pub type Result<T> = std::result::Result<T, Error>;
pub fn convert_err<E: std::fmt::Debug>(e: E) -> Error { pub(crate) fn convert_err<E: std::fmt::Debug>(e: E) -> Error {
format!("{e:?}").into() Error(format!("{e:?}").into())
}
impl<T: AsRef<str>> From<T> for Error {
fn from(value: T) -> Self {
convert_err(value.as_ref())
}
}
impl ToString for Error {
fn to_string(&self) -> String {
format!("{:?}", self.0)
}
} }

View file

@ -3,37 +3,64 @@ pub mod error;
/// Public export as the oneshot channel is in the `Actor` trait signature /// Public export as the oneshot channel is in the `Actor` trait signature
pub use tokio::sync::oneshot; pub use tokio::sync::oneshot;
use tokio::{ use tokio::{sync::mpsc, task::JoinHandle};
sync::{mpsc},
task::JoinHandle,
};
use tokio_util::sync::CancellationToken; use tokio_util::sync::CancellationToken;
use crate::error::{Result, convert_err}; use crate::error::{Result, convert_err};
const DEFAULT_CHANNEL_SIZE: usize = 100; const DEFAULT_CHANNEL_SIZE: usize = 100;
type ActorRequest<A> = (
<A as Actor>::Message,
oneshot::Sender<<A as Actor>::Response>,
);
/// Wrapper defining the transmission end of a Request/Response channel with an `Actor`
pub struct ActorRequestSender<A: Actor>(mpsc::Sender<ActorRequest<A>>);
impl<A: Actor> ActorRequestSender<A> {
pub async fn send(&self, request: A::Message) -> Result<A::Response> {
let (tx, rx) = oneshot::channel::<A::Response>();
self.0.send((request, tx)).await.map_err(convert_err)?;
rx.await.map_err(convert_err)
}
}
impl<A: Actor> From<mpsc::Sender<ActorRequest<A>>> for ActorRequestSender<A> {
fn from(value: mpsc::Sender<ActorRequest<A>>) -> Self {
Self(value)
}
}
impl<A: Actor> Clone for ActorRequestSender<A> {
fn clone(&self) -> Self {
Self(self.0.clone())
}
}
/// Combined 'JoinHandle' to await the actor process and 'Sender' for communication /// Combined 'JoinHandle' to await the actor process and 'Sender' for communication
pub struct Handle<A> pub struct Handle<A>
where where
A: Actor, A: Actor,
{ {
cancel_token: CancellationToken, cancel_token: CancellationToken,
tx: mpsc::Sender<(A::Message, oneshot::Sender<A::Response>)>, tx: ActorRequestSender<A>,
/// the task drops when the `JoinHandle` does, so be careful with the `Handle`
_handle: JoinHandle<Result<()>>, _handle: JoinHandle<Result<()>>,
// to prevent accidental swaps, strongly type the handle // to prevent accidental swaps, strongly type the handle
_type: std::marker::PhantomData<A>, _type: std::marker::PhantomData<A>,
} }
impl<A: Actor> Handle<A> { impl<A: Actor> Handle<A> {
/// Send a message to the spawned `Actor` task and get a response corresponding to the `Actor::Response` type /// Send a message to the spawned `Actor` task and get a response corresponding to the `Actor::Response` type
pub async fn send(&self, msg: A::Message) -> Result<A::Response> { pub async fn send(&self, msg: A::Message) -> Result<A::Response> {
let (tx, rx) = oneshot::channel::<A::Response>(); self.tx.send(msg).await
self.tx.send((msg, tx)).await.map_err(convert_err)?; }
rx.await.map_err(convert_err) /// Get a cloned sender for messaging the `Actor` this handle is for
pub fn get_connection(&self) -> ActorRequestSender<A> {
self.tx.clone()
} }
} }
@ -44,7 +71,7 @@ impl<A: Actor> Drop for Handle<A> {
} }
/// Primary trait defining an `Actor` capable of receiving, processing, and transmitting messages /// Primary trait defining an `Actor` capable of receiving, processing, and transmitting messages
pub trait Actor: Send + Sized + Unpin + 'static { pub trait Actor: Send + Sized + 'static {
/// The type for messages received by this `Actor` /// The type for messages received by this `Actor`
type Message: Send; type Message: Send;
/// The type for responses given by this actor when called from `Handle::send(..)` /// The type for responses given by this actor when called from `Handle::send(..)`
@ -84,7 +111,7 @@ pub trait Actor: Send + Sized + Unpin + 'static {
Handle { Handle {
cancel_token, cancel_token,
_handle: handle, _handle: handle,
tx, tx: tx.into(),
_type: std::marker::PhantomData::<Self>, _type: std::marker::PhantomData::<Self>,
} }
} }