Async-саги
Часть write-операций MineFlow исполняется не синхронно, а как сага — цепочка шагов с компенсацией при сбое. Такой POST не возвращает результат сразу: бэк ставит сагу в очередь, отвечает 202 с идентификатором саги и продолжает работу в фоне. Фронт опрашивает статус до терминального состояния (SSE для саг нет).
Канонический пример — утверждение сменного рапорта: центральная 5-шаговая approve-сага. Сторнирование уже утверждённого рапорта — компенсирующая сага.
В React почти всё за тебя делают useApproveShiftReport / useRejectShiftReportAfterApprove (и обобщённый useSagaMutation). Прямой pollSaga из ядра нужен в не-React коде или для нестандартных сценариев. Раздел про ядро ниже объясняет, что именно происходит под капотом.
TL;DR
import { useApproveShiftReport } from '@mineflow/client-react';
function ApproveButton({ id }: { id: string }) {
const approve = useApproveShiftReport();
return (
<button
disabled={approve.isPending}
onClick={() =>
approve.mutate(
{ id },
{
onSuccess: () => toast('Рапорт утверждён'),
onError: () => toast('Сага откатилась — рапорт не утверждён'),
},
)
}
>
{approve.isPending ? 'Утверждаю…' : 'Утвердить'}
</button>
);
}
approve.data — это финальный SagaStatus (вся хронология шагов). Терминальный провал саги уходит в onError, а не в onSuccess — см. ниже.
Форма ответа: 202 + sagaId
POST саги о тдаёт не доменную сущность, а идентификатор саги. Важно: поле называется по-разному в зависимости от операции — sagaId для approve, rejectSagaId для сторно. Хуки уже знают, какое поле читать.
// approve
const res = unwrap(await c.POST('/api/v1/prd/shift-reports/{id}/approve', { /* ... */ }));
res.sagaId; // → строка
// reject-after-approve (компенсация)
const res = unwrap(await c.POST('/api/v1/prd/shift-reports/{id}/reject-after-approve', { /* ... */ }));
res.rejectSagaId; // → строка
Полный список саг и их полей — в REST-референсе (роуты с ответом 202).
Поллинг статуса: pollSaga
Ядро экспортирует pollSaga — он опрашивает GET /api/v1/sagas/{sagaId}/status, пока сага не «устаканится», и резолвится финальным SagaStatus.
import { pollSaga, unwrap } from '@mineflow/client-core';
const { sagaId } = unwrap(
await client.POST('/api/v1/prd/shift-reports/{id}/approve', {
params: { path: { id }, header: { 'Idempotency-Key': '' } },
}),
);
const final = await pollSaga(client, sagaId, {
intervalMs: 1000, // период опроса (по умолчанию 1000)
timeoutMs: 60_000, // лимит ожидания (по умолчанию 60_000)
});
// final.steps — полная хронология шагов
pollSaga НЕ бросает на провале сагиpollSaga бросает только SagaTimeoutError (см. ниже). Если сага закончилась откатом (failed/compensated), он резолвится этим статусом, а не reject'ит промис. Решение «успех или провал» принимаешь ты — в React это делает хук автоматически. Подробнее — в разделе про терминальный провал.
Опции PollSagaOptions
| Опция | По умолчанию | Назначение |
|---|---|---|
intervalMs | 1000 | Пауза между опросами. |
timeoutMs | 60_000 | Общий лимит; по превышении — SagaTimeoutError. |
isSettled | — | Переопределяемый предикат «сага завершена» (по умолчанию defaultIsSettled). |
sleep | — | Своя реализация задержки (полезно в тестах). |
now | — | Свой источник времени (полезно в тестах). |
Полные сигнатуры и типы — в API-референсе client-core.