API Reference
Everything below is exported from the package root:
import {
FsmProcess, FsmState,
startProcess, KEY_DISPATCH, KEY_TERMINATE, KEY_STATES, KEY_EVENT,
getStateTransitions, isStateTransitionEnabled,
setProcessTracer, setStateTracer, setProcessPrinter,
} from "@statewalker/fsm";
There are two entry points: the low-level FsmProcess engine and the higher-level startProcess runner built on top of it. Types are exported alongside their values.
Configuration
FsmStateConfig
The declarative shape you pass to FsmProcess or startProcess. One plain object describes an entire hierarchical machine, so it stays serializable and inspectable.
type FsmStateConfig = {
key: string;
transitions?: [from: string, event: string, to: string][];
states?: FsmStateConfig[];
} & Record<string, unknown>;
key— the state’s name, unique among its siblings.transitions—[from, event, to]triples between this state’s direct children.""is the initial (from) or final (to) pseudo-state;"*"is a wildcard state or event.states— nested child configs. Entering a state descends into its initial child.
The index signature lets you attach arbitrary metadata (a description, a label, anything). The engine ignores extra fields; they travel with the config for your own tooling. Named constants for the sentinels are exported too: STATE_INITIAL / STATE_FINAL (""), STATE_ANY / EVENT_ANY ("*"), EVENT_EMPTY ("").
Transition resolution. For a (state, event) pair the engine tries the most specific rule first, then falls back through the wildcards: (state, event) → (*, event) → (state, *) → (*, *). If nothing matches, the state exits to its parent (as if the target were ""). Within a running machine, an event unhandled by the current leaf bubbles up the parent chain.
FsmProcess
The running machine — owner of the active-state stack and the traversal.
const process = new FsmProcess(config);
Properties
state?: FsmState— the current leaf state (undefined before start / after finish).event?: string— the event of the last dispatch.config: FsmStateConfig— the config it was built from.
Methods
dispatch(event: string): Promise<boolean>— feed an event to the machine and run the enter/exit cycle until it rests on a leaf. Dispatch""once to enter the initial state. If a dispatch is already running (e.g. a handler dispatches synchronously), the event is queued and applied when the current run settles — runs never nest. Returnsfalseonce the machine has finished,trueotherwise.shutdown(event?: string): Promise<void>— force-exit the whole stack (root last), running every state’sonExit. Ends the machine.dump(...args): Promise<FsmProcessDump>— snapshot the machine to{ status, event, stack }, wherestackis root→leaf and each entry carries whatever itsdumphooks recorded. See Serialization.restore(dump, ...args): Promise<this>— rebuild the machine from a dump, recreating each state (firingonStateCreate) and replaying itsrestorehooks. Restore into a fresh, not-yet-started process.
Callbacks
Each registration returns a disposer that unregisters it.
onStateCreate(handler: (state: FsmState) => void | Promise<void>)— the primary extension point. Fires once for every state the machine creates; attach that state’sonEnter/onExithooks here.onStateError(handler: (state: FsmState, error: unknown) => void | Promise<void>)— fires whenever a state handler throws.
FsmState
One live node in the active-state stack. Created by the engine on entry, discarded on exit, so handlers can capture per-activation closures instead of sharing machine-wide state.
Properties
key: string— this state’s key.parent?: FsmState— the enclosing state, or undefined for the root.process: FsmProcess— the owning machine.
Callbacks
Each returns a disposer.
onEnter(handler: (state) => void | Promise<void>)— run when the state is entered. Async handlers are awaited before the machine proceeds.onExit(handler: (state) => void | Promise<void>)— run when the state exits. Registered inner-first, so unwinding runs inner-to-outer.onStateError(handler: (state, error) => void | Promise<void>)— handle an error thrown by another handler on this state; also bubbles to the process.dump(handler: (state, dump) => void | Promise<void>)— contribute to this state’s snapshot by fillingdump.data.restore(handler: (state, dump) => void | Promise<void>)— rehydrate from this state’s snapshot by readingdump.data.
There is no
getData/setDataonFsmState. Per-state data that must survive a dump lives in thedump/restorehandlers’databag; transient per-activation data lives in your handler closures.
startProcess
The ergonomic runner. Builds an FsmProcess, installs behaviour from one load callback, and binds the machine into a shared context.
const handle = await startProcess(context, config, load, startEvent = "");
context— a shared object your handlers read and write; the machine binds itself into it (see keys below).config— theFsmStateConfig.load(state: string, event: string | undefined): StageHandler[] | Promise<StageHandler[]>— returns the handlers to run for a state on entry.startEvent— the initial event dispatched to enter the machine (default"").
It returns a ProcessHandle. startFsmProcess is an exported alias of startProcess.
StageHandler
type StageHandler<C> = (context: C) =>
| void
| (() => void | Promise<void>) // cleanup, registered as onExit
| AsyncGenerator<string> | Generator<string> // events to dispatch back
| Promise<void | (() => void | Promise<void>)>;
A handler runs on entry with the shared context. Its return value wires up the rest:
- return nothing — no teardown;
- return a function — registered as this state’s
onExitcleanup; - return a generator — run concurrently; each yielded string is dispatched back into the machine (self-driving / reactive states), and the generator is
return()-ed automatically on exit.
Context keys
startProcess binds the machine into context under these exported keys, so any handler reaches the running machine uniformly:
KEY_DISPATCH—(event: string) => Promise<void>, dispatches an event (guarded — ignores events with no matching transition).KEY_TERMINATE—() => Promise<void>, shuts the machine down.KEY_STATES— the current state-key stack (root→leaf), refreshed on every transition.KEY_EVENT— the last dispatched event.
ProcessHandle
interface ProcessHandle {
shutdown(): Promise<void>;
dump(...args): Promise<FsmProcessDump>;
restore(dump, ...args): Promise<void>;
}
The caller’s remote control: stop and snapshot/rehydrate the machine without holding the underlying FsmProcess.
Transition queries
Read-only helpers over the graph — for a UI that needs to know which events can fire right now.
getStateTransitions(state?: FsmState): [from, event, to][]— the transitions reachable fromstate, collected by walking up the parent chain. The nearest state’s rule for an event wins, so an inner override masks an outer fallback. Ordered outer→inner.isStateTransitionEnabled(process: FsmProcess, event: string): boolean— wouldeventtrigger any transition in the current state? Use it to enable/disable controls or to skip dead events.
Tracing
See Tracing & Logging for the full guide.
setProcessTracer(process, print?)— trace every state: emits<key event="…">on enter and</key>on exit, so nested states read as nested tags. Returns a disposer.setStateTracer(state, print?)— trace a single state’s lifecycle.setProcessPrinter(process, config?)— attach a printer that indents each line by state depth.PrinterConfigacceptsprefix,print(defaults toconsole.log), andlineNumbers.getProcessPrinter(process)/getPrinter(state)— retrieve the attached printer.