/** * Wait-continuation dispatch: delay + idempotency-key selection. * * When V2 suspension processing observes a pending wait, it enqueues a * delayed "continuation" message that fires once the wait elapses and * drives the next replay (which completes the wait via the "complete * elapsed waits" pass). This module decides the message's `delaySeconds` * and `idempotencyKey`. * * The continuation is keyed on the wait's correlationId: while a wait is * pending, every replay pass over the run re-observes it (e.g., once per * step completion in `Promise.all([steps..., sleep()])`), and without * dedupe each pass would enqueue another delayed continuation — each one * a spurious full replay when the wait elapses, and each a fresh message * that resets the delivery-attempt runaway guard. A key is attached in * all cases: some worlds (e.g. world-postgres) serialize key-less * workflow messages per run, which would park the continuation behind * the handler's own inline step execution and defeat the race semantics * the continuation exists to provide. * * The bare correlationId cannot be the key in every case, though: world * dedupe windows outlive the first delivery (VQS keeps idempotency * records until message-retention TTL; world-postgres keeps a * completed-keys cache), so once a key has been used, a later enqueue * under the same key is silently dropped. Any situation where a * continuation is delivered while its wait is still pending therefore * needs a fresh key for the re-enqueue, or the wait's timer is lost and * the run stalls until unrelated traffic happens to wake it. Two such * situations exist, each with its own key variation: * * - Waits longer than the maximum queue delay are chained: the delay is * clamped to `WAIT_CONTINUATION_MAX_DELAY_SECONDS` (23h — VQS messages * have a 24h retention limit, and one hour of buffer matches * world-vercel's own clamp for delayed re-enqueues), so the * continuation intentionally fires early, re-observes the wait, and * must enqueue the next hop. The key is suffixed with the hop index * (`ceil(timeoutSeconds / maxDelay)`): stable for every re-observation * within the same hop window (so passes dedupe), decremented at each * hop delivery (so the chain always advances). Worlds without a delay * limit (world-postgres, world-local) simply take the same ≤23h hops. * * - Near-elapsed waits (≤2s remaining) get a second-bucketed suffix. A * continuation delivered marginally early (clock skew between the * enqueuing and handling hosts; the ceil() on the delay can leave a ~0 * margin) re-observes the wait as pending with ~1s remaining and must * be able to enqueue a fresh short-delay retry. The bucket suffix * keeps that retry enqueueable (its ≥1s delay guarantees a later * bucket) while still collapsing same-instant duplicates. * * Mid-range waits (more than the near-elapsed threshold, at most one * hop) use the bare correlationId: every re-observation targets the same * deadline, so deduping to the first message is semantically lossless. * Host clock skew beyond the near-elapsed threshold could in principle * deliver such a continuation early enough to re-observe its wait and * lose the re-enqueue to the burnt key; the threshold is the skew * tolerance we accept for the benefit of exactly-one continuation per * wait. */ /** * Maximum `delaySeconds` for a single wait-continuation message. Waits * longer than this are chained across multiple hops. 23 hours: VQS * messages have a 24h retention limit; the 1h buffer mirrors * world-vercel's `MAX_DELAY_SECONDS`. */ export declare const WAIT_CONTINUATION_MAX_DELAY_SECONDS = 82800; /** * Waits with at most this many seconds remaining use a second-bucketed * idempotency key so an early-delivered continuation can re-enqueue its * short-delay retry. This is also the host clock-skew tolerance for * mid-range waits keyed on the bare correlationId. */ export declare const NEAR_ELAPSED_WAIT_THRESHOLD_SECONDS = 2; export interface WaitContinuationDispatch { delaySeconds: number; idempotencyKey: string; } /** * Computes the queue delay and idempotency key for a wait-continuation * message. `timeoutSeconds` is the time until the wait's `resumeAt` * (floored at 1s by the suspension handler); `waitCorrelationId` * identifies the wait so repeated suspension passes dedupe. */ export declare function getWaitContinuationDispatch(timeoutSeconds: number, waitCorrelationId: string, now?: number): WaitContinuationDispatch; //# sourceMappingURL=wait-continuation.d.ts.map