# Cookbook
Opinionated, copy/paste recipes for wiring `ready-active-safe` into real runtimes.
All code blocks in this document are intended to compile as-is with the default
feature set (`full`).
---
## Runner + Dispatch (Basic Event Loop)
The simplest pattern: `Runner` maintains the current mode, processes events,
and yields commands for your code to execute.
```rust
use ready_active_safe::prelude::*;
use ready_active_safe::runtime::Runner;
#[derive(Debug, Clone, PartialEq, Eq)]
enum Mode {
Ready,
Active,
}
#[derive(Debug)]
enum Event {
Start,
}
#[derive(Debug, Clone, PartialEq, Eq)]
enum Command {
Init,
}
struct System;
impl Machine for System {
type Mode = Mode;
type Event = Event;
type Command = Command;
fn initial_mode(&self) -> Mode {
Mode::Ready
}
fn on_event(&self, mode: &Mode, event: &Event) -> Decision<Mode, Command> {
match (mode, event) {
(Mode::Ready, Event::Start) => transition(Mode::Active).emit(Command::Init),
_ => ignore(),
}
}
}
let system = System;
let mut runner = Runner::new(&system);
let mut dispatched: Vec<Command> = Vec::new();
assert_eq!(runner.mode(), &Mode::Active);
assert_eq!(dispatched, vec![Command::Init]);
```
---
## Runner + Policy (Denial Handling)
Use `Runner::feed_checked` when the runtime must enforce allowed transitions.
On denial, the runner **does not change the mode** and you get a
`LifecycleError::TransitionDenied`.
```rust
use ready_active_safe::prelude::*;
use ready_active_safe::runtime::Runner;
#[derive(Debug, Clone, PartialEq, Eq)]
enum Mode {
Ready,
Active,
}
#[derive(Debug)]
enum Event {
Start,
Reset,
}
struct System;
impl Machine for System {
type Mode = Mode;
type Event = Event;
type Command = ();
fn initial_mode(&self) -> Mode {
Mode::Ready
}
fn on_event(&self, mode: &Mode, event: &Event) -> Decision<Mode, ()> {
match (mode, event) {
(Mode::Ready, Event::Start) => transition(Mode::Active),
(Mode::Active, Event::Reset) => transition(Mode::Ready),
_ => ignore(),
}
}
}
struct ForwardOnly;
impl Policy<Mode> for ForwardOnly {
fn is_allowed(&self, from: &Mode, to: &Mode) -> bool {
matches!((from, to), (Mode::Ready, Mode::Active))
}
}
let system = System;
let policy = ForwardOnly;
let mut runner = Runner::new(&system);
runner.feed_checked(&Event::Start, &policy).unwrap();
assert_eq!(runner.mode(), &Mode::Active);
let result = runner.feed_checked(&Event::Reset, &policy);
match result {
Err(LifecycleError::TransitionDenied { from, to, reason }) => {
assert_eq!(from, Mode::Active);
assert_eq!(to, Mode::Ready);
assert_eq!(reason, "transition denied by policy");
}
_ => panic!("expected TransitionDenied error"),
}
// Denied transitions do not update the runner's mode.
assert_eq!(runner.mode(), &Mode::Active);
```
---
## Timeouts as Events + Journal Replay
Real systems have timeouts. This recipe stores deadlines in the mode and
checks them on each tick, then records and replays the full history.
Time enters the machine as event data — the runtime owns the clock.
```rust
use core::time::Duration;
use ready_active_safe::prelude::*;
use ready_active_safe::runtime::Runner;
use ready_active_safe::time::{Deadline, Instant, ManualClock};
use ready_active_safe::journal::InMemoryJournal;
#[derive(Debug, Clone, PartialEq, Eq)]
enum Mode {
Ready,
Active { deadline: Deadline },
Safe,
}
#[derive(Debug, Clone)]
enum Event {
Start { now: Instant },
Tick { now: Instant },
}
#[derive(Debug, Clone, PartialEq, Eq)]
enum Command {
Shutdown,
}
struct System {
timeout: Duration,
}
impl Machine for System {
type Mode = Mode;
type Event = Event;
type Command = Command;
fn initial_mode(&self) -> Mode {
Mode::Ready
}
fn on_event(&self, mode: &Mode, event: &Event) -> Decision<Mode, Command> {
match (mode, event) {
(Mode::Ready, Event::Start { now }) => {
let at = now
.checked_add(self.timeout)
.unwrap_or(Instant::from_nanos(u64::MAX));
transition(Mode::Active {
deadline: Deadline::at(at),
})
}
(Mode::Active { deadline }, Event::Tick { now })
if deadline.is_expired(*now) =>
{
transition(Mode::Safe).emit(Command::Shutdown)
}
_ => ignore(),
}
}
}
let clock = ManualClock::new(Instant::from_nanos(0));
let system = System {
timeout: Duration::from_nanos(5),
};
let mut runner = Runner::new(&system);
let mut journal: InMemoryJournal<Mode, Event, Command> = InMemoryJournal::new();
let start = Event::Start { now: clock.now() };
let from = runner.mode().clone();
let commands = runner.feed(&start);
let to = runner.mode().clone();
journal.record_step(&from, &to, &start, &commands);
clock.advance(Duration::from_nanos(10));
let tick = Event::Tick { now: clock.now() };
let from = runner.mode().clone();
let commands = runner.feed(&tick);
let to = runner.mode().clone();
journal.record_step(&from, &to, &tick, &commands);
assert_eq!(runner.mode(), &Mode::Safe);
assert_eq!(journal.len(), 2);
let replay_mode = journal.replay(&system, system.initial_mode()).unwrap();
assert_eq!(replay_mode, runner.mode().clone());
```
---
## Async Integration (tokio / async-std)
To use `ready-active-safe` with an async runtime, run the `Runner` inside a
task and feed events through an async channel. The pattern works with any
executor — this example uses tokio.
This recipe won't compile without a tokio dependency, which this crate
intentionally avoids.
```rust,ignore
use tokio::sync::mpsc;
// Same Mode, Event, Command, and Machine impl as before.
async fn run_lifecycle(
machine: &System,
mut rx: mpsc::Receiver<Event>,
) {
let mut runner = Runner::new(machine);
while let Some(event) = rx.recv().await {
runner.feed_and_dispatch(&event, |cmd| {
// Dispatch commands — log, send to another channel, etc.
println!("dispatch: {cmd:?}");
});
if *runner.mode() == Mode::Safe {
break;
}
}
}
#[tokio::main]
async fn main() {
let system = System;
let (tx, rx) = mpsc::channel(32);
let handle = tokio::spawn(async move {
run_lifecycle(&system, rx).await;
});
// Send events from other tasks.
tx.send(Event::Start).await.unwrap();
tx.send(Event::Fault).await.unwrap();
// Drop the sender to signal shutdown.
drop(tx);
handle.await.unwrap();
}
```
Key points:
- `Runner` lives inside **one** task — no `Arc<Mutex<>>` needed.
- Events arrive over a `tokio::sync::mpsc` channel.
- Graceful shutdown: drop the sender or send a `Shutdown` event.
- For CPU-bound machines, use `tokio::task::spawn_blocking`.
---
## Metrics and Observability
To emit metrics, wrap the runner's feed loop with counters or structured logging.
The machine stays pure — observability belongs at the runtime layer.
See `examples/metrics.rs` for a full runnable version with per-mode timing.
```rust
use std::collections::HashMap;
use ready_active_safe::prelude::*;
use ready_active_safe::runtime::Runner;
#[derive(Debug, Clone, PartialEq, Eq, Hash)]
enum Mode { Ready, Active, Safe }
#[derive(Debug)]
enum Event { Start, Fault }
#[derive(Debug)]
enum Command { Init, Stop }
struct System;
impl Machine for System {
type Mode = Mode;
type Event = Event;
type Command = Command;
fn initial_mode(&self) -> Mode { Mode::Ready }
fn on_event(&self, mode: &Mode, event: &Event) -> Decision<Mode, Command> {
match (mode, event) {
(Mode::Ready, Event::Start) => transition(Mode::Active).emit(Command::Init),
(Mode::Active, Event::Fault) => transition(Mode::Safe).emit(Command::Stop),
_ => ignore(),
}
}
}
let system = System;
let mut runner = Runner::new(&system);
let mut transition_count: u64 = 0;
let mut mode_entries: HashMap<Mode, u64> = HashMap::new();
*mode_entries.entry(runner.mode().clone()).or_insert(0) += 1;
let events = [Event::Start, Event::Fault];
for event in &events {
let before = runner.mode().clone();
let commands = runner.feed(event);
if *runner.mode() != before {
transition_count += 1;
*mode_entries.entry(runner.mode().clone()).or_insert(0) += 1;
// In production, emit a structured log or metric here:
// tracing::info!(from = ?before, to = ?runner.mode(), "transition");
}
// commands.len() is your "commands dispatched" counter.
let _ = commands;
}
assert_eq!(transition_count, 2);
assert_eq!(mode_entries[&Mode::Ready], 1);
assert_eq!(mode_entries[&Mode::Active], 1);
assert_eq!(mode_entries[&Mode::Safe], 1);
```