Documentation
Finite State Machines
A tiny, type-safe statechart engine built on top of Alwatr Signals.
@alwatr/fsm replaces ad-hoc boolean flags — isLoading, hasError, isUploading, all mutable independently and therefore all capable of contradicting each other — with a declarative statechart where only one state is ever active at a time. It's built directly on top of Alwatr Signals and is fully re-exported by @alwatr/flux.
A config-driven machine
import type {StateMachineConfig} from '@alwatr/fsm';
import {createFsmService} from '@alwatr/fsm';
type FileState = 'idle' | 'uploading' | 'success' | 'failed';
interface FileContext { fileId: string | null; progress: number; }
type FileEvent =
| {type: 'START_UPLOAD'; fileId: string}
| {type: 'PROGRESS_UPDATE'; percent: number}
| {type: 'UPLOAD_SUCCESS'};
const config: StateMachineConfig<FileState, FileEvent, FileContext> = {
name: 'file-upload-lifecycle',
initial: 'idle',
context: {fileId: null, progress: 0},
states: {
idle: {
on: {
START_UPLOAD: {
target: 'uploading',
assigners: [({context, event}) => ({...context, fileId: event.fileId})],
},
},
},
uploading: {
on: {
// no target → internal transition: context updates, state stays 'uploading'
PROGRESS_UPDATE: {
assigners: [({context, event}) => ({...context, progress: event.percent})],
},
UPLOAD_SUCCESS: {target: 'success'},
},
},
success: {},
},
};
const fileUploadService = createFsmService(config);
fileUploadService.stateSignal.subscribe((state) => {
console.log(state.name, state.context.progress);
});
fileUploadService.dispatch({type: 'START_UPLOAD', fileId: 'doc_102'});Glossary
| Term | Meaning |
|---|---|
| State | A finite, named mode the machine can be in. |
| Context | Arbitrary extended data carried alongside the current state. |
| Event | A {type} message dispatched into the machine. |
| Transition | A rule describing how an event moves state A to state B. |
| Assigner | A pure function that computes the next context. |
| Effect | A fire-and-forget side-effect run on state entry or exit. |
| Actor | A spawned async process with its own lifecycle and cleanup, tied to a state. |
| Guard | A boolean predicate gating whether a transition may fire. |
Internal vs. external transitions
Every transition runs synchronously and atomically through a queue — Run-to-Completion semantics mean concurrent dispatches can never race each other.
- No
target: an internal, context-only update. Entry/exit effects and running actors are left untouched. - With
target(even the same state — a self-transition): a full exit → enter cycle, re-running entry/exit effects and respawning actors.
Guards, actors, and effects
An event can map to an array of {target, guard, assigners} candidates — the first whose guard passes wins; a guard-less entry acts as the fallback. Entry/exit effects are arrays of sync or async fire-and-forget functions (the FSM never awaits their return value). Actors are spawned on state entry and can dispatch events back asynchronously:
uploading: {
actors: [
({context, dispatch}) => {
const controller = uploadFile(context.fileId, (percent) => {
dispatch({type: 'PROGRESS_UPDATE', percent});
});
return () => controller.abort(); // cleanup, run automatically on exit
},
],
},Assigner, guard, and effect closures all run inside a try/catch — a thrown error is caught, logged, and the context update reverted. Also note the __init__ caveat: the initial state's entry effects and actors run with a synthetic {type: '__init__'} event, so handle custom event fields defensively if you read them there.
Persistence
Add a persistent: {schemaVersion, storageKey} block to the config to auto-sync state and context to storage — the same versioning story as Storage & Hydration.
Actor Model synergy
FSM is the "brain" (state + mailbox) of the Actor Model pattern described in Architecture & UDF: an input controller translates global actions into local FSM events, and FSM effects broadcast state changes back out via the global action bus — giving you fully decoupled feature modules with no direct imports between them.
API surface: createFsmService(config) → FsmService<S, E, C> exposing .stateSignal (a read-only signal of MachineState<S, C>), .dispatch(event), and .destroy().