Ручная отмена эффектов
Помимо type: "latest" и автоматического игнорирования устаревших transition-вызовов, lite-fsm позволяет задать собственное правило блокировки через cancelFn. Используйте cancelFn, когда актуальность effect зависит не только от номера запуска.
Механизм ручной отмены
Ручная отмена не прерывает async-код сама по себе. Она задаёт predicate, который проверяется перед каждым transition внутри effect.
Пример использования
В следующем примере показано, как реализовать ручную отмену эффекта на основе изменения значения в контексте:
import { createEffect, createMachine } from "@lite-fsm/core";
export const streams = createMachine({
config: {
IDLE: {
DO_LOAD_STREAMS: "LOAD_STREAMS_PENDING",
LOAD_STREAMS_RESOLVE: "IDLE",
LOAD_STREAMS_REJECT: "IDLE",
},
LOAD_STREAMS_PENDING: {
LOAD_STREAMS_STARTED: "IDLE",
},
},
initialState: "IDLE",
initialContext: {},
effects: {
LOAD_STREAMS_PENDING: createEffect({
type: "latest",
// Функция отмены, которая проверяет условие перед каждым вызовом transition
cancelFn: ({ getState }) => {
const startSession = getState().player.context.session.sessionId;
return () => {
const currentSession = getState().player.context.session.sessionId;
// Если сессия изменилась — transition-вызовы будут игнорироваться
return startSession !== currentSession;
};
},
effect: async ({ transition, api }) => {
transition({ type: "LOAD_STREAMS_STARTED" });
try {
// Сетевой запрос в любом случае выполнится до конца
await api.loadStreams();
transition({ type: "LOAD_STREAMS_RESOLVE" });
} catch (error) {
transition({
type: "LOAD_STREAMS_REJECT",
payload: { error },
});
}
},
}),
},
});Механизм работы отмены
Функция cancelFn возвращает проверку, которая вызывается перед каждым вызовом transition в эффекте:
- Инициализация: При первом запуске эффекта вызывается
cancelFn, которая сохраняет начальное состояние и возвращает функцию проверки - Выполнение эффекта продолжается: Важно понимать, что сам код эффекта продолжает выполняться даже при отмене
- Проверка перед transition: Перед каждым вызовом
transitionвнутри эффекта, система вызывает функцию проверки отмены - Игнорирование transition при отмене: Если функция проверки возвращает
true, вызовtransitionигнорируется, и машина состояний не получает событие
Таким образом, cancelFn не прерывает выполнение effect, а блокирует его влияние на состояние машины.
Сложные сценарии отмены
Отмена по внешнему сигналу
import { MachineManager, createEffect, createMachine } from "@lite-fsm/core";
import { immerMiddleware } from "@lite-fsm/middleware/immer";
const createSearchController = () => ({
controller: new AbortController(),
reset() {
this.controller.abort();
this.controller = new AbortController();
return this.controller;
},
});
export const search = createMachine({
config: {
IDLE: {
SEARCH: "SEARCH_PENDING",
},
SEARCH_PENDING: {
SEARCH_STARTED: "SEARCHING",
},
SEARCHING: {
SEARCH: "SEARCH_PENDING",
SEARCH_RESOLVE: "RESULTS",
SEARCH_REJECT: "ERROR",
},
RESULTS: {
SEARCH: "SEARCH_PENDING",
},
ERROR: {
SEARCH: "SEARCH_PENDING",
},
},
initialState: "IDLE",
initialContext: {
query: "",
results: null as unknown[] | null,
error: null as Error | null,
},
reducer: (state, action, { nextState }) => {
state.state = nextState;
switch (action.type) {
case "SEARCH":
state.context.query = action.payload.query;
break;
case "SEARCH_RESOLVE":
state.context.results = action.payload.results;
state.context.error = null;
break;
case "SEARCH_REJECT":
state.context.error = action.payload.error;
state.context.results = null;
break;
}
},
effects: {
SEARCH_PENDING: ({ transition }) => {
transition({ type: "SEARCH_STARTED" });
},
SEARCHING: createEffect({
cancelFn: ({ searchController }) => {
const controller = searchController.controller;
return () => controller.signal.aborted;
},
effect: async ({ transition, api, searchController, getState }) => {
const { query } = getState().search.context;
const controller = searchController.reset();
try {
const results = await api.search(query, { signal: controller.signal });
transition({ type: "SEARCH_RESOLVE", payload: { results } });
} catch (err) {
if (err instanceof Error && err.name === "AbortError") return;
transition({ type: "SEARCH_REJECT", payload: { error: err as Error } });
}
},
}),
},
});
const manager = MachineManager({ search }, { middleware: [immerMiddleware] });
manager.setDependencies({
api: apiService,
searchController: createSearchController(),
getState: manager.getState,
});
manager.transition({ type: "SEARCH", payload: { query: "первый" } });
// Предыдущий запрос будет автоматически отменён через AbortController
manager.transition({ type: "SEARCH", payload: { query: "второй" } });Лучшие практики
- Чётко определите условие блокировки: условие должно быть детерминированным и основанным на состоянии автомата или явной зависимости.
- Помните, что код эффекта продолжает выполняться: Отмена эффекта блокирует только вызовы
transition, но сам код продолжает выполняться. - Используйте AbortController для фактической отмены операций: поскольку код эффекта продолжает выполняться, используйте AbortController для отмены HTTP-запросов и других ресурсоёмких операций.
- Сочетайте блокировку transition и отмену операции: при необходимости комбинируйте
cancelFnиAbortController. - Избегайте побочных эффектов в функции проверки: функция, возвращаемая из
cancelFn, должна проверять условие, а не изменять состояние. - Документируйте условия отмены: Явно указывайте, при каких условиях будут игнорироваться transition-вызовы.
Last updated on