updated skills

This commit is contained in:
2026-06-09 21:06:27 +01:00
parent db1ce43ee1
commit 427a5bd919
1283 changed files with 24684 additions and 2313 deletions

View File

@@ -54,19 +54,19 @@ bun add @suaveplan/workflow
import { createWorkflow } from "@suaveplan/workflow";
interface OrderData {
orderId: string;
items: string[];
total: number;
orderId: string;
items: string[];
total: number;
}
const result = await createWorkflow<OrderData>("process-order")
.addStep("validate", (ctx) => {
if (ctx.items.length === 0) throw new Error("No items");
})
.build()
.run({
initialData: { orderId: "ord-1", items: ["widget"], total: 29.99 },
});
.addStep("validate", (ctx) => {
if (ctx.items.length === 0) throw new Error("No items");
})
.build()
.run({
initialData: { orderId: "ord-1", items: ["widget"], total: 29.99 },
});
```
## Modules
@@ -103,15 +103,15 @@ builder.onError("continue"); // log + skip the failure, run the rest
import { RetryPresets } from "@suaveplan/workflow";
builder
.addStep("flaky-api", async (ctx) => callExternalApi(ctx))
.withRetry(RetryPresets.standard());
.addStep("flaky-api", async (ctx) => callExternalApi(ctx))
.withRetry(RetryPresets.standard());
builder.addStep("custom", (ctx) => callApi(ctx)).withRetry({
maxAttempts: 5,
strategy: "exponential",
delayMs: 500,
maxDelayMs: 10_000,
backoffMultiplier: 2,
maxAttempts: 5,
strategy: "exponential",
delayMs: 500,
maxDelayMs: 10_000,
backoffMultiplier: 2,
});
```
@@ -121,13 +121,13 @@ builder.addStep("custom", (ctx) => callApi(ctx)).withRetry({
const engine = createWorkflowEngine(config);
engine.hooks.on("workflow:start", (p) =>
console.log(`Workflow ${p.workflowId} started`),
console.log(`Workflow ${p.workflowId} started`),
);
engine.hooks.on("step:complete", (p) =>
console.log(`Step ${p.stepId} completed`),
console.log(`Step ${p.stepId} completed`),
);
engine.hooks.on("step:fail", (p) =>
console.error(`Step ${p.stepId} failed:`, p.error),
console.error(`Step ${p.stepId} failed:`, p.error),
);
```
@@ -139,18 +139,18 @@ import { createSaga, createStep } from "@suaveplan/workflow";
const saga = createSaga({ id: "booking", name: "Booking Saga" });
saga
.addStep(createStep({
id: "reserve",
name: "Reserve Seats",
execute: (ctx) => reserveSeats(ctx),
compensate: (ctx) => releaseSeats(ctx),
}))
.addStep(createStep({
id: "charge",
name: "Charge Payment",
execute: (ctx) => chargeCard(ctx),
compensate: (ctx) => refundCard(ctx),
}));
.addStep(createStep({
id: "reserve",
name: "Reserve Seats",
execute: (ctx) => reserveSeats(ctx),
compensate: (ctx) => releaseSeats(ctx),
}))
.addStep(createStep({
id: "charge",
name: "Charge Payment",
execute: (ctx) => chargeCard(ctx),
compensate: (ctx) => refundCard(ctx),
}));
const result = await saga.execute(context);
// If "charge" fails, "reserve" is automatically compensated.
@@ -159,4 +159,3 @@ const result = await saga.execute(context);
## License
MIT

View File

@@ -49,12 +49,12 @@ await comp.compensateAll({ orderId: "ord-42" });
import { createCompensator } from "@suaveplan/workflow";
const comp = createCompensator<{ id: string }>({
continueOnError: false,
clock: () => 0,
continueOnError: false,
clock: () => 0,
});
comp.register("a", () => {
throw new Error("a failed");
throw new Error("a failed");
});
comp.register("b", () => undefined);
@@ -62,7 +62,7 @@ await comp.compensateAll({ id: "x" });
// Records contain a single entry for "b" with status "compensated"
// then the loop breaks because continueOnError is false.
if (comp.hasFailures === false) {
// Re-attempt the failed compensations later.
// Re-attempt the failed compensations later.
}
```
@@ -86,7 +86,7 @@ The internal `_runEntry` helper is shared by `compensateAll` and
`continueOnError` is false) so the callers can short-circuit without
duplicating the predicate.
`compensateSteps` filters by id *before* reversing, so the relative order
`compensateSteps` filters by id _before_ reversing, so the relative order
between matching ids is preserved as the inverse of registration order —
mirroring the `compensateAll` contract.

View File

@@ -34,27 +34,27 @@ leaking state across runs.
import { createSaga, createStep } from "@suaveplan/workflow";
const saga = createSaga<{ orderId: string }>({
id: "booking",
name: "Booking saga",
id: "booking",
name: "Booking saga",
});
saga
.addStep(
createStep({
id: "reserve",
name: "Reserve seats",
execute: (ctx) => reserveSeats(ctx),
compensate: (ctx) => releaseSeats(ctx),
}),
)
.addStep(
createStep({
id: "charge",
name: "Charge payment",
execute: (ctx) => chargeCard(ctx),
compensate: (ctx) => refundCard(ctx),
}),
);
.addStep(
createStep({
id: "reserve",
name: "Reserve seats",
execute: (ctx) => reserveSeats(ctx),
compensate: (ctx) => releaseSeats(ctx),
}),
)
.addStep(
createStep({
id: "charge",
name: "Charge payment",
execute: (ctx) => chargeCard(ctx),
compensate: (ctx) => refundCard(ctx),
}),
);
const result = await saga.execute({ orderId: "ord-1" });
```
@@ -66,28 +66,28 @@ import { createSaga, createStep } from "@suaveplan/workflow";
const fakeClock = () => 1_000;
const saga = createSaga<{ id: string }>({
id: "two-phase",
name: "Two-phase commit",
continueCompensationOnError: false,
clock: fakeClock,
id: "two-phase",
name: "Two-phase commit",
continueCompensationOnError: false,
clock: fakeClock,
});
saga.addStep(
createStep({
id: "lock",
name: "Lock resource",
execute: (ctx) => acquireLock(ctx.id),
compensate: (ctx) => releaseLock(ctx.id),
}),
createStep({
id: "lock",
name: "Lock resource",
execute: (ctx) => acquireLock(ctx.id),
compensate: (ctx) => releaseLock(ctx.id),
}),
);
saga.addStep(
createStep({
id: "commit",
name: "Commit",
execute: () => {
throw new Error("conflict");
},
}),
createStep({
id: "commit",
name: "Commit",
execute: () => {
throw new Error("conflict");
},
}),
);
const result = await saga.execute({ id: "row-7" });

View File

@@ -36,13 +36,13 @@ the live context mutating beneath the caller.
import { createContext } from "@suaveplan/workflow";
interface OrderData {
orderId: string;
total: number;
orderId: string;
total: number;
}
const ctx = createContext<OrderData>("wf-1", {
orderId: "ord-1",
total: 9.99,
orderId: "ord-1",
total: 9.99,
});
ctx.setStatus("running");
ctx.updateData({ total: 14.99 });
@@ -56,21 +56,21 @@ import { createContext } from "@suaveplan/workflow";
const ctx = createContext("wf-1", { user: "alice" });
ctx.setMeta("traceId", "00-abc-def");
ctx.addStepResult({
stepId: "validate",
status: "completed",
startedAt: 0,
completedAt: 1,
attempts: 1,
stepId: "validate",
status: "completed",
startedAt: 0,
completedAt: 1,
attempts: 1,
});
const before = ctx.snapshot();
ctx.updateData({ user: "bob" });
ctx.addStepResult({
stepId: "charge",
status: "completed",
startedAt: 1,
completedAt: 2,
attempts: 1,
stepId: "charge",
status: "completed",
startedAt: 1,
completedAt: 2,
attempts: 1,
});
console.log(before.data.user); // "alice" — snapshot is unaffected

View File

@@ -33,11 +33,11 @@ so callers can subscribe to lifecycle events before the engine is built.
import { createWorkflow } from "@suaveplan/workflow";
const result = await createWorkflow<{ orderId: string }>("checkout")
.addStep("validate", (ctx) => {
if (!ctx.orderId) throw new Error("missing");
})
.build()
.run({ initialData: { orderId: "ord-1" } });
.addStep("validate", (ctx) => {
if (!ctx.orderId) throw new Error("missing");
})
.build()
.run({ initialData: { orderId: "ord-1" } });
```
## Usage: Advanced Example
@@ -46,21 +46,21 @@ const result = await createWorkflow<{ orderId: string }>("checkout")
import { createWorkflow, RetryPresets } from "@suaveplan/workflow";
const engine = createWorkflow<{ user: string }>("onboarding", {
clock: () => 1_700_000_000_000,
clock: () => 1_700_000_000_000,
})
.withTimeout(30_000)
.onError("compensate")
.on("step:fail", (p) => console.error("failed:", p.stepId));
.withTimeout(30_000)
.onError("compensate")
.on("step:fail", (p) => console.error("failed:", p.stepId));
engine
.addStep("create", (ctx) => createAccount(ctx.user))
.withCompensation((ctx) => deleteAccount(ctx.user))
.withRetry({ maxAttempts: 2, strategy: "exponential" });
.addStep("create", (ctx) => createAccount(ctx.user))
.withCompensation((ctx) => deleteAccount(ctx.user))
.withRetry({ maxAttempts: 2, strategy: "exponential" });
engine
.addStep("notify", (ctx) => sendWelcome(ctx.user))
.withGuard((ctx) => ctx.user.startsWith("@"))
.withTimeout(5_000);
.addStep("notify", (ctx) => sendWelcome(ctx.user))
.withGuard((ctx) => ctx.user.startsWith("@"))
.withTimeout(5_000);
const result = await engine.run({ initialData: { user: "@alice" } });
```

View File

@@ -29,7 +29,7 @@ import { createWorkflowEngine } from "@suaveplan/workflow";
const engine = createWorkflowEngine({ id: "w", name: "n", steps: [] });
engine.transitionHooks.hook((data) => {
console.log("entering", data.stepName);
console.log("entering", data.stepName);
});
```
@@ -37,18 +37,18 @@ engine.transitionHooks.hook((data) => {
```ts
const engine = createWorkflowEngine<{ region: string }>({
id: "w",
name: "checkout",
steps,
id: "w",
name: "checkout",
steps,
});
const unsubscribe = engine.transitionHooks.hook(async (data) => {
if (data.context.region === "EU") {
return {
...data,
context: { ...data.context, region: "EU-norm" },
};
}
if (data.context.region === "EU") {
return {
...data,
context: { ...data.context, region: "EU-norm" },
};
}
});
await engine.run({ initialData: { region: "EU" } });

View File

@@ -40,18 +40,18 @@ codes.
import { createWorkflowEngine } from "@suaveplan/workflow";
const engine = createWorkflowEngine<{ orderId: string }>({
id: "wf-1",
name: "checkout",
onError: "stop",
steps: [
{
id: "validate",
name: "Validate",
execute: (ctx) => {
if (!ctx.orderId) throw new Error("missing");
},
},
],
id: "wf-1",
name: "checkout",
onError: "stop",
steps: [
{
id: "validate",
name: "Validate",
execute: (ctx) => {
if (!ctx.orderId) throw new Error("missing");
},
},
],
});
const result = await engine.run({ initialData: { orderId: "ord-1" } });
@@ -63,31 +63,31 @@ const result = await engine.run({ initialData: { orderId: "ord-1" } });
import { createWorkflowEngine } from "@suaveplan/workflow";
const engine = createWorkflowEngine<{ user: string }>({
id: "wf-1",
name: "onboarding",
timeout: 30_000,
onError: "compensate",
steps: [
{
id: "create-account",
name: "Create account",
execute: (ctx) => createAccount(ctx.user),
compensate: (ctx) => deleteAccount(ctx.user),
},
{
id: "send-welcome",
name: "Send welcome email",
execute: (ctx) => sendWelcome(ctx.user),
},
],
id: "wf-1",
name: "onboarding",
timeout: 30_000,
onError: "compensate",
steps: [
{
id: "create-account",
name: "Create account",
execute: (ctx) => createAccount(ctx.user),
compensate: (ctx) => deleteAccount(ctx.user),
},
{
id: "send-welcome",
name: "Send welcome email",
execute: (ctx) => sendWelcome(ctx.user),
},
],
});
engine.hooks.on("step:fail", (p) => {
console.error("step failed", p.stepId, p.error?.message);
console.error("step failed", p.stepId, p.error?.message);
});
engine.transitionHooks.hook((data) => {
return { ...data, context: { ...data.context, user: data.context.user.trim() } };
return { ...data, context: { ...data.context, user: data.context.user.trim() } };
});
const run = engine.run({ initialData: { user: " alice " } });
@@ -121,7 +121,7 @@ while still satisfying the registry's reachability gate.
is the original step error, so callers walking the cause chain still see
the real underlying error.
The `workflow:cancel` emit happens during the loop *and* the status is
The `workflow:cancel` emit happens during the loop _and_ the status is
re-set to `"cancelled"` after the loop returns; the second assignment is
a guard for the rare case where the loop returns immediately because the
flag was set before the first iteration.

View File

@@ -40,20 +40,20 @@ throw new WorkflowError(WORKFLOW_CODES.TIMEOUT, 'Workflow "checkout" timed out')
import { WORKFLOW_CODES, WorkflowError } from "@suaveplan/workflow/errors";
try {
await engine.run({ initialData });
await engine.run({ initialData });
} catch (error) {
if (error instanceof WorkflowError) {
switch (error.code) {
case WORKFLOW_CODES.TIMEOUT:
metrics.record("workflow.timeout");
break;
case WORKFLOW_CODES.COMPENSATION_FAILED:
alerts.fire({ severity: "critical", error });
break;
default:
logger.error("workflow failed", { code: error.code });
}
}
if (error instanceof WorkflowError) {
switch (error.code) {
case WORKFLOW_CODES.TIMEOUT:
metrics.record("workflow.timeout");
break;
case WORKFLOW_CODES.COMPENSATION_FAILED:
alerts.fire({ severity: "critical", error });
break;
default:
logger.error("workflow failed", { code: error.code });
}
}
}
```

View File

@@ -13,7 +13,7 @@ that aren't built in, push-notifications, audit trails). It deliberately
keeps the surface to four operations — `on`, `once`, `off`, `emit` — so
the engine and its consumers depend on a stable contract.
Every other module in the package emits *through* this class rather than
Every other module in the package emits _through_ this class rather than
acquiring its own listeners-map, so the registry is the canonical extension
point for downstream tooling.
@@ -34,12 +34,12 @@ import { createHookRegistry } from "@suaveplan/workflow";
const hooks = createHookRegistry<{ orderId: string }>();
hooks.on("workflow:start", (p) => {
console.log("starting", p.workflowId);
console.log("starting", p.workflowId);
});
await hooks.emit("workflow:start", {
workflowId: "wf-1",
context: { orderId: "ord-1" },
workflowId: "wf-1",
context: { orderId: "ord-1" },
});
```
@@ -47,34 +47,34 @@ await hooks.emit("workflow:start", {
```ts
import {
buildStepPayload,
createHookRegistry,
buildStepPayload,
createHookRegistry,
} from "@suaveplan/workflow";
const hooks = createHookRegistry<{ user: string }>();
const unsubscribe = hooks.on("step:fail", async (p) => {
if (p.stepResult && p.stepResult.attempts >= 3) {
await pageOnCall(p.error);
}
if (p.stepResult && p.stepResult.attempts >= 3) {
await pageOnCall(p.error);
}
});
hooks.once("workflow:complete", (p) => {
console.log("done", p.workflowId, p.timestamp);
console.log("done", p.workflowId, p.timestamp);
});
await hooks.emit("step:fail", buildStepPayload(
"wf-1",
{ user: "alice" },
"step-1",
{
stepId: "step-1",
status: "failed",
startedAt: 0,
completedAt: 100,
attempts: 3,
},
new Error("boom"),
"wf-1",
{ user: "alice" },
"step-1",
{
stepId: "step-1",
status: "failed",
startedAt: 0,
completedAt: 100,
attempts: 3,
},
new Error("boom"),
));
unsubscribe();

View File

@@ -35,8 +35,8 @@ the numbers in every consumer.
import { withRetry, RetryPresets } from "@suaveplan/workflow";
const result = await withRetry(
() => fetch("/api/data").then((r) => r.json()),
RetryPresets.standard(),
() => fetch("/api/data").then((r) => r.json()),
RetryPresets.standard(),
);
```
@@ -46,14 +46,14 @@ const result = await withRetry(
import { createRetryWrapper } from "@suaveplan/workflow";
const wrap = createRetryWrapper({
maxAttempts: 4,
strategy: "exponential",
delayMs: 100,
maxDelayMs: 2_000,
shouldRetry: (err) => !(err instanceof TypeError),
onRetry: (err, attempt) => {
console.warn(`attempt ${attempt} failed:`, err.message);
},
maxAttempts: 4,
strategy: "exponential",
delayMs: 100,
maxDelayMs: 2_000,
shouldRetry: (err) => !(err instanceof TypeError),
onRetry: (err, attempt) => {
console.warn(`attempt ${attempt} failed:`, err.message);
},
});
const json = await wrap(() => fetch("/api/v1").then((r) => r.json()));
@@ -72,7 +72,7 @@ const json = await wrap(() => fetch("/api/v1").then((r) => r.json()));
## Implementation Notes
`withRetry` always rethrows the *last* error, not the *first*. Callers
`withRetry` always rethrows the _last_ error, not the _first_. Callers
that want the first-error semantics can capture it inside an `onRetry`
closure. The loop checks `attempt >= maxAttempts` before `shouldRetry` so
the final attempt always actually runs even if `shouldRetry` would have

View File

@@ -52,7 +52,7 @@ await engine.run({ initialData });
const snapshot = await reader.collect();
const stepCounter = snapshot.resourceMetrics.scopeMetrics[0].metrics.find(
(m) => m.descriptor.name === "workflow.step.executed_total",
(m) => m.descriptor.name === "workflow.step.executed_total",
);
expect(stepCounter?.dataPoints.length).toBeGreaterThan(0);

View File

@@ -34,19 +34,19 @@ concerns. `compensate` rolls back only the branch that actually ran.
import { createConditionalStep, createStep } from "@suaveplan/workflow";
const branch = createConditionalStep<{ premium: boolean }>({
id: "discount",
name: "Discount",
condition: (ctx) => ctx.premium,
onTrue: createStep({
id: "premium",
name: "Premium discount",
execute: () => ({ discountPct: 30 }),
}),
onFalse: createStep({
id: "standard",
name: "Standard discount",
execute: () => ({ discountPct: 5 }),
}),
id: "discount",
name: "Discount",
condition: (ctx) => ctx.premium,
onTrue: createStep({
id: "premium",
name: "Premium discount",
execute: () => ({ discountPct: 30 }),
}),
onFalse: createStep({
id: "standard",
name: "Standard discount",
execute: () => ({ discountPct: 5 }),
}),
});
const result = await branch.run({ premium: true });
```
@@ -57,21 +57,21 @@ const result = await branch.run({ premium: true });
import { createConditionalStep, createStep } from "@suaveplan/workflow";
const ship = createStep<{ shipped: boolean }>({
id: "ship",
name: "Ship",
execute: async (ctx) => {
await dispatch(ctx);
},
compensate: async (ctx) => {
await recall(ctx);
},
id: "ship",
name: "Ship",
execute: async (ctx) => {
await dispatch(ctx);
},
compensate: async (ctx) => {
await recall(ctx);
},
});
const guard = createConditionalStep<{ ready: boolean }>({
id: "maybe-ship",
name: "Maybe ship",
condition: (ctx) => ctx.ready,
onTrue: ship,
id: "maybe-ship",
name: "Maybe ship",
condition: (ctx) => ctx.ready,
onTrue: ship,
});
await guard.run({ ready: true });

View File

@@ -36,13 +36,13 @@ failure.
import { createParallelStep, createStep } from "@suaveplan/workflow";
const group = createParallelStep<{ url: string }>({
groupId: "fetch-group",
name: "Fetch resources",
groupId: "fetch-group",
name: "Fetch resources",
});
group
.addStep(createStep({ id: "a", name: "A", execute: () => "a" }))
.addStep(createStep({ id: "b", name: "B", execute: () => "b" }));
.addStep(createStep({ id: "a", name: "A", execute: () => "a" }))
.addStep(createStep({ id: "b", name: "B", execute: () => "b" }));
const result = await group.run({ url: "https://example.com" });
```
@@ -53,31 +53,31 @@ const result = await group.run({ url: "https://example.com" });
import { createParallelStep, createStep } from "@suaveplan/workflow";
const group = createParallelStep<{ jobIds: string[] }>({
groupId: "process",
name: "Process jobs",
maxConcurrency: 2,
failFast: true,
groupId: "process",
name: "Process jobs",
maxConcurrency: 2,
failFast: true,
});
for (const id of ["a", "b", "c", "d"]) {
group.addStep(
createStep({
id,
name: `process-${id}`,
execute: async (ctx) => {
if (id === "b") throw new Error("nope");
return id;
},
compensate: async () => {
await unprocess(id);
},
}),
);
group.addStep(
createStep({
id,
name: `process-${id}`,
execute: async (ctx) => {
if (id === "b") throw new Error("nope");
return id;
},
compensate: async () => {
await unprocess(id);
},
}),
);
}
const result = await group.run({ jobIds: [] });
if (result.status === "failed") {
await group.compensateAll({ jobIds: [] });
await group.compensateAll({ jobIds: [] });
}
```

View File

@@ -37,9 +37,9 @@ unless they want to.
import { createStep } from "@suaveplan/workflow";
const charge = createStep<{ total: number }>({
id: "charge",
name: "Charge card",
execute: (ctx) => ({ chargedAt: Date.now(), amount: ctx.total }),
id: "charge",
name: "Charge card",
execute: (ctx) => ({ chargedAt: Date.now(), amount: ctx.total }),
});
const result = await charge.run({ total: 9.99 });
@@ -52,20 +52,20 @@ import { createStep, RetryPresets } from "@suaveplan/workflow";
const fakeClock = () => 0;
const flaky = createStep<{ url: string }>({
id: "fetch",
name: "Fetch resource",
execute: async (ctx) => {
const response = await fetch(ctx.url);
if (!response.ok) throw new Error(`status ${response.status}`);
return response.json();
},
guard: (ctx) => ctx.url.startsWith("https://"),
retry: RetryPresets.standard(),
timeout: 5_000,
compensate: async () => {
await releaseLock();
},
clock: fakeClock,
id: "fetch",
name: "Fetch resource",
execute: async (ctx) => {
const response = await fetch(ctx.url);
if (!response.ok) throw new Error(`status ${response.status}`);
return response.json();
},
guard: (ctx) => ctx.url.startsWith("https://"),
retry: RetryPresets.standard(),
timeout: 5_000,
compensate: async () => {
await releaseLock();
},
clock: fakeClock,
});
```

View File

@@ -26,18 +26,18 @@ package root barrel and reason about wire shapes without chasing imports.
import type { WorkflowConfig, StepDefinition } from "@suaveplan/workflow";
const validate: StepDefinition<{ items: string[] }> = {
id: "validate",
name: "Validate",
execute: (ctx) => {
if (ctx.items.length === 0) throw new Error("empty");
},
id: "validate",
name: "Validate",
execute: (ctx) => {
if (ctx.items.length === 0) throw new Error("empty");
},
};
const config: WorkflowConfig<{ items: string[] }> = {
id: "wf-1",
name: "checkout",
steps: [validate],
onError: "stop",
id: "wf-1",
name: "checkout",
steps: [validate],
onError: "stop",
};
```
@@ -45,35 +45,35 @@ const config: WorkflowConfig<{ items: string[] }> = {
```ts
import type {
HookListener,
StepResult,
RetryConfig,
HookListener,
StepResult,
RetryConfig,
} from "@suaveplan/workflow";
const auditLog: HookListener<{ orderId: string }> = (payload) => {
if (payload.event === "step:fail" && payload.stepResult) {
console.error("step failed", {
id: payload.stepResult.stepId,
attempts: payload.stepResult.attempts,
error: payload.stepResult.error?.message,
});
}
if (payload.event === "step:fail" && payload.stepResult) {
console.error("step failed", {
id: payload.stepResult.stepId,
attempts: payload.stepResult.attempts,
error: payload.stepResult.error?.message,
});
}
};
const exponential: RetryConfig = {
maxAttempts: 5,
strategy: "exponential",
delayMs: 100,
backoffMultiplier: 2,
maxDelayMs: 5000,
maxAttempts: 5,
strategy: "exponential",
delayMs: 100,
backoffMultiplier: 2,
maxDelayMs: 5000,
};
const skipped: StepResult = {
stepId: "skip",
status: "skipped",
startedAt: 0,
completedAt: 0,
attempts: 0,
stepId: "skip",
status: "skipped",
startedAt: 0,
completedAt: 0,
attempts: 0,
};
```

View File

@@ -32,7 +32,7 @@ changing the handler signatures.
import type { WorkflowTransitioningHandler } from "@suaveplan/workflow";
const audit: WorkflowTransitioningHandler<{ userId: string }> = (data) => {
console.log("entering step", data.stepName, "for", data.context.userId);
console.log("entering step", data.stepName, "for", data.context.userId);
};
```
@@ -40,25 +40,25 @@ const audit: WorkflowTransitioningHandler<{ userId: string }> = (data) => {
```ts
import type {
WorkflowTransitionData,
WorkflowTransitioningHandler,
WorkflowTransitionData,
WorkflowTransitioningHandler,
} from "@suaveplan/workflow";
interface OrderCtx {
orderId: string;
region: string;
taxRate?: number;
orderId: string;
region: string;
taxRate?: number;
}
// Enrich context with region-specific tax rate before each step runs.
const enrichTax: WorkflowTransitioningHandler<OrderCtx> = async (data) => {
if (data.context.taxRate !== undefined) return;
const rate = await fetchTaxRate(data.context.region);
const next: WorkflowTransitionData<OrderCtx> = {
...data,
context: { ...data.context, taxRate: rate },
};
return next;
if (data.context.taxRate !== undefined) return;
const rate = await fetchTaxRate(data.context.region);
const next: WorkflowTransitionData<OrderCtx> = {
...data,
context: { ...data.context, taxRate: rate },
};
return next;
};
```