ready-active-safe 0.1.0

Lifecycle engine for externally driven systems
Documentation
  • Coverage
  • 100%
    97 out of 97 items documented19 out of 62 items with examples
  • Size
  • Source code size: 296.1 kB This is the summed size of all the files inside the crates.io package for this release.
  • Documentation size: 9.2 MB This is the summed size of all files generated by rustdoc for all configured targets
  • Ø build duration
  • this release: 24s Average build duration of successful builds.
  • all releases: 24s Average build duration of successful builds in releases after 2024-10-23.
  • Links
  • Homepage
  • Repository
  • crates.io
  • Dependencies
  • Versions
  • Owners
  • babilonczyk

ready-active-safe

Crates.io Documentation CI License MSRV

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_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:

[dependencies]
ready-active-safe = "0.1"

Then define modes, events, and commands:

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

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:

[dependencies]
ready-active-safe = { version = "0.1", default-features = false }

To select specific features:

[dependencies]
ready-active-safe = { version = "0.1", default-features = false, features = ["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 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. recoveryPolicy enforcement and apply_checked
  4. runnerRunner 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

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.