Seesaw
A Redux-style state machine with TypeId-based multi-event dispatch.
Named after the playground equipment that balances back and forth — representing the back-and-forth nature of state transitions.
Guarantees
- Serial reduction: Reducers are executed serially under a write lock. No two events are ever reduced concurrently, even when emitted from multiple tasks.
- Multi-event dispatch: Support for multiple event types via TypeId routing.
- Effect system: Register handlers that react to events and can emit new events, spawn tracked tasks, and access shared dependencies.
- Per-event reducers: Register reducers for specific event types.
Example
use seesaw::{Engine, on, fold};
#[derive(Clone, Debug, Default)]
struct AppState {
user_count: i32,
order_count: i32,
}
// Define event types (struct-per-event pattern)
#[derive(Clone)]
struct UserCreated { name: String }
#[derive(Clone)]
struct OrderPlaced { amount: f64 }
#[derive(Clone)]
struct UserWelcomed { name: String }
// Create engine with fold (reducers) and on (effects)
let engine = Engine::new()
// Fold events into state
.with_reducer(fold::<UserCreated>().into(|state, _| AppState {
user_count: state.user_count + 1,
..state
}))
.with_reducer(fold::<OrderPlaced>().into(|state, _| AppState {
order_count: state.order_count + 1,
..state
}))
// On event, then return next event
.with_effect(on::<UserCreated>().then(|event, _ctx| async move {
println!("User created: {}", event.name);
Ok(UserWelcomed { name: event.name.clone() })
}));
// Activate and dispatch events via run()
let handle = engine.activate(AppState::default());
handle.run(|_| Ok(UserCreated { name: "Alice".into() }))?;
handle.run(|_| Ok(OrderPlaced { amount: 99.99 }))?;
handle.settled().await?;