@tonytoolkit/fsm-lib (1.0.0)

Published 2026-05-27 17:57:02 +03:00 by anton

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.jsonpublishConfig.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

При входе в состояние машина выполняет:

  1. beforeEnter
  2. установку currentStateId
  3. afterEnter
  4. process — автоматически, без отдельного вызова

При выходе:

  1. beforeExit
  2. afterExit

После этого можно снова вызывать 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(), только если машина не занята.

Рекомендации

  1. Один класс — одно состояние. Не смешивайте unrelated-логику в одном process.
  2. Контекст — единственный shared state. Не храните business-data в static-полях состояний.
  3. Порядок transitions важен для next(). Более специфичные guard-переходы ставьте выше fallback-ов.
  4. Используйте transition(to), когда целевое состояние известно заранее; next() — когда достаточно «первого подходящего» перехода.
  5. Учитывайте auto-process при входе. После start() и каждого перехода текущее состояние уже получит один вызов process.
  6. 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

Keywords

FSM state machine
Details
npm
2026-05-27 17:57:02 +03:00
3
Anton Lapshin
PERSONAL
latest
14 KiB
Assets (1)
Versions (1) View all
1.0.0 2026-05-27