import type { Span } from '#compiled/@opentelemetry/api/index.js'; import { type SerializedData, type WorkflowRun, type World } from '#compiled/@workflow/world/index.js'; import type { StepInvocationQueueItem, WorkflowSuspension } from '../global.js'; import { type LoadedEventLog } from './helpers.js'; import { ReplayRecoveryReporter } from './replay-recovery-reporter.js'; export interface SuspensionHandlerParams { suspension: WorkflowSuspension; world: World; run: WorkflowRun; span?: Span; requestId?: string; /** * The runtime's loaded event log. Every event creation this suspension makes * is sent with the precondition snapshot derived from it, so a backend that * has recorded an event the replay did not see rejects the write with a 412 * instead of accepting a divergent event. The rejection is not retried here: * the event's correlation id was minted by *this* replay's seeded sequence, * so re-committing it against a corrected log would persist an event no * correct replay produces. The caller restarts the replay instead. */ eventLog?: LoadedEventLog; /** * Turbo mode only: a promise that resolves once the backgrounded * `run_started` has landed (the run exists). When present, every world write * this suspension performs (`hook_created`, `wait_created`, eager overflow * `step_created`, …) is gated on it so the write never races ahead of the * run's creation. The pure inline hot path defers all of its steps and writes * nothing here, so it never awaits this barrier. `undefined` outside turbo, * where `run_started` was already awaited up front. */ runReadyBarrier?: Promise; /** One-shot telemetry reporter, activated only after replay has recovered. */ replayRecoveryReporter?: ReplayRecoveryReporter; } /** * Result of handling a suspension. Returns pending step items so the caller * can decide which to execute inline vs queue to background. */ export interface SuspensionHandlerResult { /** Pending step items with events created but NOT queued */ pendingSteps: StepInvocationQueueItem[]; /** * Correlation IDs for which this suspension call actually wrote the * step_created event (as opposed to catching EntityConflictError because * a concurrent handler wrote it first). Only the handler that wrote the * step_created event should queue / inline-execute the step — this * guarantees a single owner per step, even when multiple handlers race * into the same batch boundary. */ createdStepCorrelationIds: Set; /** * The steps whose `step_created` writes were intentionally deferred so the * caller can run them inline via lazy `step_started` events (which create * the step on the fly), saving one world round-trip per inline step. Up to * `getMaxInlineSteps()` steps are deferred; the caller runs them inline in * parallel and queues the rest. Empty when no step was deferred (nothing * pending, or a `hook.getConflict()` awaiter is present so nothing is * executed inline). The caller passes each `dehydratedInput` straight to * `executeStep`, which sends it as the `step_started` payload. The atomic * create-claim inside each `step_started` is the exactly-one-owner gate that * the standalone `step_created` provided before: the loser of the race gets * `EntityConflictError` → `skipped` and does not run the body. */ lazyInlineSteps: Array<{ correlationId: string; stepName: string; dehydratedInput: SerializedData; }>; /** * The soonest pending wait, if any: seconds until it elapses and the * correlationId of the wait that produced that timeout. The * correlationId seeds the idempotency key for the wait-continuation * queue message so that repeated suspension passes over the same * pending wait collapse into a single delayed continuation. */ waitTimeout?: { seconds: number; correlationId: string; }; /** Whether a hook conflict was detected (should re-invoke immediately) */ hasHookConflict: boolean; /** Whether a `hook.getConflict()` awaiter needs the workflow to continue immediately */ hasAwaitedHookCreation: boolean; /** Whether native workflow attribute events were written for replay. */ hasAttributeEvents: boolean; /** * Whether this suspension created any hook (`hook_created`) events. Unlike * `hasHookConflict` / `hasAwaitedHookCreation`, this is true even for a plain * fire-and-forget hook with no conflict and no awaiter. Turbo mode uses it to * detect "a hook was created this suspension" and stop forcing optimistic * inline start (a hook introduces later resume invocations that could race). */ hasHookEvents: boolean; /** * Wall-clock ms spent committing this suspension's `hook_created` events * (0 when it created none). The caller accumulates this across iterations * and subtracts it from the TTFS latency measurement, so time spent * durably creating the user's hooks doesn't count as runtime overhead. */ hookCreationMs: number; /** * Whether serializing this suspension's new step inputs was passive (did * not execute workflow-owned code such as getters, proxy traps, or custom * serializers). `false` means the retained VM may have diverged from what * a cold replay would compute, so the caller must demote to replay. */ retainedStepInputsSafe: boolean; } /** * Handles a workflow suspension by processing all pending operations (hooks, steps, waits). * Creates events for all operations but does NOT queue step messages — returns the pending * steps so the caller can decide which to execute inline vs queue to background. * * Processing order: * 1. Hooks are processed first to prevent race conditions with webhook receivers * 2. Step events and wait events are created in parallel */ export declare function handleSuspension({ suspension, world, run, span, requestId, eventLog, runReadyBarrier, replayRecoveryReporter, }: SuspensionHandlerParams): Promise; //# sourceMappingURL=suspension-handler.d.ts.map