# ready-active-safe
[](https://crates.io/crates/ready-active-safe)
[](https://docs.rs/ready-active-safe)
[](https://github.com/stateruntime/ready-active-safe/actions/workflows/ci.yml)
[](LICENSE)
[](https://blog.rust-lang.org/)
Lifecycle engine for externally driven systems.
## The Idea
Most real systems do not have an "anything can go anywhere" state graph. They have a lifecycle.
They start up, run, and shut down. When things go wrong, they go safe and recover.
`ready-active-safe` gives you a small kernel for that shape of problem.
You write one pure function, `Machine::on_event`, and you get back a `Decision`.
The decision is plain data that says:
- move to a new mode, or stay
- emit zero or more commands for your runtime to execute
The machine never does I/O. Your runtime stays yours.
This crate is not a general purpose state machine framework. It is a lifecycle engine.
## How It Works
A `Machine` implementation is a pure function from `(mode, event)` to `Decision`.
The `Decision` contains an optional `ModeChange` (transition to a new mode) and a list of
`Command` values for the runtime to execute. The runtime owns the current mode, feeds events,
checks the `Policy`, and dispatches commands. The machine itself never performs side effects.
```text
Event --> Machine::on_event(mode, event) --> Decision
|-- ModeChange (optional)
+-- Commands (Vec<C>)
```
## Why This Crate
Lifecycle logic is often tangled with I/O, threading, and error handling,
making it hard to test and reason about. This crate separates the two concerns:
- **Pure logic boundary:** `Machine::on_event` never performs I/O or side effects. It returns data. You can test every transition with a simple assertion.
- **Decisions as data:** mode changes and commands are plain values your runtime executes — not callbacks.
- **Batteries are optional:** `runtime::Runner`, `journal`, and `time` are feature-gated helpers, not a framework.
- **no_std core:** core types work in `no_std` with `alloc`.
- **Safety posture:** no `unsafe`, no panic/unwrap/expect in library code, strict CI, runnable docs.
## Quick Start
Add this to your `Cargo.toml`:
```toml
[dependencies]
ready-active-safe = "0.1"
```
Then define modes, events, and commands:
```rust
use ready_active_safe::prelude::*;
use ready_active_safe::runtime::Runner;
#[derive(Debug, Clone, PartialEq, Eq)]
enum Mode {
Ready,
Active,
Safe,
}
#[derive(Debug)]
enum Event {
Start,
Stop,
Fault,
}
#[derive(Debug, Clone, PartialEq, Eq)]
enum Command {
Initialize,
BeginProcessing,
Shutdown,
}
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> {
use Command::*;
use Event::*;
use Mode::*;
match (mode, event) {
(Ready, Start) => transition(Active)
.emit_all([Initialize, BeginProcessing]),
(Active, Stop | Fault) => transition(Safe)
.emit(Shutdown),
_ => stay(),
}
}
}
let system = System;
let decision = system.decide(&Mode::Ready, &Event::Start);
assert_eq!(decision.target_mode(), Some(&Mode::Active));
assert_eq!(decision.commands(), &[Command::Initialize, Command::BeginProcessing]);
let decision = system.decide(&Mode::Active, &Event::Fault);
assert_eq!(decision.target_mode(), Some(&Mode::Safe));
let mut runner = Runner::new(&system);
let commands = runner.feed(&Event::Start);
assert_eq!(runner.mode(), &Mode::Active);
assert_eq!(commands, vec![Command::Initialize, Command::BeginProcessing]);
```
## Feature Flags
| `full` | Yes | none | Enables all features below |
| `std` | No* | none | Standard library support |
| `runtime` | No* | `std` | Event loop and command dispatch |
| `time` | No* | none | Clock, instant, and deadline types |
| `journal` | No* | `std` | Transition recording and replay |
*Enabled by default through the `full` feature.
To use in a `no_std` environment:
```toml
[dependencies]
ready-active-safe = { version = "0.1", default-features = false }
```
To select specific features:
```toml
[dependencies]
ready-active-safe = { version = "0.1", default-features = false, features = ["std", "time"] }
```
## API Reference
| [`Machine`](docs/api/machine.md) | core | Pure function from (mode, event) to `Decision` |
| [`Decision`](docs/api/decision.md) | core | Outcome of processing an event: mode change + commands |
| [`ModeChange`](docs/api/mode_change.md) | core | A requested transition to a new mode |
| [`Policy`](docs/api/policy.md) | core | Determines whether a transition is allowed |
| [`LifecycleError`](docs/api/lifecycle_error.md) | core | Transition failure errors |
| [Free functions](docs/api/functions.md) | core | `stay()`, `transition()`, `ignore()`, `apply_decision()` |
| [Macros](docs/api/macros.md) | core | `assert_transitions_to!`, `assert_stays!`, `assert_emits!` |
| [`Clock`](docs/api/clock.md) | `time` | Trait: a source of monotonic time |
| [`Instant`](docs/api/instant.md) | `time` | Nanosecond-resolution monotonic instant |
| [`Deadline`](docs/api/deadline.md) | `time` | An expiration point expressed as an `Instant` |
| [`ManualClock`](docs/api/manual_clock.md) | `time` | Deterministic clock for tests and simulation |
| [`SystemClock`](docs/api/system_clock.md) | `time` | Monotonic wall clock (requires `std`) |
| [`Runner`](docs/api/runner.md) | `runtime` | Owns the current mode and feeds events into a `Machine` |
| [`TransitionRecord`](docs/api/transition_record.md) | `journal` | A record of one processed event |
| [`InMemoryJournal`](docs/api/in_memory_journal.md) | `journal` | In-memory journal with recording and replay |
| [`ReplayError`](docs/api/replay_error.md) | `journal` | Errors from deterministic replay |
See the full [API reference index](docs/api/README.md) for categorized documentation.
## Use Cases
This pattern fits best when your system is driven by external events, and when recovery matters.
Common examples include:
- OpenXR session lifecycles
- embedded controllers and robotics nodes
- services that need explicit start and stop phases
- instruments that must fail safe and recover
## What It Does / Does Not Do
**Does:**
- Model system lifecycles as typed modes with pure transition logic
- Return decisions as data: commands, mode changes, or both
- Enforce transition policies externally via the `Policy` trait
- Support `no_std` environments (core types require only `alloc`)
- Enable deterministic replay through pure `Machine` implementations
**Does not:**
- Execute side effects. That is the runtime's job.
- Manage threads, async runtimes, or I/O
- Prescribe a fixed set of modes. You define your own.
- Require a specific serialization format or persistence layer
## Examples
Run any example with `cargo run --example <name>`. Suggested reading order:
1. **`basic`** — core API: `Machine`, `Decision`, `stay()`, `transition()`, `emit()`
2. **`openxr_session`** — real-world domain mapping (XR session lifecycle)
3. **`recovery`** — `Policy` enforcement and `apply_checked`
4. **`runner`** — `Runner` event loop with policy denial handling
5. **`recovery_cycle`** — repeated fault/recovery with external retry logic
6. **`channel_runtime`** — multi-threaded event feeding over channels
7. **`metrics`** — observability wrapper around `Runner`
8. **`replay`** — journal recording and deterministic replay
## Documentation
- [User guide](docs/USER_GUIDE.md) — start here
- [API reference](docs/api/README.md) — per-type documentation
- [Cookbook (recipes)](docs/COOKBOOK.md) — copy-paste patterns
- [API reference on docs.rs](https://docs.rs/ready-active-safe)
- [Design rationale](docs/DESIGN.md)
- [Architecture overview](docs/ARCHITECTURE.md)
- [Roadmap to v1.0](docs/ROADMAP.md)
## Contributing
Contributions are welcome. Please read [CONTRIBUTING.md](CONTRIBUTING.md) before submitting a pull request.
## Security
To report a security vulnerability, see [SECURITY.md](SECURITY.md).
## License
Licensed under the Apache License, Version 2.0. See [LICENSE](LICENSE) for details.