dispatch
dispatch(strategy, ...targets): RouteBuilder<Current>
Run exactly one of several targets, chosen by a load-balancing strategy. The sibling of multicast (all targets) and choice (one target by predicate); dispatch is one target by strategy. A target is a bare destination, a sub-pipeline callback (b) => b... (the same path surface as multicast), or either wrapped in weighted(...) to co-locate a relative weight.
.from(http("/jobs"))
.dispatch("round-robin", workerA, workerB, workerC)
.to(next); // runs on the original exchange after the selected target settles
The leading strategy argument is required: there is no safe default, because each strategy makes a materially different routing decision.
Strategies
failover-- try targets in order until one succeeds. A target that deliberately drops the exchange counts as handled; only a genuine failure fails over. The preferred-target cursor persists across exchanges, so a healthy target keeps serving and a dead one is not re-probed every exchange (it does not auto-revert to a recovered earlier target until the current one fails). Pairs naturally with per-target.retry()/.circuitBreaker().round-robin-- hand out targets in order, cycling.weighted-- distribute by theweighted()weights using smooth weighted round-robin, so the distribution matches the weights and is deterministic rather than random. Un-weighted targets default to weight 1.sticky-- exchanges sharing akeygo to the same target. New keys are round-robined across targets and remembered in an LRU-bounded affinity map. Object form only, becausekeyis required.
// Failover: primary, then secondary if it fails.
.dispatch("failover", primary, secondary)
// Weighted canary: ~95% to stable, ~5% to canary.
.dispatch("weighted", weighted(stable, 95), weighted(canary, 5))
// Sticky sessions: one user's traffic always lands on one worker.
.dispatch({ strategy: "sticky", key: (ex) => ex.body.userId }, workerA, workerB)
Semantics
- Side-effect-only. The selected target runs on its own deep clone (fresh id, preserved correlation id) and the ORIGINAL exchange continues downstream unchanged, so the body type is preserved and a target's output is unconstrained. Dispatch waits for the selected target to settle before the original continues.
- Error isolation. A target that throws fires its own clone's error events (
route:error/route:exchange:failed) but does not fail the route or the dispatch step. Forfailover, a failure advances to the next target; if every target fails,route:operation:dispatch:exhaustedfires and the original still continues. - Per-route state. The round-robin cursor, the failover cursor, the weighted running weights, and the sticky affinity map are kept per route, so distinct contexts running the same route definition never cross-route each other's traffic.
A bare destination must be an object destination ({ send }); a callable destination (a bare function with a send method) is indistinguishable from a sub-pipeline callback at runtime, so wrap it as (b) => b.to(callableDest).
The sticky affinity map is bounded by maxKeys (default 10,000). When the cap is reached the least-recently-seen key is evicted and its next occurrence is reassigned (possibly to a different target):
.dispatch({ strategy: "sticky", key: (ex) => ex.body.userId, maxKeys: 50_000 }, a, b)
Events
route:operation:dispatch:selected--{ routeId, exchangeId, correlationId, strategy, targetIndex }, fired when a target is chosen to run. Forfailover, fired once per attempt.route:operation:dispatch:exhausted--{ routeId, exchangeId, correlationId, strategy: "failover", targetCount }, fired whenfailoverruns out of targets and none handled the exchange.