01 · Problem
What is it for?
Async guards and overlapping events can corrupt a state machine if execution order is implicit.
02 · Mental model
How should you think about it?
A transition is a serialized operation: evaluate a guard, exit the old state, change state and enter the new state.
Serializes transitions, awaits guards and enters the correct target state. Actions may have external effects. Do not await recursively dispatched events from inside an active action; schedule follow-up events after it finishes.
03 · Control flow
What happens in which order?
- Queue an incoming event.
- Resolve the current-state transition.
- Await its guard.
- Run exit and transition actions.
- Commit the target and run entry actions.
- Notify observers.
04 · Building blocks
The important contracts
StateMachine / addTransition / transitionConfigure and serialize guarded state transitions.subscribe / getCurrentState / isInEndStateObserve with explicit unsubscribe and inspect state.05 · Build it yourself
Implement a small core
Start with this small independent implementation, then use the steps below to make its contract explicit.
- Queue an incoming event.
- Resolve the current-state transition.
- Await its guard.
- Run exit and transition actions.
- Commit the target and run entry actions.
- Notify observers.
type Transition<S, P> = {
target: S;
guard?: (payload: P) => boolean | Promise<boolean>;
};
class Machine<S extends string, E extends string, P> {
constructor(
private state: S,
private table: Record<S, Partial<Record<E, Transition<S, P>>>>,
) {}
async send(event: E, payload: P) {
const next = this.table[this.state][event];
if (!next || (next.guard && !(await next.guard(payload)))) return this.state;
this.state = next.target;
return this.state;
}
}06 · Verify
What should you test?
- An async false guard must prevent the transition.
- Verify exit, transition and entry action order.
- Serialize overlapping events and allow observers to unsubscribe.
Try it yourself
Turn the first verification claim into a failing test, implement the smallest fix, then add a boundary case.
An async false guard must prevent the transition.
Compare your approach
Your check should fail when the stated invariant is deliberately broken, and pass again after the fix. Keep setup deterministic and assert an observable result.
07 · Limits
Trade-offs and failure modes
- Actions can fail after external side effects.
- Recursive event dispatch needs a queue.
- Hierarchical machines need explicit event propagation rules.
Actions may have external effects. Do not await recursively dispatched events from inside an active action; schedule follow-up events after it finishes.
08 · Keep exploring
Source code and related ideas
yarn tsx packages/state-machine/examples/basic.ts