Relocate the ~6k-file docs tree from the repo root into plugins/document-navigator/docs/ so the plugin is self-contained and portable. docs/superpowers/ (specs and plans) stays at the repo root. Moved via cp+rm rather than git mv because directory rename() fails with EPERM on this WSL /mnt DrvFs mount; git rename-detection pairs the identical-content files at commit time. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
@suaveplan/fsm - Typed finite state machine with guards, actions, history tracking, hierarchical states, parallel regions, deferred events, and OTel instrumentation
Summary
Typed, generic finite state machine (FSM) for TypeScript. Ships a synchronous-by-default interpreter with guard predicates, ordered transition actions, context threading, entry/exit lifecycle hooks, history recording, and OTel instrumentation. Zero peer dependencies beyond @suaveplan/error, @suaveplan/events, and @suaveplan/types.
Documentation tree
- interpreter — (no submodules)
- fsm-interpreter — (no submodules)
- errors — (no submodules)
- fsm-error — (no submodules)
- context-helpers — Provides ergonomic factory helpers for building context-update and side-effect actions that can be slotted directly int…
- definition-validator — Validates an
FSMDefinitionobject before the interpreter begins executing. Early structural validation prevents subtl…
- definition-validator — Validates an
FSMDefinitionobject before the interpreter begins executing. Early structural validation prevents subtl… - fsm-interpreter — (no submodules)- errors — (no submodules)- fsm-error — (no submodules)- context-helpers — Provides ergonomic factory helpers for building context-update and side-effect actions that can be slotted directly int…
- definition-validator — Validates an
FSMDefinitionobject before the interpreter begins executing. Early structural validation prevents subtl… - fsm-interpreter — (no submodules)- errors — (no submodules)- fsm-error — (no submodules)- context-helpers — Provides ergonomic factory helpers for building context-update and side-effect actions that can be slotted directly int…
- definition-validator — Validates an
FSMDefinitionobject before the interpreter begins executing. Early structural validation prevents subtl… - fsm-interpreter — (no submodules)- errors — (no submodules) - fsm-error — (no submodules)- context-helpers — Provides ergonomic factory helpers for building context-update and side-effect actions that can be slotted directly int…
- definition-validator — Validates an
FSMDefinitionobject before the interpreter begins executing. Early structural validation prevents subtl…
Install
bun add @suaveplan/fsm
Quick Start
import { createMachine, assign } from '@suaveplan/fsm';
interface TrafficCtx { count: number }
const traffic = createMachine<TrafficCtx>({
id: 'traffic',
initial: 'red',
context: { count: 0 },
states: {
red: { on: { NEXT: { target: 'green' } } },
green: { on: { NEXT: { target: 'yellow' } } },
yellow: {
on: {
NEXT: {
target: 'red',
actions: [assign<TrafficCtx>(ctx => ({ count: ctx.count + 1 }))],
},
},
},
},
});
traffic.send('NEXT'); // → green
traffic.send('NEXT'); // → yellow
const state = traffic.send('NEXT'); // → red
console.log(state.context.count); // 1
console.log(state.history.length); // 3
Modules
| Module | Path | Description |
|---|---|---|
| Root | @suaveplan/fsm |
createMachine, types, assign, pure, validateDefinition |
| Interpreter | @suaveplan/fsm/interpreter |
FSMInterpreter, createMachine |
| Errors | @suaveplan/fsm/errors |
FSMError, FSM_CODES |
Core Concepts
createMachine(definition)
Factory function returning an FSMInterpreter instance. The definition is validated eagerly — malformed definitions throw FSMError(INVALID_DEFINITION) before any events are processed.
interface FSMDefinition<TContext> {
id?: string; // used in OTel metric labels
initial: string; // must exist in states
context: TContext; // initial extended context
states: Record<string, StateDefinition<TContext>>;
}
Guards
Guard functions on a transition prevent it from firing when they return false. If multiple transitions are declared for the same event (as an array), the first one whose guard returns true is taken.
states: {
locked: {
on: {
UNLOCK: [
{ target: 'open', guard: (ctx, evt) => evt.payload === ctx.pin },
{ target: 'locked', guard: () => true }, // fallthrough (stay locked)
],
},
},
}
Actions
Transition actions are called with the accumulated context. Returning a new context replaces it; returning void leaves it unchanged.
import { assign, pure } from '@suaveplan/fsm';
const bump = assign<Ctx>(ctx => ({ ...ctx, count: ctx.count + 1 }));
const logEvt = pure<Ctx>((ctx, evt) => console.log(evt.type, ctx));
Entry / Exit lifecycle
states: {
loading: {
entry: ctx => ({ ...ctx, loading: true }),
exit: ctx => ({ ...ctx, loading: false }),
on: { DONE: { target: 'idle' } },
},
}
Final states
Entering a state with type: 'final' sets state.done = true.
Subscriptions
const unsub = machine.onTransition(state => {
console.log('now in', state.value, 'done?', state.done);
});
// later:
unsub();
can() and matches()
machine.can('NEXT'); // true if the event is declared in current state
machine.matches('green'); // true if currently in 'green'
reset()
Resets to initial state, original context, and clears history.
OTel Metrics
| Name | Type | Attributes |
|---|---|---|
fsm.transition.total |
Counter | id, from, to, event |
fsm.transition.duration_ms |
Histogram | id, from, to |
Error Codes
| Code | Meaning |
|---|---|
FSM.INVALID_DEFINITION |
Structural error in definition (missing initial, unknown target, etc.) |
FSM.INVALID_TRANSITION |
No matching transition for event in current state |
FSM.INVALID_STATE |
Current state not found in definition |
FSM.GUARD_FAILED |
All guards for an event returned false |
FSM.ACTION_ERROR |
An action or lifecycle function threw |