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

@duyquangnvx/state-machine

v0.1.2

Published

Generic, type-safe finite state machine library for TypeScript

Readme

state-machine

A generic, type-safe finite state machine library for TypeScript with synchronous lifecycle hooks, transition guards, hierarchical states, event history, and a tick-based update loop.

Features

  • Fully genericStateMachine<TContext, TStateId> works with any context and state ID types
  • Synchronous lifecycleonEnter / onExit / onUpdate are all sync for deterministic state at every moment
  • Transition guardscanTransitionTo(target, ctx) lets each state control allowed transitions
  • Tick-based updatesonUpdate(ctx, dt) runs every frame/tick; return a state ID to auto-transition
  • Hierarchical statesHierarchicalState embeds a nested state machine inside a parent state
  • Event system — Subscribe to state changes with on(listener), unsubscribe with the returned function
  • Bounded history — Configurable history buffer for debugging and replay

Quick start

npm install
npm run build

Define states

import { BaseState } from "state-machine";

type MyStateId = "idle" | "loading" | "ready";

interface MyContext {
  data: string | null;
}

class IdleState extends BaseState<MyContext, MyStateId> {
  readonly id = "idle" as const;

  override onEnter(ctx: MyContext, prevState: MyStateId | null): void {
    console.log("Entered idle");
  }

  override onUpdate(ctx: MyContext, dt: number): MyStateId | undefined {
    return "loading"; // auto-transition on next tick
  }
}

class LoadingState extends BaseState<MyContext, MyStateId> {
  readonly id = "loading" as const;

  override onUpdate(): MyStateId | undefined {
    return "ready";
  }
}

class ReadyState extends BaseState<MyContext, MyStateId> {
  readonly id = "ready" as const;
}

Create and run the machine

import { StateMachine } from "state-machine";

const sm = new StateMachine<MyContext, MyStateId>({
  states: [new IdleState(), new LoadingState(), new ReadyState()],
  initialState: "idle",
  context: { data: null },
  historySize: 50, // optional, defaults to 100
});

sm.start();            // enters "idle", calls onEnter
sm.update(0.016);      // calls onUpdate, may auto-transition
sm.transitionTo("ready"); // explicit transition
sm.stop();             // exits current state, calls onExit

Transition guards

Override canTransitionTo to block transitions conditionally:

class IdleState extends BaseState<MyContext, MyStateId> {
  readonly id = "idle" as const;

  override canTransitionTo(target: MyStateId, ctx: MyContext): boolean {
    return ctx.data !== null; // only allow transitions when data is loaded
  }
}

Blocked transitions throw TransitionDeniedError.

Events and history

const unsub = sm.on((event) => {
  console.log(`${event.from} -> ${event.to} at ${event.timestamp}`);
});

sm.getHistory(); // ReadonlyArray<StateChangeEvent<MyStateId>>
unsub();         // stop listening

Async work as a state

The state machine is fully synchronous — onEnter, onExit, and onUpdate all return void. This ensures the machine is always in exactly one definite state at any moment, which is critical for game loops and real-time systems.

To handle async operations (API calls, loading, etc.), model the async work as its own state that polls for completion:

type MyStateId = "IDLE" | "LOADING" | "READY";

interface MyContext {
  loading: boolean;
  data: string | null;
  fetchData: () => Promise<string>;
}

class LoadingState extends BaseState<MyContext, MyStateId> {
  readonly id = "LOADING" as const;

  override onEnter(ctx: MyContext): void {
    ctx.loading = true;
    ctx.fetchData().then((data) => {
      ctx.data = data;
      ctx.loading = false;
    });
  }

  override onUpdate(ctx: MyContext): MyStateId | undefined {
    if (!ctx.loading) return "READY";
    return undefined;
  }
}

This pattern keeps the state machine synchronous while still supporting async operations. The state machine ticks on each frame/update, and the loading state simply polls until the async work completes.

API

StateMachine<TContext, TStateId>

| Method / Property | Description | |---|---| | start(): void | Enter the initial state. Idempotent. | | stop(): void | Exit the current state and shut down. | | transitionTo(stateId): void | Transition to a specific state (checks guard). | | update(dt): void | Tick the current state; auto-transitions if onUpdate returns a state ID. | | on(listener) | Subscribe to state changes. Returns unsubscribe function. | | getHistory() | Get the bounded transition history. | | currentStateId | The active state's ID. Throws if not started. | | isStarted | Whether the machine is running. | | context | The shared mutable context object. |

BaseState<TContext, TStateId> (lifecycle hooks)

| Hook | Signature | Notes | |---|---|---| | canTransitionTo | (target, ctx) => boolean | Sync guard. Default: true. | | onEnter | (ctx, prevState) => void | Called when entering. prevState is null on start(). | | onUpdate | (ctx, dt) => TStateId \| undefined | Return a state ID to auto-transition. | | onExit | (ctx, nextState) => void | Called when leaving. nextState is null on stop(). |

HierarchicalState<TContext, TParentId, TChildId>

A composite state that runs a nested StateMachine. Override createChildConfig(ctx) to define the child machine. The child starts/stops automatically with the parent state.

Errors

| Error | Thrown when | |---|---| | StateNotFoundError | Transitioning to an unknown state ID. | | MachineNotStartedError | Calling transitionTo, update, or currentStateId before start(). | | TransitionDeniedError | canTransitionTo returns false. |

Demos

Tower Defense

Three interlocking state machines (tower, enemy, wave) simulating a tower defense game loop.

Tower:  BUILDING -> IDLE -> TARGETING -> ATTACKING -> IDLE
Enemy:  SPAWNING -> MOVING -> ATTACKING -> DYING -> DEAD
Wave:   PREPARING -> WAVE_ACTIVE -> WAVE_COMPLETE -> ... -> GAME_OVER
npm run build && npm run demo

Slot Machine

Demonstrates the "async work as a state" pattern — API calls (bet deduction, payout crediting) are modeled as dedicated states that poll for completion.

IDLE -> DEDUCTING_BET -> SPINNING -> STOPPING -> EVALUATING -> CREDITING_WIN -> IDLE
                                                     |
                                                     +--> IDLE (no win)
npm run build && npm run demo:slot

Testing

npm test

67 tests covering the core library (construction, start/stop, transitions, guards, updates, events, history, hierarchical states) and both demos.

Project structure

src/
  lib/                    # Core library
    State.ts              # BaseState abstract class
    StateMachine.ts       # StateMachine engine
    HierarchicalState.ts  # Composite state with nested machine
    StateEvent.ts         # Event emitter with bounded history
    interfaces.ts         # IState, IStateMachine, config types
    errors.ts             # StateNotFoundError, MachineNotStartedError, TransitionDeniedError
    index.ts              # Public exports
  demo/
    tower/                # Tower defense FSM (tower states)
    enemy/                # Tower defense FSM (enemy states)
    wave/                 # Tower defense FSM (wave management)
    slot/                 # Slot machine FSM
    main.ts               # Tower defense entry point
    slot-main.ts          # Slot machine entry point
    GameLoop.ts           # Tick-based game loop utility
tests/
  lib/                    # Core library tests
  demo/                   # Demo-specific tests

License

ISC