Перейти к содержанию

Длительный процесс как Workflow: проект «Согласование»

Заявка на публикацию редко заканчивается одним HTTP-запросом. Сначала нужно подготовить материал, затем дождаться редактора, а после решения выполнить публикацию. Ожидание может длиться дольше жизни серверного процесса. Если приложение остановилось между этими шагами, недостаточно просто «повторить функцию»: нужно знать, что уже произошло и какие действия ещё допустимы.

В серии построим TypeScript-проект «Согласование». Его Workflow резервирует место для материала, ждёт решения редактора и публикует либо освобождает бронь. Учебная внешняя редакционная система будет отдельным HTTP-сервисом. Вам нужны основы TypeScript, async/await и HTTP. Автор не устанавливал пакеты и не запускал Temporal, поэтому все результаты описаны как ожидаемые.

Идентичность заявки и результат процесса

Начальные данные сохраняют предметный смысл:

export interface Submission {
  submissionId: string;
  revision: number;
  title: string;
  approvalWindowMs?: number;
}

Submission ID определяет материал, revision — согласуемую редакцию. Если текст изменился после решения, новое согласование должно явно относиться к другой редакции. Иначе сигнал «одобрено» способен разрешить публикацию содержимого, которого редактор не видел. Версия данных здесь важнее случайного времени поступления сообщения.

Workflow ID в CLI-примере будет approval-article-101-r1. Он связывает исполнение с конкретной заявкой. Run ID обозначает отдельный запуск внутри жизненного цикла этой идентичности. Эти два значения нельзя смешивать: пользователь узнаёт предметную заявку по Workflow ID, а инженер расследует конкретное исполнение также по Run ID.

Конечные предметные результаты — published, rejected и expired. Ошибка внешнего сервиса или запрос отмены не обязаны выглядеть как один из них. Если публикация не подтверждена, не следует показывать пользователю «отклонено редактором»: решение человека и технический сбой имеют разные причины и дальнейшие действия.

На ранних стадиях курса процесс будет проще: сначала две Activities, затем таймер, затем Signal. Каждая стадия имеет полный файл Workflow. Мы не обещаем, что история любого раннего варианта автоматически совместима с финальным кодом. Для обычного перехода читатель создаёт новую учебную заявку; совместимость развёртывания отдельно разбирается в уроке versioning.

Workflow, Activity, Worker и Service

Workflow описывает последовательность и предметные ветвления. Activity выполняет действие вне детерминированной модели: HTTP-запрос, работу с файлом или внешнюю операцию. Worker исполняет код этих функций и получает задания из Task Queue. Temporal Service хранит историю и координирует выполнение. У каждого участника собственная ответственность.

Например, решение «при одобрении вызвать publish» относится к Workflow. Сам HTTP POST редакционной системе выполняет Activity publish. Адрес EDITORIAL_URL находится в окружении Worker, потому что именно он делает запрос. Браузер и Temporal Service не выполняют наш TypeScript-код публикации от своего имени.

Если Worker остановится во время ожидания редактора, история не должна исчезнуть вместе с его памятью. После появления подходящего Worker выполнение восстановится из истории. Это не сериализация стека Node и не сохранение всех переменных произвольной программы на диск. Код Workflow повторно воспроизводит решения, согласуя их с записанными событиями.

Роль компонентов и начальная модель изложены в руководстве TypeScript SDK. Для нашего примера особенно полезно сохранить две границы: Temporal отвечает за длительный процесс, редакционная система — за собственные публикации и квитанции. История одного участника не заменяет данные другого.

Почему не достаточно обычной очереди

Очередь заданий хорошо подходит для независимых коротких работ: создать превью изображения, отправить уведомление, обработать файл. Например, BullMQ может хранить job, повторы и отложенный запуск. Но для нашего согласования потребуется также выразить ожидание человека, изменение состояния, события для текущего процесса и совместимость кода длительного исполнения.

Это не означает, что очередь «плохая» или не позволяет строить процессы. Можно спроектировать таблицы состояний и переходов, обработчики сообщений и регламент восстановления самостоятельно. Вопрос в том, какую часть модели вы хотите реализовывать и сопровождать. Temporal предоставляет устойчивую модель Workflow с историей, а работа с внешними эффектами всё равно требует предметного проектирования.

В n8n удобно собирать интеграции визуально и связывать готовые узлы. «Согласование» в этом курсе опирается на TypeScript-код, типы, детерминированное воспроизведение и явные границы Activities. Переход от одного инструмента к другому не должен сводиться к замене названия блока: отличаются модель исполнения, развёртывание кода и способы расследования.

Для задачи «один раз вызвать HTTP endpoint» полный Temporal-контур может быть избыточным. Для многодневного согласования с человеком и восстановлением после сбоев его возможности становятся предметно значимыми. Поэтому начинаем не с модной технологии, а с срока процесса, допустимых повторов, внешних эффектов и требований к пониманию состояния.

История и внешние эффекты

Представим, что редакционная система записала публикацию, но ответ потерялся. Worker видит ошибку и Activity повторяется. Temporal не может узнать из воздуха, совершилась ли внешняя операция. Следовательно, устойчивый Workflow не освобождает внешний endpoint от идемпотентности. Повтор с тем же operation key должен возвращать прежний результат без второй публикации.

В нашем учебном gateway публикация и квитанция записываются вместе в один локальный снимок. Это модель внешнего контракта, не готовая производственная база. Один процесс обслуживает изменения последовательно; несколько процессов с общим каталогом и устойчивость к потере питания не заявлены. Реальное хранилище реализует аналогичный принцип транзакцией и уникальным ключом.

История также задаёт ограничения Workflow-кода. Нельзя читать случайный файл или вызывать обычный fetch прямо из Workflow, а затем ожидать такое же решение при воспроизведении. Внешние данные приходят через Activity result, Signal или начальные аргументы. Тогда их можно связать с записанным событием и объяснить источник каждого предметного решения.

Для проекта используем Temporal TypeScript SDK 1.24.0, подтверждённый официальным релизом, и Node.js 24. Версия CLI отдельно фиксируется читателем. Не называем весь набор «последним», потому что сервер, SDK и CLI имеют разные линии выпуска и совместимости.

Результат первого урока — сформулированный процесс с идентичностью, ожиданием и внешними эффектами. В следующем материале создадим минимальные Workflow, Activities, Worker и Client, используя полные исходники вместо несуществующего шаблона приложения.

Оглавление серии