EventConsumer
Defined in: packages/server/src/events/consumer.ts:248
Properties
Section titled “Properties”drainOnce
Section titled “drainOnce”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).
Parameters
Section titled “Parameters”options?
Section titled “options?”Returns
Section titled “Returns”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.
Returns
Section titled “Returns”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.
Returns
Section titled “Returns”Promise<void>