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

Signal редактора, Query состояния и срок решения

Таймер предыдущего урока продолжал публикацию без решения человека. Теперь Workflow будет ждать либо Signal редактора, либо истечение окна согласования. При approve публикуется материал, при reject или отсутствии решения освобождается бронь. Query позволит узнать текущую стадию процесса. Результат урока — связать входящее сообщение с уже существующей заявкой и отделить изменение состояния от чтения.

Полный Workflow находится в src/workflows/05-signals.ts, общий Client уже содержит необходимые команды. Автор не отправлял Signal и не выполнял Query; дальнейшие ответы являются ожидаемыми. Для самостоятельной работы используется новый ID article-105 и Worker именно этой стадии.

Контракт решения и обработчик

Decision содержит идентификатор решения, revision, outcome и редактора. Идентификатор может помочь внешнему интерфейсу сопоставить отправку с журналом пользователя, а revision защищает от применения решения к другой редакции. Имя Signal постоянно: editorDecision. Оно обозначает тип сообщения, а не отдельного пользователя.

Внутри полного Workflow:

export const editorDecision = defineSignal<[Decision]>('editorDecision');
export const statusQuery = defineQuery<Status>('status');

let decision: Decision | undefined;
let status: Status = 'starting';
setHandler(editorDecision, value => {
  if (!decision && value.revision === input.revision &&
      (value.outcome === 'approve' || value.outcome === 'reject')) {
    decision = value;
  }
});
setHandler(statusQuery, () => status);

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

Обработчик синхронный и короткий. Он проверяет revision и принимает первое допустимое решение. Повторное сообщение не меняет уже выбранную ветку. Мы не используем decisionId как единственную защиту, потому что два разных ID всё ещё могут описывать противоречивые решения одной редакции. Предметное правило «первое принятое решение» выражено прямо.

TypeScript-тип не является проверкой всех внешних данных во время исполнения. В примере outcome дополнительно проверяется, но настоящему интерфейсу нужны полная валидация и авторизация редактора перед отправкой. Поле editor в payload само по себе не доказывает, кто действительно действовал. Это название участника, а не доверенный токен.

Ожидание сообщения и таймера

После подтверждённой брони Workflow переводит status в waiting:

const received = await condition(
  () => decision !== undefined,
  input.approvalWindowMs ?? 60000,
);
if (!received || decision?.outcome !== 'approve') {
  await releaseSlot(input);
  status = received ? 'rejected' : 'expired';
  return { status };
}

Condition ждёт, пока предикат станет истинным, или наступит timeout. Результат true означает, что условие выполнено; false — что в этой ветке истёк срок. Решение не читается из внешней базы при каждом опросе. Signal изменяет внутреннее состояние, а Workflow реагирует на записанное событие.

Если решение пришло до перехода в waiting, переменная уже заполнена, и condition не должна заставить человека ждать полную минуту. Поэтому обработчик устанавливается до reserveSlot. Ранний Signal может быть частью процесса, даже когда внешняя подготовка ещё выполняется. Подготовка всё равно должна закончиться до публикации.

Если deadline и Signal близки по времени, окончательное ветвление определяется обработанной последовательностью событий, а не временем на экране отправителя. Нельзя обещать принятие решения только по локальному timestamp кнопки. UI должен показать результат обработки процесса или использовать более строгий протокол подтверждения, если бизнес требует такого взаимодействия.

Что подтверждает отправка Signal

Будущие команды:

WORKFLOW_STAGE=05-signals npm run worker
npm run client -- start article-105
npm run client -- query article-105
npm run client -- signal article-105 approve
npm run client -- result article-105

Client печатает «Signal accepted by Temporal Service». Такая формулировка точнее, чем «материал одобрен и опубликован». Успешное завершение signal-вызова подтверждает принятие сообщения Service, но Workflow ещё должен обработать его и выполнить publish. Между этими событиями может пройти время, особенно при недоступном Worker.

Пример использует handle.signal('editorDecision', decision), то есть строковое имя. Это позволяет Client не импортировать Workflow-модуль. Ошибка в строке имени может привести к тому, что нужный обработчик не получит ожидаемое сообщение. В крупном проекте контракты сообщений выделяют отдельно и проверяют согласованность имён без загрузки sandbox-кода в клиент.

Повторное нажатие кнопки должно иметь предметное правило. В нашем обработчике первое допустимое решение сохраняется. Если нужен сценарий «редактор может изменить мнение до публикации», это другая модель: потребуется допустимое состояние для изменения, версия решения и объяснение конкурентного publish. Не стоит добавлять её скрыто одной новой строкой присваивания.

Signal нельзя отправлять бесконечно в уже завершённый процесс с ожиданием, что он снова оживёт. Для повторного согласования нужна новая редакция или другой определённый жизненный цикл. Идентичность Workflow помогает пользователю отличить старое решение от новой заявки.

Query не выполняет действие

Ответ Query не даёт права выполнить следующий внешний шаг самостоятельно из браузера. Если UI видит waiting, он может предложить кнопку решения, но publication по-прежнему создаёт Workflow через Activity. Иначе два независимых участника начнут менять одну систему: процесс согласования и случайный HTTP-обработчик интерфейса.

Также возможна смена состояния между Query и Signal. Пользователь прочитал waiting, но пока нажимал кнопку, окно истекло. Это обычная конкурентная ситуация. Интерфейс должен узнать окончательный исход процесса, а не считать старое чтение обещанием принять будущее решение. Такая граница особенно заметна в коротком учебном окне одной минуты.

Query status читает состояние и не вызывает publish или releaseSlot. Он не должен изменять переменные Workflow. Это важно для интерфейса: обычное обновление страницы не должно становиться командой бизнес-процесса. Чтение может происходить многократно и в любое удобное пользователю время.

Query обычно требует подходящего Worker, способного обработать состояние Workflow, поэтому недоступность исполнителя может затруднить чтение даже при сохранённой истории. Это отличается от утверждения, что заявка исчезла. Операционная UI и история помогают определить статус Service, а пользовательский список заявок в продукте часто строится отдельной проекцией.

Отдельные Workflow Updates позволяют выразить другой контракт взаимодействия, включая подтверждённую обработку и результат. В этом уроке используем Signal, потому что достаточно асинхронно передать решение. Если интерфейсу необходимо немедленно получить предметный ответ «решение принято для revision 3», стоит выбрать соответствующий механизм осознанно, а не трактовать Signal как синхронный RPC.

Правила сообщений представлены в документации message passing. Для «Согласования» главное различие уже выражено: Signal меняет процесс, Query читает его, condition связывает событие с временем. Далее настроим retry и timeout внешних Activities, поскольку одобрение ещё не гарантирует доступную редакционную систему.

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