Длительный процесс как 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, используя полные исходники вместо несуществующего шаблона приложения.