Small deterministic FSM library for replayable TypeScript/JavaScript state machines. Definitions are plain data; guards/actions/effects are injected at runtime.
Status: 0.5.9 - stable 1.0-track core. Core FSM, guards, effects, inspect, replay, PBT helpers, scheduler, and sub-machines are live.
pnpm add aifsmjsimport { assign, createRuntime, setup } from "aifsmjs";type Ctx = { ticks: number };
type Evt = { type: "NEXT" };
const trafficLight = setup<Ctx, Evt>().defineMachine({
id: "trafficLight",
initial: "red",
context: { ticks: 0 },
states: {
red: { on: { NEXT: { target: "green", actions: ["bump"] } } },
green: { on: { NEXT: { target: "yellow", actions: ["bump"] } } },
yellow: { on: { NEXT: { target: "red", actions: ["bump"] } } },
},
});
const runtime = createRuntime(trafficLight, {
actions: {
bump: assign(({ context }) => ({ ticks: context.ticks + 1 })),
},
});
runtime.send({ type: "NEXT" });
console.log(runtime.getSnapshot().value); // "green"Prefer setup<Ctx, Evt>().defineMachine() for state inference. Use bare defineMachine<Ctx, Evt, States>() only when you need explicit generic control.
| Import | Purpose |
|---|---|
aifsmjs |
setup, defineMachine, createRuntime, createMachine, step, assign, snapshots, runtime/errors/types. |
aifsmjs/guards |
and, or, not, stateIn. Guards must be synchronous. |
aifsmjs/effects |
enqueue.effect() descriptors and runEffects(). |
aifsmjs/inspect |
Read-only middleware helpers: logger, persist, recorder. |
aifsmjs/replay |
Pure event-log replay. |
aifsmjs/pbt |
fast-check property helpers. |
aifsmjs/timer |
after() and createScheduler(). |
step(def, snapshot, event, impl)is pure and returns{ snapshot, effects, changed }.createRuntime()owns mutable runtime state, dispatches effects after commit, and emits transition/error/dispose events.- Guards and reducers are sync. Thenable guards throw
AsyncGuardError. - Effects are fire-and-forget descriptors. Async rejection is routed to the runtime
"error"channel. reset()rewinds the snapshot and notifies listeners, but does not run entry actions.dispose()is idempotent; post-disposesend()/reset()throwRuntimeDisposedError.
- Middleware and synchronous effect throws happen after snapshot commit. A throw can leave the committed snapshot visible without later notification.
- Sub-machine replacement can roll back on init failure, but dispose failure has already torn down the old child.
subRuntime()can return a disposed child handle if external code disposed it; it is recreated only after the parent exits and re-enters the sub state.setup().defineMachine()usesNoInferso states infer fromkeyof states; keep regression tests for exact optional property configurations.- Do not perform async I/O inside guards or actions. Send events from effects instead.
- Short index:
llms.txt - Full generated context:
llms-full.txt - Stability contract:
STABILITY.md - Current review backlog:
REVIEW.md - Release history:
CHANGELOG.md
MIT