@tonytoolkit/fsm-lib (1.0.0)
Installation
@tonytoolkit:registry=npm install @tonytoolkit/fsm-lib@1.0.0"@tonytoolkit/fsm-lib": "1.0.0"About this package
@tonytoolkit/fsm-lib
@tonytoolkit/fsm-lib — лёгкая библиотека конечного автомата (FSM).
Она даёт типизированную машину состояний с явными переходами, guard-условиями и async lifecycle-хуками.
Установка
Пакет лежит в npm registry Forgejo пользователя anton. Один раз настройте scope и (при необходимости) токен:
npm config set @tonytoolkit:registry https://git.serverbox.dev/api/packages/anton/npm/
# для приватного инстанса: Personal Access Token с правами на пакеты
# npm config set //git.serverbox.dev/api/packages/anton/npm/:_authToken=YOUR_TOKEN
npm install @tonytoolkit/fsm-lib
Публикация (локально)
После авторизации тем же PAT:
npm publish
Реестр задаётся в package.json → publishConfig.registry.
Публикация из CI
После пуша git-тега вида v1.2.3 workflow CI сначала гоняет тесты, затем выполняет npm publish (нужен секрет NPM_PUBLISH_TOKEN в настройках репозитория на Forgejo).
Импорт
Рекомендуемый barrel-импорт (удобен для Cocos Creator):
import {StateMachine, createTransition} from '@tonytoolkit/fsm-lib/fsm/index';
import type {IStateMachineContext, IStateMachine, IState} from '@tonytoolkit/fsm-lib/fsm/index';
import {AbstractState} from '@tonytoolkit/fsm-lib/fsm/AbstractState';
Также доступны subpath-импорты:
import {StateMachine} from '@tonytoolkit/fsm-lib/fsm/StateMachine';
import {createTransition} from '@tonytoolkit/fsm-lib/fsm/Transition';
import type {StateMachineOptions} from '@tonytoolkit/fsm-lib/fsm/StateMachineOptions';
Ключевые сущности
StateMachine
Центральный объект. Хранит:
- контекст — общие данные, доступные всем состояниям;
- states — зарегистрированные состояния;
- transitions — допустимые переходы между состояниями;
- currentStateId — текущее активное состояние.
Основные методы:
| Метод | Назначение |
|---|---|
start() |
Активирует initialStateId. Можно вызвать только один раз. |
process() |
Вызывает process() текущего состояния. |
transition(to) |
Явный переход в состояние to, если есть разрешённый Transition. |
next() |
Переход по первому подходящему исходящему Transition (с учётом guard и порядка в массиве). |
getContext() |
Возвращает контекст машины. |
getCurrentStateId() |
ID текущего состояния или null, если машина ещё не запущена. |
IState / AbstractState
Состояние — объект с lifecycle-хуками:
| Хук | Когда вызывается |
|---|---|
beforeEnter |
Перед входом в состояние |
afterEnter |
После входа, когда состояние уже активно |
process |
Основная логика состояния |
beforeExit |
Перед выходом из состояния |
afterExit |
После выхода из состояния |
Наследуйте AbstractState<TContext> и переопределяйте только нужные методы.
IStateMachineContext
Базовый контракт контекста. Минимально требует get(key) и set(key, value).
На практике контекст расширяют своими полями: очередь задач, флаги, сервисы, getter-ы и т.д.
interface CounterContext extends IStateMachineContext {
count: number;
inbox: Array<{type: string}>;
get<T>(key: string): T;
set<T>(key: string, value: T): void;
}
Transition и createTransition
Переход описывает допустимый путь from → to.
createTransition('idle', 'processing');
createTransition('processing', 'done', (context) => context.items.length === 0);
- Если
guardне задан, переход всегда разрешён. - Если
guardвозвращаетfalse, переход блокируется. next()выбирает первый подходящий переход из массиваtransitions— порядок записей важен.
GuardCondition
Функция (context, machine) => boolean, которая решает, можно ли выполнить переход.
Guard получает и контекст, и саму машину — это удобно для проверки currentAction, inbox, флагов busy/idle и т.п.
Жизненный цикл
stateDiagram-v2
[*] --> StateA: start()
StateA --> StateB: transition("StateB") / next()
note right of StateA
enter:
beforeEnter → afterEnter → process
exit:
beforeExit → afterExit
end note
При входе в состояние машина выполняет:
beforeEnter- установку
currentStateId afterEnterprocess— автоматически, без отдельного вызова
При выходе:
beforeExitafterExit
После этого можно снова вызывать machine.process() извне, если нужен повторный проход текущего состояния.
Быстрый старт
Минимальный пример
import {StateMachine, createTransition} from '@tonytoolkit/fsm-lib/fsm/index';
import type {IStateMachineContext, IStateMachine} from '@tonytoolkit/fsm-lib/fsm/index';
import {AbstractState} from '@tonytoolkit/fsm-lib/fsm/AbstractState';
interface CounterContext extends IStateMachineContext {
count: number;
get<T>(key: string): T;
set<T>(key: string, value: T): void;
}
class IdleState extends AbstractState<CounterContext> {
public async process(machine: IStateMachine<CounterContext>): Promise<void> {
const context = machine.getContext();
if (context.count > 0) {
await machine.next();
}
}
}
class IncrementState extends AbstractState<CounterContext> {
public async process(machine: IStateMachine<CounterContext>): Promise<void> {
machine.getContext().count += 1;
await machine.next();
}
}
const context: CounterContext = {
count: 0,
get(key) {
return (this as CounterContext)[key as keyof CounterContext] as never;
},
set(key, value) {
(this as Record<string, unknown>)[key] = value;
},
};
const machine = new StateMachine<CounterContext>({
context,
initialStateId: 'idle',
states: {
idle: new IdleState(),
increment: new IncrementState(),
},
transitions: [
createTransition('idle', 'increment', (ctx) => ctx.count < 3),
createTransition('increment', 'idle'),
],
});
await machine.start(); // idle → beforeEnter → afterEnter → process
// count = 0, guard разрешает переход idle → increment
await machine.process(); // increment → process → next() → idle
Переходы с guard-условиями
const machine = new StateMachine<OrderContext>({
context,
initialStateId: 'IdleState',
states: {
IdleState: new IdleState(),
PayState: new PayState(),
ShipState: new ShipState(),
},
transitions: [
createTransition('IdleState', 'PayState', (ctx) => ctx.inbox.length > 0),
createTransition('PayState', 'ShipState', (ctx) => ctx.isPaid),
createTransition('ShipState', 'IdleState'),
],
});
Если из одного состояния возможны несколько исходов, порядок transitions определяет, какой путь выберет next():
transitions: [
// Сначала проверится этот переход
createTransition('IdleState', 'UnsubscribeState', (ctx) => ctx.action === 'unsubscribe'),
// Потом этот
createTransition('IdleState', 'SubscribeState', (ctx) => ctx.action === 'subscribe'),
// И только затем fallback без guard
createTransition('IdleState', 'ProcessEventState', (ctx) => ctx.action === 'event'),
],
Паттерн «inbox + idle loop»
Типичный паттерн для сервисов: состояния обрабатывают очередь задач и сами инициируют следующий шаг pipeline.
class IdleState extends AbstractState<ServiceContext> {
public async process(machine: IStateMachine<ServiceContext>): Promise<void> {
const context = machine.getContext();
if (context.inbox.length === 0) {
context.isBusy = false;
context.currentAction = null;
return;
}
context.currentAction = context.inbox.shift();
context.isBusy = true;
await machine.next();
}
}
class WorkerState extends AbstractState<ServiceContext> {
public async process(machine: IStateMachine<ServiceContext>): Promise<void> {
// ... выполняем currentAction
await machine.next(); // возвращаемся в idle или идём дальше по pipeline
}
}
Внешний код кладёт задачи в inbox и вызывает machine.process(), только если машина не занята.
Рекомендации
- Один класс — одно состояние. Не смешивайте unrelated-логику в одном
process. - Контекст — единственный shared state. Не храните business-data в static-полях состояний.
- Порядок
transitionsважен дляnext(). Более специфичные guard-переходы ставьте выше fallback-ов. - Используйте
transition(to), когда целевое состояние известно заранее;next()— когда достаточно «первого подходящего» перехода. - Учитывайте auto-
processпри входе. Послеstart()и каждого перехода текущее состояние уже получит один вызовprocess. - Async-safe. Все lifecycle-хуки могут возвращать
Promise;StateMachineих await-ит.
Публичный API
| Экспорт | Описание |
|---|---|
StateMachine |
Реализация FSM |
createTransition |
Фабрика переходов |
AbstractState |
Базовый класс состояния |
IState |
Контракт состояния |
IStateMachine |
Контракт машины, доступный из состояний |
IStateMachineContext |
Базовый контракт контекста |
GuardCondition |
Тип guard-функции |
Transition |
Тип описания перехода |
StateMachineOptions |
Тип опций конструктора |
Дополнительно в репозитории есть IStorage — минимальный key-value контракт, совместимый с localStorage. Он не re-export-ится из barrel, но доступен через subpath @tonytoolkit/fsm-lib/fsm/IStorage.
Разработка
npm install
npm run build
npm test
npm run lint
Лицензия
PERSONAL — внутренний пакет.
Dependencies
Development dependencies
| ID | Version |
|---|---|
| @types/jest | ^29.2.2 |
| @types/node | ^14.11.2 |
| jest | ^29.2.2 |
| ts-jest | ^29.0.3 |
| ts-node | ^10.9.1 |
| typescript | ^5.7.3 |