@iworkflow/core
Порт движка долгих процессов: код процесса пишется один раз и исполняется Temporal
(@iworkflow/temporal) или DBOS (@iworkflow/dbos). Зависимостей нет; контракту процесса нужен @sinclair/typebox.
import type { Flow } from "@iworkflow/core";
export const outreach: Flow = async (ctx, input: { id: string }) => {
const draft = await ctx.step<string>("compose", [input]);
const profile = await ctx.child({ app: "iprofile", flow: "enrich" }, [input], { id: `${ctx.workflowId}-p` });
const reply = await ctx.recv<string>("reply", 86_400_000);
if (await ctx.patched("send-twice")) await ctx.step("remind", [input]);
await ctx.setState("status", { reply });
return { draft, profile, reply };
};
Шаг с прогрессом: heartbeat(details) и lastHeartbeat() из этого пакета внутри функции шага.
Рантайм шага ставит адаптер на время шага (stepScope): в одном процессе могут жить оба движка.
@iworkflow/core и адаптеры — одной версии: область шага общая через Symbol.for.
Отказы одинаковы на обоих движках. Шаг бросает nonRetryable(message, type, details) или
new Failure(message, { type }); порт бросает FlowError с reason (failed, timeout,
terminated, already-started, not-found), type, details и exhausted:
try {
await ctx.step("send", [input], { attempts: 20, retryIntervalMs: 120_000, backoff: 1.5, pool: "paced" });
} catch (e) {
if (e instanceof FlowError && e.type === "QuietHours") await ctx.sleep(e.details as number);
else throw e;
}
Ещё: ctx.startChild (без ожидания), ChildOptions.timeoutMs (id не переиспользуется: занятый — already-started), ctx.startedAt,
ctx.parentId, AppDefinition.pools и maxConcurrentSteps, StartOptions.onExisting, handle.describe() и handle.terminate(),
client.list({ idPrefix, flow, status, since, limit }) — процессы движка, свежие первыми (без своего реестра запусков).
Приоритет: client.start(..., { priority }) (1 — первым, 5 — последним) → ctx.priority; шаги и
дети его наследуют, StepOptions.priority — свой. Срок повтора из отказа: new Failure(msg, { type, retryAfterMs }); benign: true — штатный отказ, Temporal не пишет его ошибкой. Номер попытки в шаге — stepAttempt(), процесс и запуск — stepInfo(), отмена попытки — stepSignal(). Журнал без повторов — ctx.log. Время в процессе — await ctx.now().
client.signalWithStart(target, args, opts, topic, value) — сигнал идущему или запуск с сигналом.
Контракт обоих движков — lab/test/contract.test.ts репозитория.
Контракт процесса
Владелец процесса объявляет контракт схемами TypeBox (@sinclair/typebox 0.34, как в Elysia) в своём пакете;
соседи импортируют его оттуда, а не копируют типы.
import { Type as T } from "@sinclair/typebox";
import { childContract, defineWorkflowContract, startContract } from "@iworkflow/core/contract";
export const organizationOnboarding = defineWorkflowContract({
name: "organizationOnboarding",
taskQueue: "iprofile",
workflowId: "iprofile-org-{code}", // поля — обязательные поля входа
input: T.Object({ code: T.String(), linkedBy: T.String() }),
outcomes: { created: T.Object({ ok: T.Literal(true), organizationId: T.String() }),
refused: T.Object({ ok: T.Literal(false), reason: T.String() }) },
states: { checking: { next: ["provisioning", "refused"] }, provisioning: { next: ["created"] } },
signals: {}, queries: {}, // тема/ключ → { description, schema }
});
const h = await startContract(client, organizationOnboarding, input); // id из шаблона, исход типизирован
const out = await childContract(ctx, organizationOnboarding, input); // из кода процесса
Ошибки согласованности (поле id не во входе, недостижимый исход, состояние без пути к исходу) — при импорте.
workflowId(input), startContract и childContract на пустом или неверном поле отдают Failure с типом InvalidWorkflowId
(nonRetryable): в процессе это отказ, а не вечный повтор задачи. Отказ по делу (bad-code) родитель получает, если сам
проверит вход до запуска или поймает isFailure(e) && e.type === INVALID_WORKFLOW_ID.
Манифест (JSON Schema входа, исходов, сигналов и query), страница markdown с диаграммой Mermaid и проверка дрейфа:
bunx iworkflow-contracts src/temporal/contract.ts --out docs/contracts # записать (нужен bun: грузит .ts)
bunx iworkflow-contracts src/temporal/contract.ts --out docs/contracts --check # в CI: падает на расхождении
Один вызов на каталог со всеми модулями: лишние файлы с меткой генератора запись удаляет, файлы без метки не трогает.