/** * Lifecycle states for an individual step within a flow. * * pending → running → (success | failed | compensating → compensated | skipped) */ type StepStatus = 'pending' | 'running' | 'success' | 'failed' | 'compensating' | 'compensated' | 'skipped'; /** * Lifecycle states for a flow execution. * * pending → running → (success | failed | compensating → compensated) */ type FlowStatus = 'pending' | 'running' | 'success' | 'failed' | 'compensating' | 'compensated'; /** * Serializable error shape stored in flow state. Full Error objects can't be * safely persisted because stack traces and prototypes are lost on clone. */ interface SerializedError { name: string; message: string; code?: string; stack?: string; transient?: boolean; } /** * Persisted state for a single step execution. Written to storage after every * transition so crashed executions can resume from the last successful step. * * For parallel step groups (created via {@link Flow.parallel}), each branch is * persisted under `branches[branchName]` as its own {@link StepState}. The * group itself uses the same `name`, `status`, `attempts` (= max across * branches), and timestamps as a regular step. Single steps leave `branches` * undefined — the field is purely additive and existing persisted states load * unchanged. */ interface StepState { name: string; status: StepStatus; attempts: number; startedAt?: number; endedAt?: number; result?: unknown; error?: SerializedError; compensationError?: SerializedError; /** * Set only for parallel step groups. Map of branch name → branch state. * Absent on regular sequential steps for backwards compatibility with * persisted state written by kompensa < 0.3. */ branches?: Record; /** * Marks this step as a parallel group. Useful when reading persisted state * to distinguish a group from a regular step that happens to have no * branches recorded yet. Absent on regular sequential steps. */ kind?: 'sequential' | 'parallel'; } /** * Persisted state for an entire flow execution. Keyed by (flowName, flowId). */ interface FlowState> { flowName: string; flowId: string; status: FlowStatus; input: TInput; steps: StepState[]; currentStepIndex: number; metadata: Record; createdAt: number; updatedAt: number; result?: TResults; error?: SerializedError; } /** * Retry policy controlling how many times a failing step is retried and how * delays are computed between attempts. */ interface RetryPolicy { /** Total attempts including the first try. Default: 1 (no retries). */ maxAttempts?: number; /** Curve applied to the delay across attempts. Default: `exponential`. */ backoff?: 'fixed' | 'linear' | 'exponential'; /** Delay before the second attempt in milliseconds. Default: 100. */ initialDelayMs?: number; /** Cap applied to any computed delay. Default: 30000. */ maxDelayMs?: number; /** Growth factor for exponential backoff. Default: 2. */ multiplier?: number; /** * Random jitter applied to delays. `true` means ±100% randomization; * a number between 0 and 1 caps the jitter fraction. Default: `true`. */ jitter?: boolean | number; /** * Decide whether a given error should trigger another attempt. Overrides * the default behavior (retry on everything that isn't a PermanentError). */ shouldRetry?: (error: unknown, attempt: number) => boolean; } /** * Context passed to every step `run` and `compensate` function. Includes the * original input, accumulated step results, per-step metadata, and signals for * cancellation. */ interface StepContext { input: TInput; results: TResults; metadata: Record; attempt: number; signal: AbortSignal; flowId: string; flowName: string; stepName: string; logger: Logger; } /** * Definition of a single step. `run` produces a result; `compensate` is the * semantic inverse invoked on downstream failure. */ interface StepDefinition { run: (ctx: StepContext) => TResult | Promise; compensate?: (ctx: StepContext, result: TResult) => void | Promise; retry?: RetryPolicy; /** Step timeout in milliseconds. Overrides flow-level default. */ timeout?: number; /** Skip this step when the predicate returns true. */ skipIf?: (ctx: StepContext) => boolean | Promise; } /** * Definition of a single branch inside a parallel step group. Identical to * {@link StepDefinition} except the branch name is the object key in the group * map rather than a separate argument. * * @internal — exposed via {@link ParallelStepDefinition}. */ type ParallelBranchDefinition = Omit, never>; /** * Map of branch name → branch definition. Used by {@link Flow.parallel}. * The TypeScript inference machinery picks each branch's result type from its * `run` return value, so `ctx.results..` is fully typed * in downstream steps. */ type ParallelStepDefinition>> = TBranches & {}; /** * Options for a parallel step group, passed as the third argument of * {@link Flow.parallel}. */ interface ParallelGroupOptions { /** * Group-level timeout in milliseconds. The group fails if all branches do * not settle within this window. Per-branch `timeout` still applies. Default: * undefined (no group-level timeout, only per-branch timeouts apply). */ groupTimeout?: number; /** * When `true`, compensation of branches in this group runs sequentially in * the reverse order branches completed. When `false` (default), compensation * runs in parallel via Promise.allSettled. Use sequential compensation only * when there is a causal dependency between branches that requires ordering. */ compensateSerially?: boolean; /** * When `true` (default), if any branch fails the group aborts the remaining * branches via the shared AbortSignal. When `false`, all branches run to * completion regardless of sibling failures (failures are still surfaced). * Fail-fast is the recommended default; disable only for observability or * when branches are fully independent. */ abortOnFailure?: boolean; } /** * Options accepted by Flow.execute. */ interface ExecuteOptions { /** * Unique key identifying this execution. Re-running with the same key * returns the cached result (if succeeded) or resumes from the last * successful step (if interrupted). */ idempotencyKey?: string; /** Abort the execution mid-flight. Respected between steps and during retry delays. */ signal?: AbortSignal; /** Free-form metadata available to all steps and hooks. */ metadata?: Record; /** Default timeout applied to steps without their own. */ timeout?: number; } /** * Minimal structured logger interface. Any subset of the levels may be * implemented; missing methods are silently ignored. */ interface Logger { debug(message: string, meta?: Record): void; info(message: string, meta?: Record): void; warn(message: string, meta?: Record): void; error(message: string, meta?: Record): void; child?(meta: Record): Logger; } interface FlowEventBase { flowName: string; flowId: string; metadata: Record; } interface FlowStartEvent extends FlowEventBase { input: TInput; resumed: boolean; } interface FlowEndEvent> extends FlowEventBase { status: FlowStatus; results?: TResults; error?: unknown; durationMs: number; } interface StepStartEvent extends FlowEventBase { stepName: string; stepIndex: number; attempt: number; } interface StepRetryEvent extends FlowEventBase { stepName: string; stepIndex: number; attempt: number; error: unknown; nextDelayMs: number; } interface StepEndEvent extends FlowEventBase { stepName: string; stepIndex: number; status: StepStatus; attempts: number; durationMs: number; result?: unknown; error?: unknown; } interface CompensateEvent extends FlowEventBase { stepName: string; stepIndex: number; status: 'compensating' | 'compensated' | 'failed'; error?: unknown; } /** * Optional lifecycle callbacks. All methods are called sequentially and may be * async; hook errors are logged but never interrupt the flow. */ interface FlowHooks { onFlowStart?(event: FlowStartEvent): void | Promise; onFlowEnd?(event: FlowEndEvent): void | Promise; onStepStart?(event: StepStartEvent): void | Promise; onStepRetry?(event: StepRetryEvent): void | Promise; onStepEnd?(event: StepEndEvent): void | Promise; onCompensate?(event: CompensateEvent): void | Promise; } /** * Handle returned by {@link StorageAdapter.acquireLock}. Call `release` when * done to free the lock. Safe to call multiple times; subsequent calls are * no-ops. `refresh` extends the TTL if the adapter supports it. */ interface Lock { release(): Promise; refresh?(): Promise; } /** * Options for {@link StorageAdapter.acquireLock}. */ interface AcquireLockOptions { /** Lock expiration in milliseconds. Set >= max expected execution time. */ ttlMs: number; /** Max time to wait for the lock. `0` fails fast; default 30_000. */ timeoutMs: number; } /** * Storage adapter contract. Implementations persist flow state so executions * can be resumed, deduplicated, and observed. * * `acquireLock` is optional but **strongly recommended** for adapters used in * multi-worker deployments. Without it, two workers handling the same * `idempotencyKey` will both execute the flow concurrently. */ interface StorageAdapter { load(flowName: string, flowId: string): Promise; save(state: FlowState): Promise; delete?(flowName: string, flowId: string): Promise; acquireLock?(flowName: string, flowId: string, options: AcquireLockOptions): Promise; } /** * Configuration injected into every flow. All ports are pluggable. */ interface FlowConfig { storage?: StorageAdapter; logger?: Logger; hooks?: FlowHooks; defaultRetry?: RetryPolicy; defaultTimeout?: number; /** * Lock TTL for this flow in ms. Only applies when the storage adapter * implements `acquireLock`. Default: 5 minutes. */ lockTtlMs?: number; /** * How long to wait for the lock before giving up. `0` fails immediately if * the lock is held. Default: 30 seconds. */ lockWaitMs?: number; } export type { AcquireLockOptions as A, CompensateEvent as C, ExecuteOptions as E, FlowConfig as F, Logger as L, ParallelBranchDefinition as P, RetryPolicy as R, StepDefinition as S, ParallelGroupOptions as a, SerializedError as b, FlowEndEvent as c, FlowHooks as d, FlowStartEvent as e, FlowState as f, FlowStatus as g, Lock as h, ParallelStepDefinition as i, StepContext as j, StepEndEvent as k, StepRetryEvent as l, StepStartEvent as m, StepState as n, StepStatus as o, StorageAdapter as p };