import type { PendingEventWait, WaitNodeConfig, WorkflowRun } from "../types.js"; import { type WorkflowBackend } from "../backends/types.js"; import type { WorkflowExecutor } from "../executor/workflow-executor.js"; export interface EventWaitManagerConfig { /** Backend for persistence */ backend: WorkflowBackend; /** Workflow executor used to resume a run once its wait is released */ executor?: WorkflowExecutor; /** Interval for sweeping timed-out waits parked by another process (ms) */ expirationCheckInterval?: number; /** Age after which an unfinished event claim is recoverable after a process exit (ms). */ deliveryClaimRecoveryDelay?: number; /** Interval for discovering event claims abandoned by another process (ms). */ claimRecoveryCheckInterval?: number; /** Enable debug logging */ debug?: boolean; } /** * What `publishEvent` did with an event. * * A boolean cannot carry this: "no wait was released" covers an event safely * buffered for a run that has not parked yet, an event dropped because the run * is already over, and a wait that matched but could not be delivered. Those * need different reactions from the caller, so they are named. */ export type PublishEventOutcome = /** A wait matched, its node completed, and the run was nudged forward. */ "delivered" /** No wait matched yet. The event is durably buffered until one does. */ | "buffered" /** The run is already terminal, so the event was discarded rather than buffered. */ | "run-terminal" /** * A wait matched but delivering it failed. The wait and the event were both * rolled back, so the run is still parked and `retryEventDelivery` can retry * the same envelope without appending a duplicate. */ | "delivery-failed"; /** * Persists what a run is parked on, and releases it when the event arrives or * its declared timeout elapses. * * This mirrors `ApprovalManager`. The differences are forced by what an event * is: nobody addresses an event to a specific wait record, so delivery matches * on the event name against a per-run durable mailbox, and an event that * arrives before its node parks is buffered there rather than dropped. A wait * with a declared timeout is released by whichever of the two happens first, * and the backend's atomic resolve decides that race. */ export declare class EventWaitManager { private readonly config; private expirationTimer?; /** Independent discovery for claims abandoned after this manager starts. */ private claimRecoveryCheckTimer?; private claimRecoveryCheck?; /** In-process deadline timers, keyed by wait id, so a short delay fires promptly. */ private readonly expiryTimers; /** Shared accumulator and completion barrier for same-backend, same-run drain passes. */ private readonly drainSessions; /** Best-effort prompt retries; durable claims remain the restart fallback. */ private readonly finalizationRetryTimers; private readonly finalizationRetryAttempts; /** Best-effort retries for mail drained after its publisher already returned. */ private readonly deliveryRetryTimers; private readonly deliveryRetryAttempts; /** Prompt retries for a committed node whose run resume did not complete. */ private readonly committedResumeRetryTimers; private readonly committedResumeRetryAttempts; /** Same-process timeout claims still reconciling their matching run transition. */ private readonly activeTimedWaitClaims; private destroyed; constructor(config: EventWaitManagerConfig); /** * Start the periodic sweep, once, for the life of this manager. * * It has to start here rather than on first use. The sweep's whole job is * waits this process did not park, above all waits that outlived the process * that parked them: after a restart nothing local ever parks or publishes, * so a sweep armed on first use would never arm at all and those waits would * never reach their declared deadline. The timer is unreferenced, so a * process holding only this is still free to exit. */ private ensureExpirationChecker; /** Discover abandoned claims independently of deadline expiration sweeping. */ private ensureClaimRecoveryChecker; private checkAbandonedClaims; /** * Record a run parking on a wait-for-event or delay node. * * Persisting happens before draining the mailbox so an event published in * between is matched by whichever side sees the other first, rather than * falling between them. */ createEventWait(run: WorkflowRun, nodeId: string, waitConfig: WaitNodeConfig): Promise; private buildPendingEventWait; private persistOwnedEventWait; private persistOwnerlessEventWait; private rearmExpiryAfterSlowPersistence; /** * Find the durable handoff for a wait creation that did not remain pending. * A delivery or timeout may claim and even finalize the record while the * backend's save promise is still settling; that is successful persistence, * not a lost append. */ private findWaitExecutionRecord; /** Drain buffered events after a complete waiting batch has been persisted. */ drainPendingEvents(runId: string): Promise; /** Release an in-process deadline after another owner resolves the wait. */ clearWaitExpiry(waitId: string): void; /** Arm this process for a timed wait that was already persisted elsewhere. */ rearmLiveWaitExpiry(run: WorkflowRun, nodeId: string): Promise; /** * Buffer an event for a run and release any wait it matches. * * The event is buffered before it is matched. A publish that arrives before * its node parks, or while no process holds the run, must survive until a * wait can claim it, which is exactly what a run-scoped mailbox provides and * a subscription does not. * * A run that has already finished is the exception. It will never park on * anything again, so buffering for it would report a durable delivery that * can never happen. Such a publish is refused up front and reported as * `run-terminal` instead. */ publishEvent(runId: string, eventName: string, payload?: unknown): Promise; /** Retry the oldest buffered envelope without publishing a duplicate. */ retryEventDelivery(runId: string, eventName: string): Promise; private assertPublicEventName; private classifyFailedRunAppend; private markEventPublicationActive; private clearEventPublicationActive; private recordDeliveredEventReceipt; /** * Observe a delivery committed by another process before its receipt write. * * A finalization retry leaves the durable claim in place. Once that claim's * wait node is completed, the publisher can report delivery without relying * on process-local attribution or waiting for the receipt write to recover. * Read the claim before the receipt so finalization racing this check is * visible on at least one side of the transition. */ private hasCommittedEventDelivery; private consumeDeliveredEventReceipt; /** Read the waits a run is currently parked on. */ getPendingEventWaits(runId: string): Promise; /** * Match this run's pending waits against its buffered events. * * Reports what happened, so a publish can distinguish waking a run from * merely buffering, and from matching a wait it could not deliver. * * Every wait is attempted independently. Delivery touches the run, which can * fail for reasons that have nothing to do with the other waits parked on it, * and one such failure must not stop the rest of this run's mail. */ private drain; private createDrainOutcome; private createDrainSession; private recoverDrainSession; private finishDrainPass; private drainWait; private expireUnclaimedOverdueWait; private handleDrainDeliveryError; private applyDrainDeliveryOutcome; /** Re-buffer an event claimed by a wait execution a checkpoint discarded. */ private retireStaleDelivery; private finalizeDelivery; private scheduleFinalizationRetry; private clearFinalizationRetry; private committedResumeRetryKey; /** * Keep the durable claim as a reconciliation signal until a committed node * has actually nudged its run. A process exit leaves that claim enumerable * for the next manager; this timer only makes the same-process retry prompt. */ private scheduleCommittedResumeRetry; private clearCommittedResumeRetry; private reconcileCommittedResume; private scheduleDeliveryRetry; private clearDeliveryRetry; private finalizeTimedWaitClaim; /** Recover durable claims left behind before delivery or finalization committed. */ private recoverAbandonedDeliveries; private reserveAbandonedDeliveryClaim; private recoverAbandonedDeliveryClaim; private recoverCommittedDeliveryClaim; private reserveAbandonedTimedWait; private recoverAbandonedTimedWait; private recoverCommittedTimedWait; /** Recover abandoned claims and promptly retry any restored mailbox state. */ private recoverAndDrainAbandonedDeliveries; /** * Undo a claimed-but-undelivered event so the run stays wakeable. * * The wait and the event are restored by ONE backend operation, not two. * Committed as separate calls, a crash after the wait restore but before * the event restore would leave the wait pending while its event is gone * from the mailbox forever: the sweep cannot recover an untimed wait, and * an event the publisher was told was accepted would be lost unless it * happened to retry. `restoreRunEventDelivery` closes that window. * * The backend restores the event to its publication-order position. Two * concurrent claims can roll back in either order, so blindly prepending * each one would reverse otherwise FIFO delivery. */ private rollBackDelivery; /** * Return a wait this process claimed to pending, and re-arm its deadline. * * This is the no-event path (a delay whose delivery failed, an expired * deadline whose run failure did not commit); a claimed EVENT is given back * through `rollBackDelivery`, whose backend operation restores the wait and * the event atomically. */ private restoreClaimedWait; /** * Re-arm the deadline of a wait whose record really came back to pending. * * Only for restored records: a claim that could not be given back belongs * to whoever holds it now. An already-elapsed deadline is left to the * periodic sweep when one exists. Managers with sweeping disabled re-arm it * with a bounded minimum delay, so a failed delay delivery remains live * without spinning on every event-loop tick. */ private rearmRestoredWaitExpiry; private nodeOutcomeCommitted; private deliver; /** * Fire a wait's deadline in this process. * * The periodic sweep alone cannot serve `delay()`: a 200ms delay must not * wait out a 60s poll. The sweep remains the recovery path for a wait whose * process died with this timer in it. */ private scheduleExpiry; private clearExpiry; private clearOwnedExpiry; /** * Apply a wait's declared timeout. * * A delay's timeout is its whole purpose, so reaching it completes the node. * A wait-for-event's timeout means the event never came, which fails the run * the same way an expired approval does. Either way the run stops consuming * capacity on a deadline it declared and then ignored. */ private expire; private shouldDeferEventExpiry; private completeClaimedDelay; private retireStaleTimedWait; private applyClaimedEventExpiry; /** * Fail a run whose wait-for-event deadline passed, marking the wait node * itself failed in the same patch. * * The node state matters for `retry()`: a wait left recorded `running` is * deliberately never re-scheduled by the DAG, so a retried run would stall * again immediately. A failed node is re-armed by retry like any other * failure. * * Resolves `true` when the failure was applied, or when the run can never be * retried and there is nothing left to apply; `false` when the conditional * update lost a race or a failed run still owns the replayable wait. */ private failRunForExpiredWait; /** * Release every wait whose deadline has already passed. * * Each record is expired on its own. This sweep spans every run on the * backend, so one run whose expiry cannot be applied must not deny every * other run its deadline. */ checkExpiredEventWaits(): Promise; private sweepPendingWait; private readSweepRunStatus; private cleanUpTerminalWait; private drainActiveSweepRun; /** Stop the manager and drop every timer it owns. */ stop(): void; } //# sourceMappingURL=event-wait-manager.d.ts.map