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

@statewalker/fsm

v0.38.2

Published

HFSM Implementation

Readme

@statewalker/fsm

What it is: a small hierarchical state machine you can pause and resume

A tiny, zero-dependency hierarchical finite state machine (HFSM) for TypeScript. Declare a tree of nested states and event-driven transitions, attach behaviour to each state, and drive the machine with events. The entire machine — its stack of active states and the per-state data you record — can be dumped to a plain object and restored, so a running process can be paused, persisted, and resumed later.

Why it exists: event-driven flows outgrow flags and callbacks

Event-driven control flow — UI wizards, agent reasoning loops, connection/session lifecycles, long-running workflows — is painful to express with ad-hoc booleans and callbacks. It degrades into "am I in this phase yet? did that step already run? what must unwind on cancel?".

This package lets you write that flow declaratively as a nested state tree and attach behaviour per state. The engine owns the hard parts:

  • descending into sub-states (entering a composite state auto-enters its initial child),
  • bubbling an unhandled event up to the parent state,
  • running enter/exit hooks in the correct order (exit unwinds inner-to-outer),
  • serializing the whole machine for pause/resume.

It is deliberately small (a single bundled entry, zero runtime dependencies) so it can sit at the core of larger systems without pulling weight.

There are two layers — pick your altitude:

  • Engine (FsmProcess + FsmState) — the low-level machine you drive by hand with dispatch(event) and lifecycle hooks. Use it when you want full control.
  • Runner (startProcess) — an ergonomic wrapper that attaches per-state handlers via a single load callback and binds the machine into a shared context object. Most consumers use this.

How to use: install, then drive it with the runner or the engine

pnpm add @statewalker/fsm

No peer or runtime dependencies. The package is ESM-only and has a single entry point, @statewalker/fsm (dist/index.js with .d.ts types; the TypeScript sources are published in src/). It uses no DOM or Node-specific APIs, so it runs in browsers, Node.js and workers.

Everything is exported from the package root:

import { startProcess, FsmProcess, type FsmStateConfig } from "@statewalker/fsm";

The machine is a stack of active states

A FsmProcess keeps a stack of active states from the root down to the current leaf. Dispatching an event resolves a transition, unwinds the states that exit, and enters the states that become active — always leaving the stack resting on a leaf:

config (declarative)          runtime stack (one FsmProcess)
Player                        Player          ← root, always active
├─ Idle                       └─ Active       ← composite, entered on "play"
└─ Active (composite)            └─ Playing   ← leaf, the "current" state
   ├─ Playing
   └─ Paused         dispatch("pause"): exit Playing → enter Paused  (inside Active)
                     dispatch("stop") : neither Playing nor Active has a "stop" rule,
                                        so it bubbles to Player's "*"→Idle, unwinding
                                        Playing then Active

Transitions are [from, event, to] tuples. "" means initial (as from) or final (as to); "*" is a wildcard matching any state or any event. When the current leaf has no matching rule, it exits (its target is final) and the parent's rules are checked for the same event, and so on up the tree. That is how an event handled only by an outer state still fires from deep inside.

What each export is for

Configuration (the declarative vocabulary)

  • FsmStateConfig — one plain-object shape to declare an entire nested machine, so configs stay serializable, diffable, and toolable: a key, a list of [from, event, to] transition tuples, and optional nested states.
  • STATE_INITIAL / STATE_FINAL (""), STATE_ANY / EVENT_ANY ("*"), EVENT_EMPTY ("") — named sentinels that make the meaning of the empty string and the wildcard explicit at the call site (["", "start", "Active"] reads as "from initial"). The runtime values are shared; the names document intent.

Engine

  • FsmProcess — the running machine: it owns the active-state stack and the traversal algorithm. dispatch(event) advances the machine until it rests on a leaf and resolves to false once the machine has finished (true otherwise); shutdown(event?) unwinds every active state; state is the current leaf; onStateCreate(handler) is the primary extension point (fires once per state instance); dump() / restore(data) serialize and rehydrate it.
  • FsmState — a live node in the stack you hang behaviour on. It carries the onEnter / onExit (exit runs in reverse-registration, inner-to-outer) / onStateError lifecycle hooks and the dump / restore hooks for per-state data; key and parent locate it in the tree.
  • FsmStateDescriptor — the compiled form of a config subtree, since resolving a transition at runtime must be cheap: an indexed transition table with wildcard-fallback lookup (getTargetStateKey). You rarely touch it directly; FsmProcess builds it for you.
  • getStateTransitions(state) / isStateTransitionEnabled(process, event) — read-only queries over the graph for UIs, viewers, and dispatch guards ("which events are available here? will this event do anything?"). The first lists the currently-reachable [from, event, to] transitions (walking up the parent chain, nearest wins); the second is the boolean guard used internally before dispatching.
  • FsmBaseClass / bindMethods — the shared handler-registry substrate under both FsmProcess and FsmState (add/run/remove typed handler lists, sequential with error routing). Mostly internal; exported for subclassing.

Runner

  • startProcess / startFsmProcess — wiring onStateCreate + onEnter + a loader by hand is repetitive, so the runner does it once and binds the machine into a shared context: it creates the process, on each state entry calls load(stateKey, event) and installs the returned StageHandlers, and returns a ProcessHandle. startFsmProcess is an alias.
  • StageHandler — the contract for per-state behaviour, with its return type doing double duty: a function of context returning void (nothing), a cleanup function (→ onExit), or an async/sync generator (its yielded strings are dispatched as events).
  • ProcessHandle — the caller's remote control after startProcess returns: shutdown(), dump(...), restore(dump, ...).
  • KEY_DISPATCH / KEY_TERMINATE / KEY_STATES / KEY_EVENT — the context keys under which startProcess binds the dispatch fn, terminate fn, current state-stack, and last event, so handlers reach the machine through the shared context rather than closures over the process. The bound dispatch fn ignores events for which isStateTransitionEnabled returns false.

Debug / observability

  • setProcessPrinter / getProcessPrinter / getPrinter / preparePrinter (+ Printer, PrinterConfig) — readable, hierarchy-indented logging you can attach without touching handler code: build or attach a Printer that prefixes each line with the current nesting depth (and optional line numbers).
  • setProcessTracer / setStateTracer — see the machine's motion as a stream of enter/exit events: emit <state event="…"> on enter and </state> on exit, for a whole process or a single state.

Examples

1. Declare the machine — FsmStateConfig

const config: FsmStateConfig = {
  key: "Player",
  transitions: [
    ["", "", "Idle"],            // initial → Idle (eventless start)
    ["Idle", "play", "Active"],  // play → Active (which enters its initial child)
    ["*", "stop", "Idle"],       // from ANY state, "stop" → Idle
  ],
  states: [
    { key: "Idle" },
    {
      // A composite state: entering it descends into its initial child. The
      // initial rule uses "*" (any event) so descent works whatever event caused
      // the entry — here, "play".
      key: "Active",
      transitions: [
        ["", "*", "Playing"],           // initial child of Active
        ["Playing", "pause", "Paused"],
        ["Paused", "play", "Playing"],
      ],
      states: [{ key: "Playing" }, { key: "Paused" }],
    },
  ],
};

2. Run it — startProcess

Pass a load(stateKey, event) callback that returns the handlers for each state. A handler runs on enter; what it returns wires up the rest:

const context: Record<string, unknown> = {};
await startProcess(context, config, (stateKey) => {
  if (stateKey === "Playing") {
    return [(ctx) => {
      console.log("▶ playing");
      return () => console.log("⏸ left Playing"); // returned fn → onExit cleanup
    }];
  }
  return [];
});

// startProcess binds control functions into `context`:
const dispatch = context["fsm:dispatch"] as (e: string) => Promise<void>;
await dispatch("play");   // Idle → Active → Playing → logs "▶ playing"
await dispatch("pause");  // Playing → Paused        → logs "⏸ left Playing"

A handler may instead return an async generator — each string it yields is dispatched back into the machine, which is how a state drives its own timed/reactive transitions:

// while in "Playing", emit a "pause" event after 1s:
return [async function* (ctx) {
  await new Promise((r) => setTimeout(r, 1000));
  yield "pause";
}];

3. Or drive the engine directly — FsmProcess

const process = new FsmProcess(config);
process.onStateCreate((state) => {
  state.onEnter(() => console.log("→", state.key));
  state.onExit(() => console.log("←", state.key)); // runs inner-to-outer
});
await process.dispatch("");      // enter the initial state (Player → Idle)
await process.dispatch("play");  // Idle → Active → Playing

4. Pause & resume — dump / restore

const snapshot = await process.dump();     // plain JSON-safe object
// ...persist it, ship it elsewhere, then on a fresh process:
await new FsmProcess(config).restore(snapshot);

To record your own per-state data in the snapshot, register dump/restore hooks on the state (they receive a mutable data bag):

state.dump((s, data) => { data.scrollTop = readScroll(); });
state.restore((s, data) => { applyScroll(data.scrollTop); });

5. Observe — printer & tracer

import { setProcessPrinter, setProcessTracer } from "@statewalker/fsm";

setProcessPrinter(process, { prefix: "[player]", lineNumbers: true });
setProcessTracer(process); // logs <Playing event="play"> … </Playing> <!-- event="pause" --> per state

Internals: traversal, transition lookup and serialization

  • Traversal & the status bitmask. dispatch pumps an enter/exit cycle over the state stack until it rests on a leaf (STATUS_LEAF) or the machine finishes (STATUS_FINISHED). The STATUS_* bits encode where in that cycle the process is: FIRST/NEXT while entering (descending to a first child vs. advancing to a target), LEAF when settled, LAST when popping to a parent. Dispatching while the machine is already running appends the event to the nextEvents FIFO queue and returns; the queued events are applied in order when the current run settles. Runs never nest.
  • Transition resolution order. For (state, event) the descriptor tries, in order: (state, event) → (*, event) → (state, *) → (*, *), then falls back to STATE_FINAL. Unhandled events bubble up the parent chain, so an outer state can catch events its children ignore.
  • Generator handlers. A handler returning an async generator runs concurrently; its yielded events feed back through dispatch, and the generator is return()-ed automatically when its state exits — this is the mechanism for self-driving/timed states.
  • Serialization. dump() walks root→leaf producing { status, event, stack: [{ key, data }] }; restore() rebuilds the stack and replays each state's restore hooks. Only what you record via state.dump(...) is persisted — the engine keeps snapshots minimal.
  • Constraints and quiet failures.
    • Once the root state exits, the process is finished: dispatch resolves to false and does nothing. Create a new FsmProcess (or restore a dump) to run again.
    • The dispatch function that startProcess binds under KEY_DISPATCH drops events for which isStateTransitionEnabled is false, without an error. Call isStateTransitionEnabled yourself when you need to know.
    • An error thrown by a handler does not reject dispatch. It goes to the state's onStateError handlers, then to the process's onStateError handlers, and is then logged with console.error; the transition continues.
    • dump / restore hooks receive the state's data bag as the second argument, although the FsmStateDumpHandler type names that parameter FsmStateDump.
  • Dependencies. None. Zero runtime dependencies, so the package can sit at the core of larger systems without adding to their install.

License

MIT.