Параллельные автоматы
Параллельные автоматы позволяют разделить процесс на несколько независимых машин, каждая из которых отвечает за свою часть состояния.
Примеры reducer-ов на этой странице написаны в стиле Immer: они мутируют draft
stateи предполагают, чтоMachineManagerсоздан сimmerMiddleware. Без middleware reducer должен вернуть новый объект{ state, context }.
Концепция параллельных автоматов
В приложении часто есть несколько процессов, которые живут рядом: авторизация, профиль, загрузка данных, уведомления. В lite-fsm такие процессы можно описать отдельными автоматами и координировать через общий MachineManager.
В такой модели автоматы:
- Работают независимо друг от друга
- Каждый имеет собственное состояние и контекст
- Могут реагировать на одни и те же события
- Общаются через события, обрабатываемые менеджером
Пример использования
Рассмотрим пример медиаплеера с параллельными автоматами, где каждый автомат обрабатывает свою часть функциональности:
import { createMachine } from "@lite-fsm/core";
// Автомат-координатор: ждёт завершения двух соседей
export const player = createMachine({
config: {
IDLE: {
CHANGE_TRACK: "LOAD_TRACK_DATA_PENDING",
},
LOAD_TRACK_DATA_PENDING: {
LOAD_TRACK_DATA_RESOLVE: "SELECT_STREAM_PENDING",
},
SELECT_STREAM_PENDING: {
SELECT_STREAM_RESOLVE: "SET_STREAM_PENDING",
},
SET_STREAM_PENDING: {},
},
initialState: "IDLE",
initialContext: {},
reducer: (state, _action, { nextState }) => {
state.state = nextState;
},
effects: {
LOAD_TRACK_DATA_PENDING: async ({ condition, transition }) => {
await Promise.all([
condition((e) => e.type === "LOAD_TRACK_META_RESOLVE"),
condition((e) => e.type === "LOAD_STREAMS_RESOLVE"),
]);
transition({ type: "LOAD_TRACK_DATA_RESOLVE" });
},
},
});
export const trackMeta = createMachine({
config: {
IDLE: {
CHANGE_TRACK: "LOAD_TRACK_META_PENDING",
},
LOAD_TRACK_META_PENDING: {
LOAD_TRACK_META_RESOLVE: "IDLE",
},
},
initialState: "IDLE",
initialContext: {},
reducer: (state, _action, { nextState }) => {
state.state = nextState;
},
effects: {
LOAD_TRACK_META_PENDING: async ({ transition }) => {
transition({ type: "LOAD_TRACK_META_RESOLVE" });
},
},
});
export const streams = createMachine({
config: {
IDLE: {
CHANGE_TRACK: "LOAD_STREAMS_PENDING",
},
LOAD_STREAMS_PENDING: {
LOAD_STREAMS_RESOLVE: "IDLE",
},
},
initialState: "IDLE",
initialContext: {},
reducer: (state, _action, { nextState }) => {
state.state = nextState;
},
effects: {
LOAD_STREAMS_PENDING: async ({ transition }) => {
transition({ type: "LOAD_STREAMS_RESOLVE" });
},
},
});
// Событие распространяется во все автоматы, зарегистрированные в менеджере
manager.transition({ type: "CHANGE_TRACK", payload: { trackId: "123" } });В этом примере все три автомата независимо реагируют на событие CHANGE_TRACK, а player ожидает завершения работы двух других автоматов через механизм condition.
Правила взаимодействия автоматов
- Один action — несколько обработчиков: каждый автомат независимо проверяет, может ли он обработать событие.
- Независимые состояния: Каждый автомат имеет свое собственное состояние и контекст.
- Коммуникация через события: Автоматы взаимодействуют, отправляя события, которые могут быть обработаны другими автоматами.
- Единая точка изменения состояния:
MachineManagerхранит состояние зарегистрированных автоматов и применяет переходы в одном dispatch.
Структурирование параллельных автоматов
Практики при работе с параллельными автоматами:
1. Чёткое разделение ответственности
Каждый автомат должен отвечать за конкретную область:
const auth = createMachine({
config: {
UNAUTHENTICATED: {
LOGIN: "AUTHENTICATING",
},
AUTHENTICATING: {
LOGIN_RESOLVE: "AUTHENTICATED",
LOGIN_REJECT: "UNAUTHENTICATED",
},
AUTHENTICATED: {
LOGOUT: "UNAUTHENTICATED",
},
},
initialState: "UNAUTHENTICATED",
initialContext: {},
});
const profile = createMachine({
config: {
IDLE: {
LOGIN_RESOLVE: "LOADING_PROFILE",
UPDATE_PROFILE: "UPDATING_PROFILE",
},
LOADING_PROFILE: {
PROFILE_LOADED: "READY",
PROFILE_LOAD_REJECT: "ERROR",
},
READY: {
UPDATE_PROFILE: "UPDATING_PROFILE",
},
UPDATING_PROFILE: {
PROFILE_UPDATED: "READY",
PROFILE_UPDATE_REJECT: "ERROR",
},
ERROR: {
RETRY: "LOADING_PROFILE",
},
},
initialState: "IDLE",
initialContext: {},
});2. Согласованные имена событий
Используйте согласованные имена событий, чтобы взаимодействие между автоматами было читаемым:
// В auth
effects: {
AUTHENTICATING: async ({ transition, api }) => {
try {
const user = await api.login(credentials);
transition({
type: "LOGIN_RESOLVE", // Это событие также обрабатывается profile
payload: { user },
});
} catch (error) {
transition({ type: "LOGIN_REJECT", payload: { error } });
}
},
}3. Координация через события
Если один процесс должен реагировать на другой, это можно выразить через общие события. Это не встроенная иерархия statecharts, а композиционный паттерн: автоматы остаются плоскими, но согласованно реагируют на события домена.
const app = createMachine({
config: {
INITIALIZING: {
APP_READY: "RUNNING",
},
RUNNING: {},
},
initialState: "INITIALIZING",
initialContext: {},
effects: {
INITIALIZING: async ({ transition }) => {
transition({ type: "APP_READY" });
},
},
});
const auth = createMachine({
config: {
IDLE: {
APP_READY: "WAITING_FOR_AUTH",
FATAL_ERROR: "DISABLED",
},
WAITING_FOR_AUTH: {
LOGIN: "AUTHENTICATING",
},
AUTHENTICATING: {
LOGIN_RESOLVE: "AUTHENTICATED",
LOGIN_REJECT: "WAITING_FOR_AUTH",
},
AUTHENTICATED: {
LOGOUT: "WAITING_FOR_AUTH",
},
DISABLED: {},
},
initialState: "IDLE",
initialContext: {},
});
const profile = createMachine({
config: {
IDLE: {
APP_READY: "WAITING_FOR_LOGIN",
FATAL_ERROR: "DISABLED",
},
WAITING_FOR_LOGIN: {
LOGIN_RESOLVE: "LOADING_PROFILE",
},
LOADING_PROFILE: {
PROFILE_LOADED: "READY",
PROFILE_LOAD_REJECT: "ERROR",
},
READY: {},
ERROR: {},
DISABLED: {},
},
initialState: "IDLE",
initialContext: {},
});
const manager = MachineManager({ app, auth, profile });
// Это событие обработают и auth, и profile
manager.transition({ type: "APP_READY" });
manager.transition({ type: "LOGIN" });
// При успешном логине auth отправит LOGIN_RESOLVE,
// и profile автоматически перейдёт в LOADING_PROFILEВ этом примере:
- Автоматы auth и profile сами следят за состоянием родительского автомата через подписку на события APP_READY и FATAL_ERROR
- Автомат profile также реагирует на событие LOGIN_RESOLVE от auth автомата
- Все автоматы явно переходят в состояние DISABLED при критической ошибке
- При перезапуске приложения (APP_READY) автоматы возвращаются в рабочее состояние
Преимущества параллельных автоматов
- Модульность: каждый автомат можно разрабатывать, тестировать и поддерживать отдельно.
- Расширяемость: новый доменный процесс можно добавить как отдельную машину.
- Читаемость: каждая машина остаётся меньше и описывает одну область.
- Локальность изменений: изменения в одной машине реже затрагивают остальные.
- Переиспользование: автомат можно перенести в другой сценарий, если сохранить контракт событий и зависимостей.
Заключение
Параллельные автоматы помогают разделять ответственность между независимыми процессами. В lite-fsm их координация проходит через MachineManager, общие события и явно переданные dependencies.