debounce
debounce(options: { waitMs: number; key?; maxWaitMs? }): RouteBuilder<Current>
Suppress bursts of exchanges, releasing only the last one in a burst after a quiet period. Useful when only the final state matters: file-system change batching, search-as-you-type, or collapsing a flurry of webhook retries.
.id('file-watcher')
.from(file({ path: './config', watch: true }))
.debounce({ waitMs: 500 }) // wait for editing to finish
.process(reloadConfig)
.to(log())
Each arrival is held (not passed downstream) and resets a waitMs quiet timer; a newer arrival supersedes and drops the one being held. When the timer finally fires, the held exchange is released through the steps that follow .debounce().
Options
waitMs(required) -- the quiet window in milliseconds. An exchange is released only afterwaitMselapses with no newer arrival in its group.key-- a selector that debounces independently per group, e.g. one window per file path:key: (ex) => ex.body.filePath. When omitted, the whole route shares a single window.maxWaitMs-- an upper bound on how long an exchange may be held, measured from the START of its burst and never reset. It guarantees eventual release under continuous activity (otherwise a steady stream of arrivals could resetwaitMsforever and starve the trailing edge). Must be>= waitMs.
// Per-path debounce with a 5s ceiling on continuous edits.
.debounce({ waitMs: 500, key: (ex) => ex.body.filePath, maxWaitMs: 5000 })
Semantics
- Trailing edge. Only the last exchange in a burst is released; earlier ones are dropped. State is per-route (and per
keygroup). - Held outside the queue. Debounce is the one operation that breaks the "process each exchange immediately" model: it holds an exchange outside the pipeline queue and re-runs it later. A released exchange runs the steps after
.debounce()as a fresh exchange (new id, preserved correlation id) with its ownexchange:started/:completedlifecycle. Because the released exchange is the route's primary flow, the detached run honors the route-scope.error()handler and enforces the route's.output()schemas before completing. - Balanced lifecycle. Every arrival ends in
route:exchange:droppedwith reason"debounced": superseded arrivals when a newer one replaces them, and the absorbed trailing arrival at release time (its content continues as the released clone). No arrival id is ever left without a terminal event. - Flush on drain / shutdown. A pending exchange is released promptly when the route drains or shuts down (release reason
"flush"), rather than being lost or waiting out its timer. - Route scope only, not wrappable. Debounce is a route-scope operation: it is deliberately not available inside a fan-out path (a held exchange has no meaning inside a transient path clone), and step-scope wrappers (
.retry()/.error()/.timeout()/ ...) refuse to wrap it at build time. Its execute never fails per-exchange, so a wrapper could never trigger, and it would falsely suggest coverage of the released exchange's downstream failures. Wrap the steps downstream of.debounce()instead.
Events
route:operation:debounce:held--{ routeId, exchangeId, correlationId, key? }, fired when an arrival is held and the quiet timer is armed or reset.route:operation:debounce:dropped--{ routeId, exchangeId, correlationId, key? }, fired when a held exchange is superseded by a newer arrival in the same burst.route:operation:debounce:released--{ routeId, exchangeId, correlationId, key?, reason }, fired when the trailing exchange is released.reasonis"quiet"(the window closed),"maxWait"(the cap fired during continuous activity), or"flush"(a drain / shutdown released it early).