npm package discovery and stats viewer.

Discover Tips

  • General search

    [free text search, go nuts!]

  • Package details

    pkg:[package-name]

  • User packages

    @[username]

Sponsor

Optimize Toolset

I’ve always been into building performant and accessible sites, but lately I’ve been taking it extremely seriously. So much so that I’ve been building a tool to help me optimize and monitor the sites that I build to make sure that I’m making an attempt to offer the best experience to those who visit them. If you’re into performant, accessible and SEO friendly sites, you might like it too! You can check it out at Optimize Toolset.

About

Hi, 👋, I’m Ryan Hefner  and I built this site for me, and you! The goal of this site was to provide an easy way for me to check the stats on my npm packages, both for prioritizing issues and updates, and to give me a little kick in the pants to keep up on stuff.

As I was building it, I realized that I was actually using the tool to build the tool, and figured I might as well put this out there and hopefully others will find it to be a fast and useful way to search and browse npm packages as I have.

If you’re interested in other things I’m working on, follow me on Twitter or check out the open source projects I’ve been publishing on GitHub.

I am also working on a Twitter bot for this site to tweet the most popular, newest, random packages from npm. Please follow that account now and it will start sending out packages soon–ish.

Open Software & Tools

This site wouldn’t be possible without the immense generosity and tireless efforts from the people who make contributions to the world and share their work via open source initiatives. Thank you 🙏

© 2025 – Pkg Stats / Ryan Hefner

finestate

v1.2.0

Published

Powerful and easy to use Finite State Machine with hierarchy, orthogonal states and 100% tests coverage.

Readme

finestate

Version Build Status Coverage License

finestate is a flexible and extensible Finite State Machine (FSM) library for TypeScript. It helps you model and manage stateful logic in a clear, maintainable way. Whether you're building game logic, complex UI flows, or any state-based application, finestate provides a structured approach with support for hierarchical states, parallel (orthogonal) states, and event-driven transitions. The library is lightweight and general-purpose, making it suitable for a wide range of domains.

Quick Usage

Get started quickly by defining your states as classes and specifying how they transition on events. Below are a couple of simple examples to illustrate usage.

Example 1: Toggle between two states
Imagine a simple on/off switch that toggles state on a "toggle" event:

Example2 Diagram

import { EventType, Fsm, State, stateDesc } from 'finestate';

class OffState extends State {
  // Entry hook (called when state is entered)
  init() {
    console.log('Entering Off');
  }
  // Event handling
  processEvent(evt: EventType) {
    if (evt === 'toggle') {
      // Transition to OnState
      // You must return on transit() immediately
      return this.transit(OnState); // it will return 'event handled'
    }
    // You may skip returning false and return nothing to specify the event is not processed
    // return false;             // event not handled
  }
  // Exit hook (called when state is exited)
  destroy() {
    console.log('Exiting Off');
  }
}

class OnState extends State {
  init() {
    console.log('Entering On');
  }
  processEvent(evt: EventType) {
    if (evt === 'toggle') {
      return this.transit(OffState);  // Transition back to OffState
    }
  }
  destroy() {
    console.log('Exiting On');
  }
}

// Set up the FSM with OffState as the initial state and OnState as an 
// alternative
const fsm = new Fsm([
  stateDesc(OffState),   // initial state
  stateDesc(OnState)     // another possible state at the same level
]);
fsm.init();              // initialize the state machine (enters OffState)

// Dispatch events to the state machine
fsm.dispatch('toggle');  // triggers transition from OffState to OnState
fsm.dispatch('toggle');  // triggers transition from OnState back to OffState

In this example, we define two state classes OffState and OnState by extending the base State class. The processEvent method in each class handles the "toggle" event by transitioning to the other state. We create an Fsm with OffState as the starting state. Calling fsm.dispatch('toggle') causes the FSM to switch states and call the appropriate entry/exit hooks (init/destroy).

Example 2: Hierarchical & Parallel states in a game menu
Consider a simple game flow: the game begins with a splash screen, moves to a loading phase, and then enters a main menu. When resources loading is finished, the game goes to Loaded state. It has two parallel regions: one for the menu UI (which can be active showing either the main menu or a settings screen, or inactive/hidden) and another for the game content (either no game running or a game in progress). We'll use string events like "begin", "ready", "play", "quit", "openMenu", "closeMenu", "settings", and "back" to transition between these states, but in the real life you would use some other markers, like all resources loaded or button clicked:

Example2 Diagram

import { EventType, Fsm, State, stateDesc } from 'finestate';

// Root states
class Splash extends State {
  init() {
    console.log('Showing splash screen.');
  }
  processEvent(evt: EventType) {
    if (evt === 'begin') {
      return this.transit(Loading);
    }
  }
  destroy() {
    console.log('Leaving splash screen.');
  }
}

class Loading extends State {
  init() {
    console.log('Loading game resources...');
  }
  processEvent(evt: EventType) {
    if (evt === 'ready') {
      return this.transit(Loaded);
    }
  }
  destroy() {
    console.log('Loading complete.');
  }
}

class Loaded extends State {
  init() {
    console.log('Initialize resource shared to Menu and Game states.');
  }
  // Loaded may handle global events if needed (none in this example)
}

// Menu region states (under Loaded)
class MenuActive extends State {
  init() {
    console.log('Menu is now active.');
  }
  processEvent(evt: EventType) {
    if (evt === 'closeMenu') {
      return this.transit(MenuInactive);
    }
  }
  destroy() {
    console.log('Menu deactivated.');
  }
}

class MenuInactive extends State {
  init() {
    console.log('Menu is inactive.');
  }
  processEvent(evt: EventType) {
    if (evt === 'openMenu') {
      return this.transit(MenuActive);
    }
  }
}

class MenuMain extends State {
  init() {
    console.log('Show main menu screen.');
  }
  processEvent(evt: EventType) {
    if (evt === 'settings') {
      return this.transit(MenuSettings);
    }
  }
  destroy() {
    console.log('Hide main menu screen.');
  }
}

class MenuSettings extends State {
  init() {
    console.log('Show settings screen.');
  }
  processEvent(evt: EventType) {
    if (evt === 'back') {
      return this.transit(MenuMain);
    }
  }
  destroy() {
    console.log('Hide settings screen.');
  }
}

// Game region states (under Loaded)
class NoGame extends State {
  processEvent(evt: EventType) {
    if (evt === 'play') {
      return this.transit(Game);
    }
  }
}

class Game extends State {
  init() {
    console.log('Init resource for the game.');
  }
  processEvent(evt: EventType) {
    if (evt === 'quit') {
      return this.transit(NoGame);
    }
  }
  destroy() {
    console.log('Destroy resources for the game.');
  }
}

// Set up the hierarchical state machine with parallel regions in Loaded
const fsm = new Fsm([
  stateDesc(Splash),    // initial root state
  stateDesc(Loading),
  stateDesc(Loaded, [
    // First child region (Menu): default MenuActive with its sub-states
    stateDesc(MenuActive, [
      stateDesc(MenuMain),
      stateDesc(MenuSettings)
    ]),
    stateDesc(MenuInactive)
  ], [
    // Second child region (Game): default NoGame
    stateDesc(NoGame),
    stateDesc(Game, [
      // Other game substates could be listed here
    ])
  ])
]);
fsm.init();  // enters Splash state initially

// Example event sequence:
fsm.dispatch('begin');      // Splash -> Loading
fsm.dispatch('ready');      // Loading -> Loaded (enters MenuActive/MenuMain and NoGame)
fsm.dispatch('settings');   // MenuMain -> MenuSettings
fsm.dispatch('back');       // MenuSettings -> MenuMain
fsm.dispatch('play');       // NoGame -> Game (game starts, menu still active)
fsm.dispatch('closeMenu');  // MenuActive -> MenuInactive (menu hidden, game still running)
fsm.dispatch('openMenu');   // MenuInactive -> MenuActive (menu shown again, enters MenuMain)
fsm.dispatch('quit');       // Game -> NoGame (game ended, menu still active)

In this example, the FSM starts in the Splash state. A "begin" event causes a transition to Loading, and when loading is complete a "ready" event transitions into the Loaded state.

The Loaded state has two orthogonal child regions running in parallel: a Menu region (MenuActive vs MenuInactive) and a Game region (NoGame vs Game). Upon entering Loaded, both regions are initialized: the menu region starts in MenuActive (which itself enters its default substate MenuMain), and the game region starts in NoGame. A "settings" event triggers a transition from MenuMain to MenuSettings (within the menu region), and a "back" event returns from MenuSettings to MenuMain. Independently, a "play" event in the game region transitions from NoGame to Game. The menu can be toggled off with "closeMenu", transitioning from MenuActive to MenuInactive while the Game state remains active. An "openMenu" event brings back the menu by transitioning MenuInactive to MenuActive (entering MenuMain by default), and finally a "quit" event from Game transitions back to NoGame. Throughout these transitions, the init and destroy methods log the state changes. This demonstrates a hierarchical state machine with parallel regions: the menu and game states operate concurrently under the Loaded parent state.

Installation

Install finestate via npm (or Yarn):

npm install finestate
# or with yarn:
yarn add finestate

The package includes type declarations and can be used in both Node.js and browser environments. It supports both CommonJS and ESModule imports:

  • ESM Import (TypeScript/modern JS):

    import { Fsm, State } from 'finestate';
  • CommonJS require:

    const { Fsm, State } = require('finestate');

No additional configuration is needed – just install and import.

Additionally, finestate provides a UMD (Universal Module Definition) build, suitable for older browsers or scenarios where you want to load the library directly via a <script> tag without using a module bundler. In this setup, a global variable named FineState is exposed on the window object, allowing you to access Fsm, State, and other exports. This can be useful for quick demos, legacy codebases, or any environment that does not yet support modern bundling or module imports. For example:

<script src="finestate.umd.js"></script>
<script>
  ...
  // Now we can create an FSM using the global FineState object
  const fsm = new FineState.Fsm([
    FineState.stateDesc(MyState)
    // ... other states ...
  ]);

  fsm.init();
  fsm.dispatch('start');
</script>

Core Concepts

finestate employs a class-based approach to define FSMs. Here are the core concepts to understand when using the library:

States as Classes

Each distinct state in your machine is represented by a class that extends the State base class. In these classes, you can override lifecycle hooks and event handlers:

  • init() – Called when the state is entered. Use this for entry side-effects or initializing state-specific data.
  • processEvent(event) – Called when an event is dispatched to this state. Implement your state's event logic here. Return true if the event is handled (to prevent further propagation), or false if not handled (so parent or sibling states can process it). This is where you can trigger transitions to other states.
  • destroy() – Called when the state is about to be exited. Use this for cleanup or exit side-effects.

You may define additional methods or properties in your state classes as needed for your application logic. Each state instance can access:

  • this.fsm – Reference to the Fsm controlling the state machine (useful if you need to access global FSM context).
  • this.parent – The parent state in a hierarchy (or null if this state is at the root).
  • this.children – An array of child state instances (for substates/orthogonal regions).

Hierarchical (Nested) States

States can be nested inside other states to form a hierarchy. A parent state can have substates (child states) that are active only when the parent is active. For example, a Moving state might contain Walking and Running substates. When the parent state is entered, one of its child states is initialized immediately (the first child listed is the default). Exiting a parent state will recursively exit its active substates.

You can access ancestor states via the context() helper. For instance, from within a substate you might call this.context(MovingState) to get a reference to the parent MovingState (or null if it’s not in that context).

Orthogonal (Parallel) States

finestate also supports orthogonal regions (parallel states), meaning a state can have multiple independent child state regions active at the same time. This is achieved by providing multiple child state arrays in the state description (see State Configuration below). Each orthogonal region will initialize its own child state, and events can be dispatched to all parallel children. By default, finestate will stop propagating an event to other regions once one child handles it (this behavior can be configured).

Parallel states are useful for representing aspects of state that evolve independently. For example, a game character could have an independent Movement state and Mood state running in parallel. (This is an advanced feature; many use-cases won’t need parallel states.)

Events and Dispatching

An event represents something that causes the state machine to react (e.g. a user action, a timer tick, a message, etc.). finestate allows events to be dispatched as:

  • A string or number (for simple event identifiers).
  • An object that is an instance of a subclass of the provided Event class (for more complex or typed events).

To send an event into the state machine, call fsm.dispatch(event). The FSM will propagate the event to the active states, following these rules:

  • Events are propagated depth-first by default: the most deeply nested active state gets the first chance to handle the event via its processEvent. If that state doesn’t handle it (processEvent returns false), the event bubbles up to its parent state.
  • In states with parallel children, the event is forwarded to each child in order. If one child handles the event (returns true), by default the event will not be sent to other parallel children (this is the StopOnProcessed policy). If no child handles it, the parent’s own processEvent is then called.
  • If a state’s processEvent returns true (indicating the event was processed and possibly a transition occurred), propagation stops at that state. If it returns false, the event continues up to the next state in the hierarchy.
  • The return value of fsm.dispatch(event) will be a boolean indicating if any state in the machine handled the event (true if handled, false if unhandled).

Using the event system, your processEvent methods can contain logic to decide transitions. You can compare the event to known types or instances (for example, if (evt instanceof MyCustomEvent) {...} or if (evt === 'someEvent') {...}) and perform actions accordingly.

Event Dispatch Order and Parallel Region Policy (Advanced)

By default, events propagate from child states up to parent states (a depth-first approach). You can customize the dispatch order and how events propagate in parallel regions for advanced scenarios:

Dispatch Order (child-first vs parent-first): Normally, a parent state's children receive events before the parent does (DispatchOrder.Ascending). You can override this by setting the protected dispatchOrder property to DispatchOrder.Descending in a state class (for example, in its constructor or init), causing that parent state to process events before its children. For instance, in the following example the parent logs the event before the child when using descending order:


class Parent extends State {
  init() {
    // Use parent-first dispatch order:
    this.dispatchOrder = DispatchOrder.Descending;
  }
  ...
}

or


class Parent extends State {
  protected dispatchOrder = DispatchOrder.Descending;
  ...
}

Parallel Regions Policy: If a state has multiple orthogonal child regions, the default policy DispatchOrthoPolicy.StopOnProcessed means that if one region handles an event (its processEvent returns true), the event will not be dispatched to the other regions. To broadcast events to all regions regardless of one handling it, set the state's dispatchOrthoPolicy to DispatchOrthoPolicy.DontStopOnProcessed. In the example below, the first child handles the event, but because we use DontStopOnProcessed, the second child still receives it:

class ParallelParent extends State {
  protected dispatchOrthoPolicy = DispatchOrthoPolicy.DontStopOnProcessed;
  ...
}

Custom Dispatch Override: In advanced cases, you can override a state’s dispatch(event) method to intercept or filter events. This is rarely needed, but it allows custom control. For example, a state could log every event and then use the default dispatch behavior:

class LoggingState extends State {
  dispatch(event) {
    console.log(`LoggingState saw event: ${event}`);
    // Perform custom handling or filtering if needed
    return super.dispatch(event); // continue with normal dispatch logic
  }
}

Generally, you will not override dispatch in typical usage, but the option is available if you need to customize event propagation.

Entry and Exit Hooks

As mentioned, each state class can implement init() and destroy() as entry and exit hooks. finestate calls these at the appropriate times:

  • init() – Called after the state object is constructed and added into the state machine. This is a good place to trigger any actions that should happen upon entering the state (e.g. resetting timers, sending a message, updating UI).
  • destroy() – Called right before the state object is removed from the state machine. Use this to clean up (e.g. clear timers, remove listeners, revert changes made in init).

There is also an initParams(params: StateParams) method you can override if you need to handle initialization data passed via the params object on fsm.init() or transit(). By default, initParams simply calls init(), ignoring any parameters, but you can override it to extract data from params when needed.

These hooks allow each state to manage its own enter/exit side effects in isolation, keeping your state logic encapsulated.

State Configuration

To create an FSM, you need to provide a description of the state structure to the Fsm constructor. finestate uses an array-based configuration to define the hierarchy and parallel regions of states. The formal type definition for a state description is:

// StateType = State class (constructor)
type StateDesc = [ StateType, ...StateDesc[][] ];

In simpler terms, a StateDesc is a tuple where the first element is a state class (constructor), and the remaining elements (if any) are arrays that describe that state’s child regions. (In code, you can use the helper function stateDesc(StateClass, ...children) to construct these tuples more readably.)

  • The first element of a StateDesc is always a State subclass (the state itself).
  • If a state has no substates, its description is just that state (e.g. `stateDesc(MyState)` with no child arrays).
  • If a state has substates, include one array for each orthogonal region:
    • For a simple hierarchy (one region of substates), include one array containing the child state descriptions. The first state in that array will be the default initial substate.
    • For parallel states, include multiple arrays (as separate regions), one per independent region of substates.

Examples:

  • `stateDesc(OffState)` – A state with no children.
  • `stateDesc(MovingState, [ stateDesc(WalkingState, [ stateDesc(RunningState) ]) ])`MovingState has one child region. In that region, WalkingState is listed first (so it’s the default substate), and RunningState is another possible substate in the same region.
  • `stateDesc(A, [ stateDesc(B) ], [ stateDesc(C) ])` – State A has two child regions running in parallel. The first region’s default state is B, and the second region’s default state is C (these would be active concurrently when A is active).

When you instantiate the Fsm with an array of StateDesc, the first element of the array is taken as the root state of the state machine. For example:

const fsm = new Fsm([
  stateDesc(RootState, [
    // Child states of RootState (single region)
    stateDesc(ChildState1, [
      // Child states of ChildState1 (single region)
      stateDesc(GrandChildState)
    ])
  ])
]);

This defines RootState as the top-level state. RootState has one child region containing ChildState1, which in turn has a child region containing GrandChildState. Calling fsm.init() will construct this hierarchy: entering RootState, then ChildState1, then GrandChildState.

You can also define multiple possible root states in the array if your machine can start in different configurations or transition at the top level. In that case, the first state in the list is the initial root, and others are inactive until you transition to them.

Transitions

A transition moves the FSM from one state to another. In finestate, transitions are initiated in code by calling the transit() method, usually inside a state’s processEvent. There are two ways to invoke a transition:

  • From within a state: Call this.transit(TargetStateClass, params?). This will request the FSM to exit the current state (and any necessary parent states) and enter a new state of type TargetStateClass. The optional params object can carry data into the new state(s) during initialization.
  • Directly via the Fsm: You can call fsm.transit(currentState, TargetStateClass, params?) if you have a reference to a state and want to transition out of it. However, in practice you rarely need to call this directly; using the state’s own transit method is more convenient.

When a transition is triggered, finestate will:

  • Call the destroy() hook on the exiting state and any of its substates that are leaving.
  • Instantiate the target state (and any of its parent states, if they weren’t already active) and call init() on them.
  • If the new state has its own default substates, those will be created and initialized as well.

Transitions can occur between states at any level of the hierarchy. finestate will automatically determine the proper states to exit and the common ancestor where the new state should be inserted. For example, transitioning from a substate to a sibling substate will exit the current substate and possibly its parent, then enter the target state (and re-enter any necessary parent context if needed).

Note: You should not manually instantiate state classes. Always use transit() to switch states, so that the FSM can manage the lifecycle (calls to init()/destroy()) correctly.

Note: When calling this.transit(OtherState) inside a processEvent, it’s best to return that call immediately. This ensures the function exits right after initiating the transition (preventing any subsequent logic in processEvent from running) and clearly signals that the event was handled.

Below is an example of an extended hierarchical state machine with parallel regions and a step-by-step demonstration of transitions between states. This scenario illustrates how finestate manages entry (init()) and exit (destroy()) hooks across a nested/parallel configuration.

Transition Sequence Example

Let's suppose we have a state machine with a following diagram: Transition Declaration The declaration will look like this:

class Root extends State {
  init() { console.log('Root.init()'); }
  destroy() { console.log('Root.destroy()'); }
}

class A1 extends State {
  init() { console.log('A1.init()'); }
  destroy() { console.log('A1.destroy()'); }
}

...

const fsm = new Fsm([
  stateDesc(Root, [
    stateDesc(A1, [
      // Parallel region #1 within A1
      stateDesc(B1),
      stateDesc(C1)
    ], [
      // Parallel region #2 within A1
      stateDesc(D1),
      stateDesc(E1)
    ]),
    stateDesc(A2, [
      // Parallel region #1 within A2
      stateDesc(B2),
      stateDesc(C2)
    ], [
      // Parallel region #2 within A2
      stateDesc(D2),
      stateDesc(E2)
    ])
  ])
]);

Below is a hypothetical series of transitions that highlights how init/destroy logs are generated. We assume each transition is triggered via something like this.transit(TargetState) inside a processEvent(...) method.

  1. Initialization (fsm.init())

    After calling init(), the FSM enters Root, then goes to A1. Inside A1, the first parallel region defaults to B1, and the second to D1.

    (Active states are highlighted)

    Transition Declaration Console logs:

    Root.init()
    A1.init()
    B1.init()
    D1.init()
  2. Transition B1 -> C1
    Within the first parallel region of A1, B1 exits and C1 enters.
    Transition Declaration Logs:

    B1.destroy()
    C1.init()
  3. Transition C1 -> A2
    This exit leaves D1 and C1 (the active regions inside A1), and then A1 itself. We enter A2 and its default parallel children (B2 and D2). Transition Declaration Logs:

    D1.destroy()
    C1.destroy()
    A1.destroy()
    A2.init()
    B2.init()
    D2.init()
  4. Transition D2 -> E1
    Since E1 belongs to A1, we have to exit the entire A2 branch (B2, D2, and A2) and then enter A1. This time, for A1’s first region, we get B1 by default again, and in the second region we specifically enter E1.
    Transition Declaration Logs:

    D2.destroy()
    B2.destroy()
    A2.destroy()
    A1.init()
    B1.init()
    E1.init()
  5. Transition E1 -> C2
    Again we exit A1 (thus B1 and E1), enter A2, and in the first region we now go directly to C2 instead of B2. The second region defaults to D2.
    Transition Declaration Logs:

    E1.destroy()
    B1.destroy()
    A1.destroy()
    A2.init()
    C2.init()
    D2.init()
  6. Transition C2 -> A2
    If the transition is defined so that C2 leads back to the “same” parent (A2) as a fresh entry, we effectively exit C2, D2, and A2, then re-enter A2 with its default parallel states (B2 and D2). Transition Declaration Logs:

    D2.destroy()
    C2.destroy()
    A2.destroy()
    A2.init()
    B2.init()
    D2.init()
  7. Transition B2 -> B2
    If the transition is defined to itself, the state will exit and re-enter. Transition Declaration Logs:

    B2.destroy()
    B2.init()

Deferred Dispatch and Transition Behavior

finestate prevents reentrant state changes by deferring or forbidding certain calls. If dispatch() or transit() is invoked in the middle of another operation (such as an ongoing dispatch or transition), the library enforces the following rules:

  • Calling dispatch() from within another dispatch(): If an event is dispatched while a previous dispatch call is still in progress, the new dispatch call is deferred. The event will not be processed immediately; instead, it is queued to run right after the current dispatch cycle completes. In this scenario, the inner dispatch() returns false immediately, indicating that the event was not handled in the current cycle since it has been scheduled for processing after the ongoing dispatch completes.

  • Calling dispatch() from within a transit() or init(): If dispatch() is invoked during a state transition (i.e., from inside a transit() or init() call), that dispatch is also deferred until the transition is complete. The dispatch() call returns false right away in this case as well, since the event will only be handled after the current transition ends.

  • Calling transit() within fsm.init(): Starting a state transition during the FSM’s initialization phase is not allowed. If transit() is called inside an fsm.init() (during the setup of the state machine), finestate will throw an error. This restriction ensures that no transitions occur while the state machine is still establishing its initial state(s).

  • Calling transit() from within another transit(): Nested state transitions are prohibited. If a transition is already in progress and transit() is invoked again (before the first transition finishes), finestate will throw an error. Only one transition can occur at a time, so any new transition must wait until the current one has fully completed.

API Documentation

Below is an overview of the main classes and types provided by finestate, and their usage. The API is designed to be simple – most interactions are via the Fsm and your own State subclasses.

Fsm class

  • new Fsm(stateDescriptions: StateDesc[]) – Constructs a new state machine using the given state description array. You should provide at least one state description (which will be the root state). Child states and parallel regions are described with nested arrays (or using the stateDesc helper function) as detailed above.
  • init(params?: StateParams): void – Initializes the FSM and enters the initial state configuration. This will construct the root state (and its default child states, recursively) and call init() on all of them. You can pass an optional params object that will be forwarded to all states’ initParams for initial setup data.
  • dispatch(event: EventType): boolean – Dispatches an event into the state machine. The event can be an instance of a subclass of Event, a string, or a number. Returns true if some state handled the event, or false if the event was unhandled. Use this to trigger transitions or actions in response to external inputs.
  • rootState: State | null – A property referencing the current root state object. After init() has been called, this will be the active root state instance. You can use it to inspect the state hierarchy (for example, fsm.rootState!.children[0] to get the first child, etc.). This will change if you transition to a different root state.
  • transit(fromState: State, toStateType: StateType, params?: StateParams): State – Transitions from an existing state to a new state of type toStateType. This method handles tearing down the appropriate part of the state tree starting at fromState and constructing the new state in its place. In most cases you don’t need to call this directly (use State.transit inside processEvent).

State class

To create your own states, subclass State. The State class provides methods and properties as the interface for each state:

  • Lifecycle hooks:

    • init(): void – Override this to define behavior when the state is entered. This is called after the state is constructed and attached to the FSM.
    • destroy(): void – Override this for behavior when the state is exited and about to be destroyed.
    • initParams(params: StateParams): void – Override it instead of init() if you need to handle custom initialization data. By default it calls init().
  • Event handling:

    • processEvent(event: EventType): boolean – Override this to handle incoming events for this state. Return true if the event is handled (consumed) by this state (which may include performing a transition or some action). Return false to indicate the event wasn’t handled here, allowing the event to propagate to parent/other states. The default implementation in State returns false (no events handled).
    • dispatchChildren(event: EventType): boolean - This method is inherited, and you typically don't override it. The method dispatches the event to children with respect to dispatchOrder and dispatchOrthoPolicy properties of the state. Should return true if the event was processed by one of children. Override it if you want to make custom way of the event delivery to the children.
    • dispatch(event: EventType): boolean – This method is inherited, and you typically don’t override it. It will dispatch the event to this state’s children through dispatchChildren() and then to processEvent of the current event if the event is not processed by the children. In most cases you will call fsm.dispatch(...) from outside the FSM, rather than calling dispatch on a state directly. You may override the method to make custom events dispatching process.
  • Transition methods:

    • transit(targetState: StateType, params?: StateParams): boolean – Call this from within a state (e.g., in processEvent) to transition out of the current state to a new state of type targetState. This is essentially a helper that calls the FSM’s transit method for you. It returns true to indicate the event triggering the transition was handled.
  • Context and hierarchy:

    • fsm: Fsm – Property pointing back to the Fsm instance managing this state.
    • parent: State | null – The parent state in the hierarchy (or null if this state is the root).
    • children: Array<State | null> – An array of this state’s child state instances. The length of this array equals the number of orthogonal (parallel) child regions defined for the state. Each entry is either null (if that region has no active state) or a reference to the active child state in that region. For example, a state with one child region will have children[0] as its single substate (or null if uninitialized).
    • context<T extends State>(stateClass: new(...args: any[]) => T): T | null – A helper method to retrieve an ancestor state of a specific class. If this state or any of its parents is an instance of stateClass, that instance is returned. Otherwise, it returns null. This is useful to access shared context or data from a parent state without global variables.
  • Event dispatch controls (advanced):

    • dispatchOrder (enum DispatchOrder) – A protected property that determines whether events are sent to child states first or to the state itself first. By default, this is DispatchOrder.Ascending (child states get the event before the parent does, i.e. depth-first propagation). You can set it to DispatchOrder.Descending in a subclass (e.g., in the constructor or init) if you need the parent state to process events before children.
    • dispatchOrthoPolicy (enum DispatchOrthoPolicy) – A protected property that controls event propagation in parallel regions. The default is DispatchOrthoPolicy.StopOnProcessed, meaning if one child in an orthogonal set handles the event, other children will not receive it. If you set this to DontStopOnProcessed, every parallel child state will receive the event regardless of others handling it (useful if events should be broadcast to all active substates).

Most of the time, you will only override init, processEvent, and destroy in your states. The other properties and methods are there if you need introspection or advanced control over the state machine’s behavior.

Event class and EventType

finestate provides an abstract Event class which you can extend to create custom event types. This is entirely optional – you can use simple strings or numbers as events if that suits your needs. However, using classes for events can be useful for strong typing and richer data.

  • class Event – Base class for custom events. (It doesn’t define any properties itself; you can subclass it to add event payload data or simply use the class type to distinguish events.)
  • EventType – This is a union type defined as Event | string | number. It indicates what can be passed to fsm.dispatch or processEvent. So, you can dispatch an instance of your custom Event subclass, or a string/number identifier.

Example: You might define class JumpEvent extends Event { /*...*/ } and then in processEvent(evt), check if (evt instanceof JumpEvent) { ... } to handle that event type.

StateParams

The library defines a StateParams interface (empty by default) that you can extend to pass structured data into state initializations. The params object you provide to fsm.init(params) or transit(..., params) will be received by each new state’s initParams. For example:

interface StateParams {
  MyState?: {
    level: number;
    playerName: string;
  }
}

fsm.init({ MyState: { level: 3, playerName: "Alice" } });

...

class MyState extends State {
  initParams(params: StateParams) {
    const { level, playerName } = params.MyState!;
    // Use parameters for the State initialization
  }
}

Then your state could override initParams (as shown above) to read those values on entry. This is an advanced usage; many times you may not need to pass parameters at initialization.

TypeScript Support and Module Compatibility

finestate is written in TypeScript and ships with complete type definitions. This means you’ll get strong typing and IntelliSense out of the box when using it in a TypeScript project. State classes, events, and the FSM methods all have typed signatures, which helps catch errors at compile time.

Even if you use finestate in a JavaScript project, the type declarations can serve as documentation for the API, and you can refer to them for clarity on how to use the library.

The distributed package includes both ESM (ES6 modules) and CommonJS builds:

  • For modern bundlers or Node.js (ESM), the module entry point is used (tree-shaking friendly).
  • For older environments or CommonJS usage, the main entry point provides a CJS bundle.

This means you can import finestate in whatever way your project requires, and it should work seamlessly. The library has no external runtime dependencies, so it’s straightforward to include in browsers or Node. It’s also framework-agnostic – use it with React, Angular, or plain Node.js scripts as needed.

License

finestate is open source and licensed under the MIT License. You are free to use it in commercial or personal projects. Please see the LICENSE file for the full text of the license.

Contributing

Contributions are welcome! If you find a bug or have an idea for an improvement, feel free to open an issue or submit a pull request on GitHub. When contributing code, please ensure that all tests pass (npm test) and add new tests for any new functionality.

We appreciate the community’s help in making finestate better. Whether it’s reporting issues, writing documentation, or adding features, any contribution is valuable.

Happy state modeling with finestate!