Skip to content

EventConsumer

Defined in: packages/server/src/events/consumer.ts:248

drainOnce: (options?) => Promise<EventDrainSummary>

Defined in: packages/server/src/events/consumer.ts:296

One bounded drain pass — the pacing mode for a host that cannot hold a process (ADR-0053 amendment, 2026-08-02).

It walks every configured Event stream, read → deliver → ack, and keeps going until EITHER every stream has returned an empty read OR the wall-clock budget is spent. Same internals as the loop mode throughout: the same read/deliver/ack path, the same lease renewal while a callback runs, the same per-sub-batch retry-by-lapsing-lease, the same dead-lettering after maxAttempts with the same onDeadLetter hook and unconditional warn log. It never sleeps: a stream that reads empty is finished for this pass, and there is no adaptive interval to wait out.

The budget is checked BETWEEN sub-batches, never inside one. A callback already running is awaited and acked exactly as the runner would (its lease stays renewed throughout), and no new read starts once the budget is gone — so a pass can overrun its budget by one callback, and budgetMs must leave head-room for that under the platform’s invocation cap. A sub-batch whose callback THROWS near the budget edge is not special-cased: it is left unacked and dropped from renewal, its lease lapses, and the queue redelivers it on a later pass with an incremented delivery count. That is at-least-once working as designed, not a lost event — and it is why the callback must be idempotent.

Hosting it. Wire a SCHEDULED invocation (a platform cron, e.g. every 10 s) that calls drainOnce and, optionally, an ingest-side nudge (createSyncServer({ onEventsEnqueued }) firing a fetch-and-forget at the same endpoint) so an interactive append is drained in milliseconds instead of waiting for the next tick. The schedule is the delivery guarantee; the nudge is only latency — a lost nudge costs nothing but time. Overlapping invocations are SAFE: two processes reading the same queue are arbitrated by the visibility timeout, exactly as two long-lived runners would be. (Two passes on ONE handle are not — that is a bug, and throws; see below.)

One handle, one pacing mode. Throws if start() is live, if another drainOnce is already in flight on this handle, or if the handle has been stopped — the lifecycle is one-way, so the next drain builds a fresh defineEventConsumer (construction is query-free, so that costs nothing).

EventDrainOptions

Promise<EventDrainSummary>


start: () => void

Defined in: packages/server/src/events/consumer.ts:253

Begin every stream’s loop. Idempotent; a no-op after EventConsumer.stop. Throws while a EventConsumer.drainOnce pass is in flight — one handle drives one pacing mode at a time.

void


stop: () => Promise<void>

Defined in: packages/server/src/events/consumer.ts:264

Graceful stop: no new reads, no new callbacks, in-flight callbacks are awaited (and their leases KEPT renewed until they ack or dead-letter), and the promise resolves when every loop — and every renewal task it still owned — is quiet. Safe to call twice (the second call awaits the first). Messages of an in-progress read whose callback had not started are released at once: they stop being renewed and are left unacked, so at-least-once redelivers them as soon as their lease lapses.

A drainOnce pass in flight is treated the same way: it starts no further read, finishes the callback it is on, and stop() resolves once it has.

Promise<void>