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 🙏

© 2026 – Pkg Stats / Ryan Hefner

nano-statechart

v0.1.3

Published

Lightweight, first-principles statechart engine in TypeScript

Downloads

364

Readme

nano-statechart

A lightweight, first-principles statechart engine in TypeScript.

nano-statechart provides a powerful pure-data state machine implementation, heavily inspired by the SCXML specification and xstate, but designed to be tiny, fast, and dependency-free. It supports advanced features like hierarchical states, parallel regions, history states, contextual data, guards, and side effects.

Features

  • Pure Data Definitions: Define your machines as pure data structures type-checked by TypeScript.
  • Pure Execution: The core execute function is referentially transparent. It takes a state, event, context, and history, and returns the next state and effects without mutations.
  • Stateful Service Wrapper: Use createService for an imperative, event-driven API similar to traditional statechart libraries.
  • Hierarchical States: Nest states within states, defining entry/exit paths recursively.
  • Parallel Regions: Execute orthogonal state machines concurrently.
  • History States: Remember the last active child state and resume seamlessly.
  • Guards & Actions: Powerful conditional routing and side effect management.
  • Lightweight: Tiny footprint. Zero external runtime dependencies.

Installation

npm install nano-statechart

Quick Start

1. Using the Stateful Service (Imperative API)

The easiest way to use nano-statechart is with the createService wrapper. It maintains the current state internally and executes effects for you.

import { createService, MachineDefinition } from "nano-statechart";

type LightState = "Green" | "Yellow" | "Red";
type LightEvent = { type: "TIMER" };
type LightFx = { type: "log"; message: string };

const lightMachine: MachineDefinition<LightState, LightEvent, LightFx> = {
    initial: "Green",
    context: undefined,
    states: {
        Green: { 
            on: { TIMER: { target: "Yellow", effects: [{ type: "log", message: "Going yellow" }] } },
            entry: [{ type: "log", message: "Entered Green" }] 
        },
        Yellow: { 
            on: { TIMER: { target: "Red" } },
        },
        Red: { 
            on: { TIMER: { target: "Green" } },
        },
    },
};

// Create a service, providing an optional effect handler
const service = createService(lightMachine, (effect) => {
    if (effect.type === "log") {
        console.log(effect.message);
    }
});

service.subscribe((result) => {
    console.log("Transitioned to:", result.next);
});

// Sends event, updates state internally, and calls the effect handler
service.send({ type: "TIMER" }); 
// Logs: 
// "Going yellow"
// "Transitioned to: Yellow"

2. Using Pure Execution

If you prefer managing state yourself (e.g., in a React generic reducer or a Redux store), you can use the pure execute function.

import { execute, getInitialState } from "nano-statechart";

// Assume `lightMachine` from the previous example

let currentState = getInitialState(lightMachine);

const result = execute(lightMachine, currentState, { type: "TIMER" }, undefined);

console.log(result.next); // "Yellow"
console.log(result.effects); // [{ type: "log", message: "Going yellow" }]

// You must track the next state yourself
currentState = result.next;

Core Concepts

Context (Extended State)

State machines can hold quantitative data in their context. Updates to the context happen via reduce functions on transitions.

type ATMState = "Idle" | "Active" | "Locked";
type ATMEvent = { type: "WRONG_PIN" };
type ATMCtx = { attempts: number };

const atmMachine: MachineDefinition<ATMState, ATMEvent, never, ATMCtx> = {
    initial: "Idle",
    context: { attempts: 0 },
    states: {
        Idle: {
            on: {
                WRONG_PIN: [
                    // Triggered if attempts is already 2 (this is the 3rd attempt)
                    {
                        target: "Locked",
                        guard: (e, ctx) => ctx.attempts >= 2,
                        reduce: (ctx) => ({ attempts: ctx.attempts + 1 }),
                    },
                    // Fallback transition
                    {
                        target: "Active",
                        reduce: (ctx) => ({ attempts: ctx.attempts + 1 }),
                    },
                ]
            }
        },
        // ... Active and Locked states
    }
}

Guards

Transitions can be conditional. By providing an array of transition objects, the machine will evaluate them in order and pick the first one where the guard returns true. If no guard is specified, it returns true.

on: {
    ENTER_PIN: [
        { target: "Unlocked", guard: (e, ctx) => e.pin === ctx.correctPin },
        { target: "Locked" } // fallback
    ]
}

Hierarchy (Nested States)

State machines can explode in complexity if you have to redefine standard transitions (like "LOGOUT") on every single state.

Hierarchical (nested) states solve this by allowing a parent state to handle shared events seamlessly for all its children. When a parent state is targeted, it automatically delegates to its defined initial child state.

You can define nested states naturally using a recursive states object:

type AppState = "LoggedOut" | "LoggedIn" | "LoggedIn.Dashboard" | "LoggedIn.Settings";
type AppEvent = { type: "LOGIN" } | { type: "LOGOUT" } | { type: "GO_SETTINGS" } | { type: "GO_DASHBOARD" };

const appMachine: MachineDefinition<AppState, AppEvent, string> = {
    initial: "LoggedOut",
    states: {
        LoggedOut: {
            on: { LOGIN: { target: "LoggedIn" } }
        },
        // Parent state:
        LoggedIn: {
            initial: "Dashboard", // Automatically enters Dashboard
            entry: ["welcome:user"],
            exit: ["goodbye:user"],
            on: { 
                // Any child state receiving "LOGOUT" will trigger this transition
                LOGOUT: { target: "LoggedOut" } 
            },
            states: {
                Dashboard: {
                    on: { GO_SETTINGS: { target: "LoggedIn.Settings" } }
                },
                Settings: {
                    on: { GO_DASHBOARD: { target: "LoggedIn.Dashboard" } }
                }
            }
        }
    }
}

If you evaluate this configuration:

  • Triggering LOGIN from LoggedOut will execute welcome:user and land automatically the LoggedIn.Dashboard leaf state.
  • Triggering GO_SETTINGS from LoggedIn.Dashboard will transition cleanly to LoggedIn.Settings.
  • Triggering LOGOUT from either LoggedIn.Dashboard or LoggedIn.Settings will bubble up, executing goodbye:user, and land safely back at LoggedOut.

Note: Our engine supports both fully nested states objects as shown here, as well as flat-map structures referencing a string parent property.

History States

Sometimes you want to return to the child state you were last in rather than the default initial state. You can transition to [ParentState].$history.

// From a "Paused" state inside a media player
on: {
    RESUME: { target: "Playing.$history" } // Returns to "Playing.Song" or "Playing.Podcast"
}

Note: The history mapping is maintained automatically by the execute function and the Service wrapper.

Parallel Regions

Orthogonal states (e.g., controlling background music and air conditioning simultaneously) can be managed with executeParallel.

import { executeParallel, ParallelRegion } from "nano-statechart";

const regions: ParallelRegion<CarEvent, string>[] = [
    { definition: acMachine, state: "Off", context: undefined },
    { definition: musicMachine, state: "Stopped", context: undefined },
];

const result = executeParallel(regions, { type: "PLAY" });
// Result merges effects from all regions and updates local states.

API Reference

Types

  • MachineEvent<Type>: Base event type ensuring your events have a type string discriminant.
  • MachineDefinition<S, E, Fx, Ctx>: The main configuration object.
  • TransitionResult<S, Fx, Ctx>: What execute and service.send return: { next: string, effects: Fx[], context: Ctx, history: HistoryMap }.

Methods

  • createService(def, effectHandler?) Creates an imperative service.

    • send(event): Dispatches an event.
    • getState() / getContext() / getHistory(): Getters.
    • subscribe(listener): Listen for transitions. Returns an unsubscribe function.
  • execute(def, current, event, ctx, history?) The pure execution function. Calculates correct exit and entry paths up to the Least Common Ancestor, updates history, and merges effects.

  • getInitialState(def) Resolves the actual starting leaf-state.

  • executeParallel(regions, event) Fires the event into multiple parallel machines and merges the resulting states and effects.


License

MIT