Reference

Cheatsheet

Quick reference for common patterns. Copy, paste, and adapt.

Basic Actor Setup

use acton_reactive::prelude::*;

#[acton_actor]
struct MyActor {
    // your state here
}

#[acton_message]
struct MyMessage;

#[acton_main]
async fn main() {
    let mut runtime = ActonApp::launch_async().await;

    let mut builder = runtime.new_actor::<MyActor>();
    builder.mutate_on::<MyMessage>(|actor, _envelope| {
        // handle message
        Reply::ready()
    });

    let handle = builder.start().await;

    handle.send(MyMessage).await;
    runtime.shutdown_all().await.ok();
}

Message Patterns

Fire-and-Forget

handle.send(DoSomething).await;

Request-Response (Reply Envelope)

// Server actor
builder.act_on::<GetValue>(|actor, envelope| {
    let value = actor.model.value;
    let reply = envelope.reply_envelope();
    Reply::pending(async move {
        reply.send(ValueResponse(value)).await;
    })
});

// Client actor receives response
client.mutate_on::<ValueResponse>(|actor, envelope| {
    let value = envelope.message().0;
    println!("Got: {}", value);
    Reply::ready()
});

Broadcast

// Publisher
let broker = runtime.broker();
broker.broadcast(Event { data: "hello".into() }).await;

// Subscriber (before starting)
builder.mutate_on::<Event>(|actor, envelope| {
    println!("Got: {}", envelope.message().data);
    Reply::ready()
});
builder.handle().subscribe::<Event>().await;
let handle = builder.start().await;

// Stop receiving Event broadcasts (automatic on actor stop);
// use unsubscribe_async::<Event>().await to await delivery to the broker
handle.unsubscribe::<Event>();

Handler Patterns

Mutate State

builder.mutate_on::<Increment>(|actor, _envelope| {
    actor.model.count += 1;
    Reply::ready()
});

Mutate State (Sync — No Future Allocation)

builder.mutate_on_sync::<Increment>(|actor, _envelope| {
    actor.model.count += 1;
});

Read State with Reply

builder.act_on::<GetCount>(|actor, envelope| {
    let count = actor.model.count;
    let reply = envelope.reply_envelope();
    Reply::pending(async move {
        reply.send(CountResponse(count)).await;
    })
});

Async Handler

builder.act_on::<Compute>(|actor, envelope| {
    let input = envelope.message().input;
    let reply = envelope.reply_envelope();
    Reply::pending(async move {
        let result = compute(input).await;
        reply.send(ComputeResponse { result }).await;
    })
});

Handler futures must be Send + Sync

The async block you pass to Reply::pending must be Send + **Sync**. That's stricter than the usual Send, and it rules out plenty of third-party futures (many HTTP and database clients). If the compiler says "future created by async block is not Sync", use the spawn-and-report-back pattern below.

Async Handler (Non-Sync Future — HTTP, DB, …)

Spawn the work with tokio::spawn (which only needs Send) and message the result back:

builder.mutate_on::<FetchData>(|actor, envelope| {
    let handle = actor.handle().clone();
    let url = envelope.message().url.clone();

    tokio::spawn(async move {
        let resp = reqwest::get(&url).await.unwrap();
        let body = resp.text().await.unwrap();
        handle.send(FetchResponse { body }).await;
    });

    Reply::ready()
});

// Then handle the result as a normal message
builder.mutate_on::<FetchResponse>(|actor, envelope| {
    actor.model.last_body = envelope.message().body.clone();
    Reply::ready()
});

Read-Only Handler (Sync — No Future Allocation)

builder.act_on_sync::<LogCount>(|actor, _envelope| {
    tracing::info!("count = {}", actor.model.count);
});

Fallible Handlers

Return a Result from a handler and handle the error separately.

builder
    .try_mutate_on::<Withdraw, Receipt, BankError>(|actor, ctx| {
        let amount = ctx.message().amount;
        let balance = actor.model.balance;
        Reply::try_pending(async move {
            if balance < amount {
                Err(BankError::InsufficientFunds { balance, amount })
            } else {
                Ok(Receipt { remaining: balance - amount })
            }
        })
    })
    .on_error::<Withdraw, BankError>(|actor, ctx, err| {
        tracing::error!("Withdrawal failed: {}", err);
        Reply::ready()
    });

Immediate results skip the async block entirely:

builder.try_act_on::<GetBalance, Balance, BankError>(|actor, _ctx| {
    Reply::try_ok(Balance(actor.model.balance))
});

Child Actors

Create and Supervise Child

Build the child from the runtime, give it a parent reference, then hand it to supervise():

// Start the parent
let parent = runtime.new_actor::<ParentState>();
let parent_handle = parent.start().await;

// Build the child with the parent in its config — this is what enables
// ChildTerminated notifications back to the parent.
let config = ActorConfig::new(
    Ern::with_root("worker")?,
    Some(parent_handle.clone()),
    None,
)?;
let mut child = runtime.new_actor_with_config::<WorkerState>(config);
child.mutate_on::<Task>(handle_task);

// supervise() starts the child and registers it under the parent
let child_handle = parent_handle.supervise(child).await?;

create_child() is Idle-only

create_child() exists only on a builder (ManagedActor<Idle, _>), not on a running actor — so you can't call it from inside a handler, and it returns an actor with the same state type as its parent. To spawn a differently-typed worker, use runtime.new_actor_with_config() as above.

Access Children

children() lives on the handle, not on the actor:

for child in actor.handle().children().iter() {
    child.value().send(Ping).await;
}

Lifecycle Hooks

Before Start

builder.before_start(|actor| async move {
    println!("Actor starting!");
});

After Stop

builder.after_stop(|actor| async move {
    println!("Actor stopped");
});

Common State Patterns

With Default

#[acton_actor]
struct Counter {
    count: i32,  // defaults to 0
}

With Custom Default

Use no_default to stop the macro deriving Default, then write your own. (Adding #[derive(Default)] and a manual impl Default collides — you'd get "conflicting implementations".)

#[acton_actor(no_default)]
struct Config {
    timeout: Duration,
}

impl Default for Config {
    fn default() -> Self {
        Self { timeout: Duration::from_secs(30) }
    }
}

no_default is also how you hold a field whose type has no Default (a Stdout, a client handle, and so on).

With External Resources

#[acton_actor]
struct DbActor {
    pool: Option<PgPool>,
}

// Initialize via message or before_start

Error Handling

In Handlers

builder.mutate_on::<RiskyOp>(|actor, envelope| {
    match do_risky_thing() {
        Ok(result) => {
            actor.model.data = result;
            Reply::ready()
        }
        Err(e) => {
            tracing::error!("Failed: {}", e);
            Reply::ready()  // Actor continues
        }
    }
});

Signal Errors to Other Actors

builder.mutate_on::<Query>(|actor, envelope| {
    let reply = envelope.reply_envelope();
    match do_query() {
        Ok(data) => Reply::pending(async move {
            reply.send(QuerySuccess(data)).await;
        }),
        Err(e) => Reply::pending(async move {
            reply.send(QueryFailed(e.to_string())).await;
        }),
    }
});

Testing

Basic Test

#[tokio::test]
async fn test_actor() {
    let mut runtime = ActonApp::launch_async().await;

    let mut counter = runtime.new_actor::<Counter>();
    counter
        .mutate_on::<Increment>(|actor, _env| {
            actor.model.count += 1;
            Reply::ready()
        });

    let handle = counter.start().await;
    handle.send(Increment).await;

    // Use probe actor or atomic counter to verify
    tokio::time::sleep(tokio::time::Duration::from_millis(50)).await;

    runtime.shutdown_all().await.ok();
}

IPC

Expose an Actor (Server)

let mut runtime = ActonApp::launch_async().await;

// Register the message type so it can be deserialized from the wire
runtime.ipc_registry().register::<GetPrice>("GetPrice");

let mut service = runtime.new_actor_with_name::<PriceService>("prices".to_string());
service
    .act_on::<GetPrice>(|actor, ctx| { /* ... */ })
    .expose_for_ipc();          // reachable as "prices"
let handle = service.start().await;

let listener = runtime.start_ipc_listener().await?;

Connect and Send (Client)

IpcClient is the channel-based client — connect once, then send, request, or subscribe:

use acton_reactive::ipc::{IpcClient, IpcConfig, IpcEnvelope};

let client = IpcClient::connect(IpcConfig::load().socket_path()).await?;

// Request-response — new_request() sets expects_reply and generates a correlation ID
let response = client.request(IpcEnvelope::new_request(
    "prices",
    "GetPrice",
    serde_json::json!({ "symbol": "ACTON" }),
)).await?;

if response.success {
    println!("{:?}", response.payload);
}

// Fire-and-forget — IpcEnvelope::new() instead
client.send(IpcEnvelope::new(
    "prices",
    "RefreshCache",
    serde_json::json!({}),
)).await?;

client.disconnect().await?;

Subscribe to Broadcasts (Client)

client.subscribe(vec!["PriceUpdate".to_string()]).await?;
let mut pushes = client.take_push_receiver().expect("already taken");

while let Some(note) = pushes.recv().await {
    println!("push: {:?}", note.payload);
}

See IPC Setup and IPC Patterns for the full picture.


Quick Imports

// Everything you need
use acton_reactive::prelude::*;

// For IPC clients
use acton_reactive::ipc::{IpcClient, IpcConfig, IpcEnvelope};

// Only if you're speaking the wire protocol by hand
use acton_reactive::ipc::protocol::{write_envelope, read_response};

Common Mistakes

WrongRight
ActonApp::launch() from an async context (panics)ActonApp::launch_async().await. launch() is fine from fn main().
ctx.messagectx.message() — it's an accessor, not a field
actor.children()actor.handle().children()
actor.create_child(..) inside a handlercreate_child is Idle-only; build from the runtime and supervise()
#[acton_actor] + #[derive(Default)] + manual impl Default#[acton_actor(no_default)] + manual impl Default
runtime.new_actor::<T>().mutate_on::<M>(h).start()Configure via &mut first, then builder.start().await
handle.ask(msg)Use reply envelope pattern
Reply::with(value)Use Reply::pending + reply envelope
Forgetting .await on start()builder.start().await
Mutating in act_onUse mutate_on for state changes
Expecting a panic to kill an actorPanics are caught by default; use try_mutate_on + on_error
Previous
Examples