| Crates.io | fasm |
| lib.rs | fasm |
| version | 0.3.0 |
| created_at | 2026-01-08 14:07:23.24622+00 |
| updated_at | 2026-01-08 14:07:23.24622+00 |
| description | Fallible Async State Machines |
| homepage | |
| repository | https://github.com/zk2u/fasm |
| max_upload_size | |
| id | 2030282 |
| size | 128,242 |
A Rust framework for building deterministic, testable, and crash-recoverable state machines with async operations and fallible state access.
Traditional state machines break down in productionβrace conditions, crashes mid-operation, and bugs that only appear under load. FASM solves this by making correctness verifiable:
use fasm::{Input, StateMachine, actions::{Action, ActionsContainer, TrackedAction, TrackedActionTypes}};
struct PaymentSystem {
balance: u64,
pending: HashMap<u64, Payment>,
next_id: u64,
}
struct PaymentTracked;
impl TrackedActionTypes for PaymentTracked {
type Id = u64;
type Action = PaymentRequest;
type Result = PaymentResult;
}
impl StateMachine for PaymentSystem {
type State = Self;
type Input = PaymentInput;
type TrackedAction = PaymentTracked;
type UntrackedAction = Notification;
type Actions = Vec<Action<Self::UntrackedAction, Self::TrackedAction>>;
type TransitionError = PaymentError;
type RestoreError = ();
async fn stf<'s, 'a>(
state: &'s mut Self::State,
input: Input<Self::TrackedAction, Self::Input>,
actions: &'a mut Self::Actions,
) -> Result<(), Self::TransitionError> {
match input {
Input::Normal(PaymentInput::Process { amount, user }) => {
// 1. Validate
if state.balance < amount {
return Err(PaymentError::InsufficientFunds);
}
// 2. Prepare (no mutation yet)
let id = state.next_id;
// 3. Fallible operations first
actions.add(Action::Tracked(TrackedAction::new(
id,
PaymentRequest::Charge { amount },
)))?;
// 4. Mutate state (point of no return)
state.next_id += 1;
state.pending.insert(id, Payment { amount, user, status: Pending });
Ok(())
}
Input::TrackedActionCompleted { id, result } => {
let payment = state.pending.get_mut(&id)
.ok_or(PaymentError::NotFound)?;
match result {
PaymentResult::Success => {
state.balance -= payment.amount;
payment.status = Confirmed;
}
PaymentResult::Failed { reason } => {
payment.status = Failed;
}
}
Ok(())
}
}
}
async fn restore<'s, 'a>(
state: &'s Self::State,
actions: &'a mut Self::Actions,
) -> Result<(), Self::RestoreError> {
for (&id, payment) in &state.pending {
if payment.status == Pending {
actions.add(Action::Tracked(TrackedAction::new(
id,
PaymentRequest::CheckStatus { id },
))).map_err(|_| ())?;
}
}
Ok(())
}
}
Input (user request, external data)
β
βΌ
βββββββββββββββββββββββββββββββββββββββ
β State Transition Function (STF) β
β βββββββββββββββββββββββββββββββββ β
β β’ Validates inputs β
β β’ Mutates state atomically β
β β’ Emits action descriptions β
βββββββββββββββββββββββββββββββββββββββ
β
ββββΊ State committed
β
ββββΊ Tracked Actions βββΊ External Systems βββΊ Results feed back as Input
β
ββββΊ Untracked Actions (fire-and-forget)
After crash: restore(state) β re-emit pending tracked actions
A deterministic function: (State, Input) β (State', Actions)
Err, state is unchangedTracked Actions: Results feed back into the STF
Untracked Actions: Fire-and-forget
After a crash, restore() rebuilds pending tracked actions from state:
If state is a database transaction, atomicity is automatic:
async fn stf(txn: &mut DbTransaction, input: Input, actions: &mut Actions) -> Result<()> {
let user = txn.get("user:123").await?;
txn.set("balance", new_balance).await?;
actions.add(Action::Tracked(...))?;
Ok(())
// If any operation fails, entire transaction aborts
}
For in-memory state, order operations carefully:
async fn stf(state: &mut State, input: Input, actions: &mut Actions) -> Result<()> {
// 1. Validate (can fail)
if state.balance < amount {
return Err(InsufficientFunds);
}
// 2. Prepare values (no mutation)
let id = state.next_id;
// 3. Fallible operations first
actions.add(Action::Tracked(...))?;
// 4. Mutate state last
state.next_id += 1;
state.pending.insert(id, ...);
Ok(())
}
rand::random(), no unseeded RNGsstate parameterDeterministic simulation testingβthe killer feature:
#[test]
async fn test_correctness() {
let mut rng = ChaCha8Rng::seed_from_u64(12345);
let mut state = MySystem::new();
let mut actions = Vec::new();
for i in 0..100_000 {
let input = generate_random_input(&mut rng);
let _ = MySystem::stf(&mut state, input, &mut actions).await;
actions.clear();
state.check_invariants()
.expect(&format!("Invariant violated at iteration {}", i));
}
// Same seed = same execution = reproducible bugs
}
# Simple counter
cargo run --example csm
# Coffee shop loyalty app with tracked/untracked actions
cargo run --example coffee_shop
# Full booking system with simulation tests
cargo test --package dentist_booking
async fn directly (Rust 2024 edition)Future implementations requiredInput::TrackedActionCompleted { id, result } (was res)MIT OR Apache-2.0