Skip to Content
👋 Добро пожаловать в документацию lite-fsm!
РуководствоПродвинутые концепцииВложенные/параллельные автоматы

Параллельные автоматы

Параллельные автоматы позволяют разделить процесс на несколько независимых машин, каждая из которых отвечает за свою часть состояния.

Примеры 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.

Правила взаимодействия автоматов

  1. Один action — несколько обработчиков: каждый автомат независимо проверяет, может ли он обработать событие.
  2. Независимые состояния: Каждый автомат имеет свое собственное состояние и контекст.
  3. Коммуникация через события: Автоматы взаимодействуют, отправляя события, которые могут быть обработаны другими автоматами.
  4. Единая точка изменения состояния: 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) автоматы возвращаются в рабочее состояние

Преимущества параллельных автоматов

  1. Модульность: каждый автомат можно разрабатывать, тестировать и поддерживать отдельно.
  2. Расширяемость: новый доменный процесс можно добавить как отдельную машину.
  3. Читаемость: каждая машина остаётся меньше и описывает одну область.
  4. Локальность изменений: изменения в одной машине реже затрагивают остальные.
  5. Переиспользование: автомат можно перенести в другой сценарий, если сохранить контракт событий и зависимостей.

Заключение

Параллельные автоматы помогают разделять ответственность между независимыми процессами. В lite-fsm их координация проходит через MachineManager, общие события и явно переданные dependencies.

Last updated on