ready-active-safe
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.
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_eventnever 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, andtimeare feature-gated helpers, not a framework. - no_std core: core types work in
no_stdwithalloc. - Safety posture: no
unsafe, no panic/unwrap/expect in library code, strict CI, runnable docs.
Quick Start
Add this to your Cargo.toml:
[]
= "0.1"
Then define modes, events, and commands:
use *;
use Runner;
;
let system = System;
let decision = system.decide;
assert_eq!;
assert_eq!;
let decision = system.decide;
assert_eq!;
let mut runner = new;
let commands = runner.feed;
assert_eq!;
assert_eq!;
Feature Flags
| Feature | Default | Requires | Description |
|---|---|---|---|
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:
[]
= { = "0.1", = false }
To select specific features:
[]
= { = "0.1", = false, = ["std", "time"] }
API Reference
| Type | Module | Description |
|---|---|---|
Machine |
core | Pure function from (mode, event) to Decision |
Decision |
core | Outcome of processing an event: mode change + commands |
ModeChange |
core | A requested transition to a new mode |
Policy |
core | Determines whether a transition is allowed |
LifecycleError |
core | Transition failure errors |
| Free functions | core | stay(), transition(), ignore(), apply_decision() |
| Macros | core | assert_transitions_to!, assert_stays!, assert_emits! |
Clock |
time |
Trait: a source of monotonic time |
Instant |
time |
Nanosecond-resolution monotonic instant |
Deadline |
time |
An expiration point expressed as an Instant |
ManualClock |
time |
Deterministic clock for tests and simulation |
SystemClock |
time |
Monotonic wall clock (requires std) |
Runner |
runtime |
Owns the current mode and feeds events into a Machine |
TransitionRecord |
journal |
A record of one processed event |
InMemoryJournal |
journal |
In-memory journal with recording and replay |
ReplayError |
journal |
Errors from deterministic replay |
See the full API reference index 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
Policytrait - Support
no_stdenvironments (core types require onlyalloc) - Enable deterministic replay through pure
Machineimplementations
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:
basic— core API:Machine,Decision,stay(),transition(),emit()openxr_session— real-world domain mapping (XR session lifecycle)recovery—Policyenforcement andapply_checkedrunner—Runnerevent loop with policy denial handlingrecovery_cycle— repeated fault/recovery with external retry logicchannel_runtime— multi-threaded event feeding over channelsmetrics— observability wrapper aroundRunnerreplay— journal recording and deterministic replay
Documentation
- User guide — start here
- API reference — per-type documentation
- Cookbook (recipes) — copy-paste patterns
- API reference on docs.rs
- Design rationale
- Architecture overview
- Roadmap to v1.0
Contributing
Contributions are welcome. Please read CONTRIBUTING.md before submitting a pull request.
Security
To report a security vulnerability, see SECURITY.md.
License
Licensed under the Apache License, Version 2.0. See LICENSE for details.