@edium/fsm

中文文档
3.0.2发布于 2026/04/23MITTypeScriptESM + CJS近 30 天下载 2.9万

Simple finite state machine framework that can be used with client or server.

原始使用文档
文档目录16 节
  1. Overview
  2. Features
  3. Migration to 3.x
  4. If you used version 2.x (or earlier)
  5. To use the new async FSM
  6. Installation
  7. Tests & Coverage
  8. Importing
  9. Example
  10. Importing
  11. Example
  12. 1. State Guards (Blocking Transitions)
  13. 2. Global Transitions
  14. 3. Sub-State Machines (Hierarchical Composition)
  15. 4. Async Workflows
  16. 5. Error Handling

Edium Finite State Machine

Overview

Edium FSM is a lightweight, flexible finite state machine written in TypeScript. It works in both the browser and Node.js, supports local and global transitions, entry/exit/decide actions, blocked transitions, and optional context objects passed to all state actions.

Version 3.x introduces a fully asynchronous state machine, useful for client/server apps, workflow systems, async game logic, and anything requiring await inside state actions.

IMPORTANT Version 3.x is 100% backward compatible with earlier versions. The original synchronous API is unchanged — the new async API is optional.


Features

  • Unlimited number of states.
  • One or more completed states.
  • Optional “go to previous state” behavior.
  • Throws errors on invalid transitions.
  • Entry, exit, and decide actions.
  • Exit actions can block transitions.
  • Reset and optional restart behavior.
  • Local transitions (state-specific).
  • Global transitions (bypass state rules).
  • Trigger-driven state changes.
  • Optional context object passed to all actions.
  • Async FSM option for await-driven logic.

Migration to 3.x

Version 3.x adds asynchronous FSM classes (AsyncState, AsyncStateMachine, AsyncTransition) without changing the existing synchronous API.

If you used version 2.x (or earlier)

You don’t need to change anything:

import { State, StateMachine } from '@edium/fsm';

const machine = new StateMachine('My FSM');
machine.start(...);
machine.trigger('next');

This continues to work exactly as before.

To use the new async FSM

Switch to:

import { AsyncState, AsyncStateMachine } from '@edium/fsm';

await asyncMachine.start(...);
await asyncMachine.trigger('next');

Both styles can coexist in the same project.


Installation

pnpm install @edium/fsm

Tests & Coverage

The codebase is fully unit-tested with near-100% coverage. All code is linted, prettified and type-checked.


Synchronous Machine

Importing

import { State, StateMachine } from '@edium/fsm';
const { State, StateMachine } = require('@edium/fsm');

Example

const entryAction = (state, context) => {
  state.trigger('next');
};

const exitAction = (state, context) => {
  return true;
};

const decideAction = (state, context) => {
  const index = context.randomize();
  if (index === 0) {
    state.trigger('gotoThree');
  } else if (index === 1) {
    state.trigger('gotoFour');
  }
};

const finalAction = (state) => {};

const context = {
  randomize: () => Math.floor(Math.random() * 2)
};

const stateMachine = new StateMachine('My first state machine', context);

const s1 = stateMachine.createState('My first state', false, entryAction);
const s2 = stateMachine.createState('My second state', false, decideAction, exitAction);
const s3 = stateMachine.createState('My third state', false, entryAction);
const s4 = stateMachine.createState('My fourth state', false, entryAction);
const s5 = stateMachine.createState('My fifth and final state', true, finalAction);

s1.addTransition('next', s2);
s2.addTransition('gotoThree', s3);
s2.addTransition('gotoFour', s4);
s3.addTransition('next', s5);
s4.addTransition('next', s5);

stateMachine.start(s1);

Asynchronous Machine

This example implements a safe asynchronous state machine that does not allow the user to directly change the state during running work.

IMPORTANT Do not call asyncStateMachine.trigger() from inside entry/exit actions. Instead you must use state.triggerInternal(). Internal triggers are queued and processed safely once the current transition finishes, whereas external triggers will generate an error.

Importing

import { AsyncState, AsyncStateMachine } from '@edium/fsm';
const { AsyncState, AsyncStateMachine } = require('@edium/fsm');

Example

const entryAction = async (state, context) => {
  // Safe: internal triggers are queued if the machine is already busy.
  await state.triggerInternal('next');
};

const exitAction = async (state, context) => {
  return true;
};

const decideAction = async (state, context) => {
  const index = context.randomize();
  if (index === 0) {
    await state.triggerInternal('gotoThree');
  } else if (index === 1) {
    await state.triggerInternal('gotoFour');
  }
};

const finalAction = async (state) => {};

const context = {
  randomize: () => Math.floor(Math.random() * 2)
};

const asyncStateMachine = new AsyncStateMachine('My first async state machine', context);

const s1 = asyncStateMachine.createState('My first state', false, entryAction);
const s2 = asyncStateMachine.createState('My second state', false, decideAction, exitAction);
const s3 = asyncStateMachine.createState('My third state', false, entryAction);
const s4 = asyncStateMachine.createState('My fourth state', false, entryAction);
const s5 = asyncStateMachine.createState('My fifth and final state', true, finalAction);

s1.addTransition('next', s2);
s2.addTransition('gotoThree', s3);
s2.addTransition('gotoFour', s4);
s3.addTransition('next', s5);
s4.addTransition('next', s5);

await asyncStateMachine.start(s1);

Common Patterns

1. State Guards (Blocking Transitions)

const guardedExit = (state, ctx) => {
  return ctx.isAllowed === true;
};

2. Global Transitions

machine.addGlobalTransition('reset', startState);
machine.trigger('reset');

3. Sub-State Machines (Hierarchical Composition)

const sub = new StateMachine('sub');
const parent = new StateMachine('parent');

parent.createState('run-sub', false, () => {
  sub.start();
});

4. Async Workflows

const entry = async (s, ctx) => {
  const data = await fetch('/api/data').then((r) => r.json());
  ctx.data = data;
  // Safe: internal triggers are queued if the machine is already busy.
  await s.triggerInternal('next');
};

5. Error Handling

All internal errors are thrown as StateMachineError:

  • error.message – Prefixed message: State Machine (My Machine) - …
  • error.machine – State machine name
  • error.state – Current state name (if any)
  • error.trigger – Trigger id (if any)

You can instanceof StateMachineError to distinguish FSM errors from other errors.


Created by Edium Interactive LLC.

版本与发布记录

@edium/fsm 共发布 21 个版本,首次发布于 2019/07/22;过去 12 个月发布了 3 个版本。

版本发布日期距今
3.0.2latest2026/04/236 个月前
3.0.12025/11/2611 个月前
3.0.02025/11/2611 个月前
2.1.52024/07/052 年前
2.1.42022/11/153 年前
2.1.32022/06/234 年前

关于 @edium/fsm 的常见问题

@edium/fsm 是什么?

Simple finite state machine framework that can be used with client or server.

如何安装 @edium/fsm?

在项目目录运行 npm install @edium/fsm,或使用 pnpm add / yarn add / bun add。

@edium/fsm 支持 TypeScript 吗?

支持。包内自带 TypeScript 类型声明(package.json 声明了 types 入口),安装后即可获得类型提示,无需再安装 @types 包。

@edium/fsm 支持 ESM 还是 CommonJS?

同时提供 ES Module 与 CommonJS 入口,import 和 require() 都可以使用,打包工具会自动选择合适的版本。

@edium/fsm 需要什么版本的 Node.js?

package.json 的 engines 字段要求 Node.js >=18。版本不符时 npm 默认只给出警告,开启 engine-strict 后会拒绝安装。

@edium/fsm 可以商用吗?

@edium/fsm 使用 MIT 许可。宽松许可:允许商业使用、修改和再分发,通常只需在分发时保留原版权与许可声明。以上为概括,不构成法律意见,请以许可原文为准。

@edium/fsm 的下载量有多少?

根据 npm 官方下载统计,@edium/fsm 在 2026/10/13 – 2026/11/11 这 30 天内被下载 28,771 次,日均约 959 次。下载量会把 CI 构建、镜像同步和作为其他包依赖的安装一并计入,适合横向比较热度,不能直接等同于使用人数。

@edium/fsm 还在维护吗?

最新版本 3.0.2 发布于 2026/04/23(6 个月前);至今共发布 21 个版本,其中过去 12 个月发布了 3 个;首次发布于 2019/07/22。发布频率只是参考信号:功能稳定的工具可能很少需要更新,建议结合源码仓库的 issue 和提交记录判断。