@iworkflow /核心
Портдвижкадолгихпроцессов:кодпроцессапишетсяодинразиисполняется时间
(@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 };
};
Шаг r прогрессом: heartbeat(details) и lastHeartbeat() из этого пакета внутри функции шага。
Рантайм шага ставит адаптер на время шага (stepScope):3 одном процессе могут жить оба движка。
@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) родитель получает, если <e:2>
проверит вход до запуска или поймает isFailure(e) && e.type === INVALID_WORKFLOW_ID.
Манифест(JSON模式входа,исходов,сигналови查询),страницас减记диаграммой美人鱼ипроверкадрейфа:
bunx iworkflow-contracts src/temporal/contract.ts --out docs/contracts # записать (нужен bun: грузит .ts)
bunx iworkflow-contracts src/temporal/contract.ts --out docs/contracts --check # в CI: падает на расхождении
Одинвызовнакаталогсовсемимодулями:лишниефайлысметкойгенераторазаписьудаляет,файлыбезметкинетрогает。