DeutschGitHub
prototypepackages/state-machine/src

State Machine

@ralphschuler/state-machine

Serializes transitions, awaits guards and enters the correct target state.

FSMguardstransitions

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.

In this repository

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?

  1. Queue an incoming event.
  2. Resolve the current-state transition.
  3. Await its guard.
  4. Run exit and transition actions.
  5. Commit the target and run entry actions.
  6. 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.

  1. Queue an incoming event.
  2. Resolve the current-state transition.
  3. Await its guard.
  4. Run exit and transition actions.
  5. Commit the target and run entry actions.
  6. Notify observers.
Concept sketch · may reference omitted helpers
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.
Important

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