micro-fsm
v0.2.5
Published
A tiny finite state machine library
Downloads
26
Maintainers
Readme
micro-fsm
A minimalistic yet powerful Finite State Machine (FSM) library for JavaScript/TypeScript projects. Only 227 bytes.
A Finite State Machine (FSM) is a computational model that represents a system as a set of distinct states and the transitions between those states. The system can only be in one state at a time, and it transitions from one state to another based on specific inputs or events. FSMs are useful for modeling systems with well-defined behaviors, such as user interfaces, communication protocols, or game logic. They help in designing, implementing, and reasoning about the behavior of such systems.
Features
- Type-safe transitions: Leverages TypeScript's advanced type system to infer valid states and transitions, enabling compile-time checks.
- Minimalistic API: Provides a simple and intuitive interface for defining and interacting with state machines.
- Customizable: Allows for custom logic execution during state transitions through optional functions.
- Lightweight: Minimal footprint, only 227 bytes minimized. No dependencies.
Installation
npm install micro-fsm
Example
import fsm from "micro-fsm";
const machine = fsm("stopped", {
start: { from: ["stopped", "paused"], to: "started" },
stop: { from: ["started", "paused"], to: "stopped" },
pause: { from: ["started"], to: "paused" },
resume: { from: ["paused"], to: "started" },
log: {
fn: (message: string) => console.log(`[${machine.current}] ${message}`),
},
});
// The type of `machine` is automatically inferred by TypeScript. The following
// assertion shows the inferred type.
machine satisfies {
current: "stopped" | "paused" | "started";
prev: "stopped" | "paused" | "started";
can(eventName: "start" | "stop" | "pause" | "resume" | "log"): boolean;
start(): void;
stop(): void;
pause(): void;
resume(): void;
log(message: string): void;
};
// Starting from 'stopped'
machine.start(); // State changes to 'started'
// Pausing from 'started'
machine.pause(); // State changes to 'paused'
// Resuming from 'paused'
machine.resume(); // State changes to 'started'
// Stopping from 'started'
machine.stop(); // State changes to 'stopped'
// Logging state from 'stopped'
machine.log("Hello, world"); // Logs '[stopped] Hello, world'
Usage
The machine
machine.current
: This property holds the current state of the FSM.machine.prev
: This property stores the previous state before the last transition.machine.can(eventName)
: This method checks if a given event can be executed from the current state. It returnstrue
if the transition is allowed according to the FSM's configuration, andfalse
otherwise.machine.<transition>()
: Perform the transition. Throws if not allowed. If the event's config has a custom functionfn
, that function is called as part of the transition.
Setting up the FSM
The FSM is initialized with fsm(initialState, config)
, where initialState
is the starting state of the machine, and config
is an object defining the states, transitions, and optional actions.
Each key in the config
object represents an event name, and its value is an object specifying the conditions for the transition.
from
: An array listing the states from which the transition can occur. It's optional; if omitted, the transition can happen from any state.to
: Specifies the target state of the transition. It's also optional; omitting it means the event doesn't change the state but can still execute logic viafn
.fn
: A function that executes when the event occurs. This allows for custom logic during transitions, such as logging, validation, or triggering side effects. Likefrom
andto
, it's optional.
Advanced state transitions
The FSM allows for defining transitions with custom logic through the fn
property. This can be leveraged for various purposes such as validation, logging, or triggering side effects during transitions.
Example: Validation during transition
Imagine you have a state machine for a simple workflow system with states like draft
, review
, and published
. You might want to validate certain conditions before transitioning from review
to published
.
Only synchronous checks are supported.
import fsm from "micro-fsm";
const workflowMachine = fsm("draft", {
submit: { from: ["draft"], to: "review" },
publish: {
from: ["review"],
to: "published",
fn: () => {
// Custom validation logic here
if (!someValidationCondition()) {
throw new Error("Cannot publish due to validation failure.");
}
},
},
});
workflowMachine.submit(); // Moves from 'draft' to 'review'
try {
workflowMachine.publish(); // Attempts to move from 'review' to 'published'
} catch (error) {
console.error(error.message); // Handles validation failure
}
API
The library exports a single function that creates a Finite State Machine.
export default function fsm<
TState extends string,
TConfig extends Config<TState, Function>,
>(initialState: TState, config: TConfig): FiniteStateMachine<TState, TConfig>;
interface Config<TState extends string> {
[eventName: string]: EventConfig<TState>;
}
interface EventConfig<TState extends string> {
from?: readonly TState[];
to?: TState;
fn?: Function;
}
type FiniteStateMachine<
TState extends string,
TConfig extends Config<TState, Function>,
> = FiniteStateMachineBase<TState, keyof TConfig>;
interface FiniteStateMachineBase<
TState extends string,
TEventName extends string,
> {
readonly current: TState;
readonly prev: TState;
can(eventName: TEventName): boolean;
}
type GetEventsFromConfig<T extends Config<string>> = {
[K in keyof T]: T[K] extends { fn: infer F } ? F : () => void;
};
Note: The actual TypeScript types differ from the API above, in order to infer all types, in order to not have to specify the complex type parameters.