lite-states
v1.0.2
Published
Zero-dependency finite state machine with strict transitions, lifecycle hooks, and emergency force() escape hatch.
Downloads
188
Maintainers
Readme
lite-states
A zero-dependency finite state machine with strict transition validation, lifecycle hooks, and an emergency escape hatch.
Built for games, UI flows, and any system where state transitions must be predictable and auditable.
Features
- Strict transitions — only allowed state changes succeed, everything else is blocked with a console warning
- Lifecycle hooks —
onEnter,onLeave,onChangewith disposer-based cleanup - Emergency
force()— bypass rules for debug menus, level skips, or error recovery - State queries —
is(),isAnyOf(),can() - Disposer pattern — every hook returns a cleanup function
- Clean teardown —
destroy()clears hooks and freezes the machine - Zero dependencies, < 1 KB
Installation
npm install lite-statesQuick Start
import { FSM } from 'lite-states';
const fsm = new FSM('idle', {
idle: ['loading'],
loading: ['ready', 'error'],
ready: ['playing'],
playing: ['complete', 'error'],
complete: ['idle'],
error: ['idle'],
});
fsm.set('loading'); // true — valid transition
fsm.set('playing'); // false — blocked (loading → playing not allowed)
fsm.set('ready'); // true
fsm.set('playing'); // trueDefining Transitions
The transition map is a plain object where each key is a state and its value is an array of states it can transition to:
const transitions = {
idle: ['loading'], // idle can only go to loading
loading: ['ready', 'error'], // loading can go to ready OR error
ready: ['playing'], // ready can only go to playing
playing: ['complete', 'error'], // playing can go to complete OR error
complete: ['idle'], // complete loops back to idle
error: ['idle'], // error recovers to idle
};Lifecycle Hooks
All hooks return a disposer function for cleanup:
// Fires when entering 'playing' — receives the previous state
const dispose1 = fsm.onEnter('playing', (prevState) => {
console.log(`Started playing from ${prevState}`);
startGameLoop();
});
// Fires when leaving 'playing' — receives the next state
const dispose2 = fsm.onLeave('playing', (nextState) => {
console.log(`Left playing, going to ${nextState}`);
stopGameLoop();
});
// Fires on EVERY state change — great for logging/analytics
const dispose3 = fsm.onChange((prev, next) => {
analytics.track('state_change', { from: prev, to: next });
});
// Clean up when done
dispose1();
dispose2();
dispose3();Hook firing order: onLeave → onEnter → onChange
API
Constructor
const fsm = new FSM(initialState, transitions);State Transitions
| Method | Returns | Description |
|--------|---------|-------------|
| .set(state) | boolean | Validated transition. Returns true if successful or same-state. |
| .force(state) | void | Bypass rules. Fires hooks. Logs a warning. |
| .can(state) | boolean | Check if transition is allowed without executing it. |
Queries
| Method | Returns | Description |
|--------|---------|-------------|
| .is(state) | boolean | Check if current state equals state. |
| .isAnyOf(...states) | boolean | Check if current state is in the list. |
| .current | string | Current state (getter). |
Hooks
| Method | Returns | Description |
|--------|---------|-------------|
| .onEnter(state, fn) | Disposer | Called when entering state. fn(prevState). |
| .onLeave(state, fn) | Disposer | Called when leaving state. fn(nextState). |
| .onChange(fn) | Disposer | Called on every change. fn(prev, next). |
Lifecycle
| Method | Description |
|--------|-------------|
| .destroy() | Clear all hooks, freeze the FSM. Idempotent. |
Emergency Escape Hatch
Sometimes you need to break the rules — debug menus, level skips, error recovery:
fsm.force('idle'); // FSM Forced: playing -> idleforce() fires all lifecycle hooks normally but skips transition validation. It logs a warning so you can trace unexpected state jumps in production.
TypeScript
import { FSM, type TransitionMap, type Disposer } from 'lite-states';
const transitions: TransitionMap = {
idle: ['loading'],
loading: ['ready', 'error'],
};
const fsm = new FSM('idle', transitions);
const dispose: Disposer = fsm.onChange((prev, next) => {
// fully typed
});License
MIT
