Skip to content

Узлы и зависимости

Workflow nodes образуют DAG — направленный граф без циклов.

Поля node

ts
interface IWorkflowNode<TValue = unknown> {
    id: string
    dependencies?: readonly string[]
    condition?: (context: IWorkflowNodeContext) => boolean | Promise<boolean>
    execute(context: IWorkflowNodeContext): Promise<TValue>
    compensate?(value: TValue, context: IWorkflowNodeContext): Promise<void>
}
ПолеОбязательноеЧто означает
idдаУникальное имя node и key результата.
dependenciesнетNodes, которые должны завершиться или быть skipped раньше.
conditionнетfalse пропускает node.
executeдаВыполняет шаг и возвращает result.
compensateнетОтменяет эффект успешного node после последующей ошибки.

Context node

ts
interface IWorkflowNodeContext {
    results: ReadonlyMap<string, unknown>
    signal: AbortSignal
}
  • results содержит значения уже завершённых nodes;
  • signal сообщает, что весь workflow отменён.

Execution levels

ts
const nodes = [
    { id: "profile", execute: loadProfile },
    { id: "permissions", execute: loadPermissions },
    {
        id: "settings",
        dependencies: ["permissions"],
        execute: loadSettings,
    },
    {
        id: "dashboard",
        dependencies: ["profile", "settings"],
        execute: buildDashboard,
    },
]

Граф создаёт уровни:

LevelNodesПочему
1profile, permissionsНет dependencies.
2settingsЖдёт permissions.
3dashboardЖдёт profile и settings.

concurrency ограничивает параллелизм внутри одного level. Он не запускает node до dependencies.

Использование результатов

ts
{
  id: "dashboard",
  dependencies: ["profile", "settings"],
  execute: async ({ results }) => {
    // Map имеет unknown values, поэтому domain code проверяет или уточняет type.
    const profile = results.get("profile") as Profile
    const settings = results.get("settings") as DashboardSettings

    return {
      title: `Кабинет ${profile.name}`,
      widgets: settings.widgets,
    }
  },
}

results — Map, а не объект. Node id может содержать точки/дефисы и не становится property name.

Для строгой type safety создайте project helpers/guards:

ts
const requireResult = <T>(results: ReadonlyMap<string, unknown>, nodeId: string): T => {
    if (!results.has(nodeId)) {
        throw new Error(`Missing workflow result: ${nodeId}`)
    }

    return results.get(nodeId) as T
}

Этот helper не входит в npm-пакет.

Condition

ts
{
  id: "admin-settings",
  dependencies: ["permissions"],
  condition: ({ results }) => {
    const permissions = requireResult<string[]>(results, "permissions")
    return permissions.includes("admin")
  },
  execute: async () => adminSettingsStore.fetch(),
}

Если condition вернула false:

  • node получает progress status skipped;
  • считается завершённой для dependencies;
  • result в Map не добавляется;
  • checkpoint запоминает node как completed.

Зависимый node должен учитывать отсутствие result skipped dependency.

Ошибка condition

Если condition бросила ошибку, это ошибка node. Workflow остановит выдачу новых nodes и начнёт compensation уже завершённых шагов.

Cancellation signal

workflow.cancel() вызывает AbortController workflow. Он прекращает запуск новых nodes, но уже начавшийся domain request должен сам отреагировать на signal.

Надёжный adapter для store с методом cancel():

ts
const executeCancelableStore = async <T>(
    signal: AbortSignal,
    execute: () => Promise<T>,
    cancel: () => void,
): Promise<T> => {
    // Если workflow уже отменён, не запускаем store.
    signal.throwIfAborted()

    // При abort вызываем cancellation конкретного query-layer store.
    const onAbort = (): void => cancel()
    signal.addEventListener("abort", onAbort, { once: true })

    try {
        return await execute()
    } finally {
        signal.removeEventListener("abort", onAbort)
    }
}

Helper проектный и не входит в пакет.

ts
{
  id: "profile",
  execute: ({ signal }) => {
    return executeCancelableStore(
      signal,
      () => profileStore.fetch(),
      () => profileStore.cancel(),
    )
  },
}

Не передавайте signal в Axios из workflow node напрямую, обходя store.

Валидация графа

При workflow.execute() проверяются:

  • duplicate id;
  • неизвестная dependency;
  • dependency на самого себя;
  • cycle A → B → A.

Пример цикла:

ts
;[
    { id: "a", dependencies: ["b"], execute: async () => null },
    { id: "b", dependencies: ["a"], execute: async () => null },
]

Workflow завершится ошибкой до выполнения HTTP nodes.

Ограничение concurrency

ts
const workflow = new WorkflowStore({
    concurrency: 3,
    nodes,
})

Значение округляется вниз и не может быть меньше 1. Без option параллелизм уровня не ограничен. Для backend rate limits задавайте явное значение.

Следующая страница: ошибки и компенсация.