angzarr_client.router.dispatch

Dispatch logic for the unified Router runtime.

In R6 this module provides only the command-handler dispatch path with a fresh state per call. State rebuild (R7), multi-handler merge (R8), seq incrementing (R9), rejection (R10), saga/PM/projector (R11-R13) land in subsequent rounds.

Attributes

Exceptions

DispatchError

gRPC-compatible dispatch error carrying a StatusCode.

Functions

dispatch_command(...)

Dispatch a ContextualCommand to the matching @handles or @rejected method.

dispatch_fact(→ angzarr_client.proto.angzarr.EventBook)

Dispatch a FactRequest through @handles_fact methods.

dispatch_replay(→ Any)

Replay events on top of a base snapshot to compute final state.

dispatch_saga(→ angzarr_client.proto.angzarr.SagaResponse)

Dispatch a source event through registered saga handlers.

dispatch_process_manager(...)

Dispatch a trigger event through registered process-manager handlers.

dispatch_projector(...)

Fan out each event in events to matching @handles methods on all

dispatch_upcaster(...)

Transform events via registered @upcaster handlers.

Module Contents

angzarr_client.router.dispatch.Factory
exception angzarr_client.router.dispatch.DispatchError(code: grpc.StatusCode, details: str, *, error_code: str = '', extras: dict[str, Any] | None = None)

Bases: grpc.RpcError

gRPC-compatible dispatch error carrying a StatusCode.

Raised when a command cannot be routed (unknown type, unknown domain), when a request is malformed (missing command/page/cover), or for any other caller-facing violation. The gRPC servicer can read .code() and forward the status to the client.

Audit finding #59: message is a static string (no interpolation); runtime context like type URLs, domains, etc. ride in extras. The SCREAMING_SNAKE error_code is a stable cross-language identifier suitable for cucumber assertions.

Initialize self. See help(type(self)) for accurate signature.

error_code = ''
extras: dict[str, str]
code() grpc.StatusCode
details() str
angzarr_client.router.dispatch.dispatch_command(factories: list[Factory], request: angzarr_client.proto.angzarr.ContextualCommand) angzarr_client.proto.angzarr.BusinessResponse

Dispatch a ContextualCommand to the matching @handles or @rejected method.

Command path: routes by (domain, type_url). Audit #62: at most one handler per (domain, type_url) — duplicate registration is a build-time error (BuildError from router/builder.py:115-138), so this loop matches at most one factory and returns at the first match. Mirrors Rust runtime.rs:43-108.

Notification path: routes rejection compensation to @rejected handlers; that path does fan out across every matching @rejected set (sagas / multi-handler compensation legitimately broadcast).

angzarr_client.router.dispatch.dispatch_fact(factories: list[Factory], request: Any) angzarr_client.proto.angzarr.EventBook

Dispatch a FactRequest through @handles_fact methods.

Audit #45. Walks each fact in request.facts, finds the matching @handles_fact(EventType) method, rebuilds state from request.prior_events, invokes the method, and concatenates emitted events into the returned EventBook.

Caller (the gRPC adapter) is responsible for gating on CommandHandlerRouter.supports_handle_fact() first — when no handler declares any @handles_fact, the adapter returns UNIMPLEMENTED without invoking dispatch. Once dispatch IS invoked, individual fact events with no matching @handles_fact are silently skipped (the framework returns whatever events the matching handlers emitted; the coordinator persists the original facts as-is via its own path).

angzarr_client.router.dispatch.dispatch_replay(factories: list[Factory], request: Any) Any

Replay events on top of a base snapshot to compute final state.

Audit #45. Decodes the base snapshot’s state into the registered state type, applies request.events through the aggregate’s @applies methods, and returns the resulting state wrapped in an Any inside a ReplayResponse.

Caller (the gRPC adapter) is responsible for gating on CommandHandlerRouter.supports_replay() first.

Replay uses the FIRST registered handler’s state type and applier set — multi-handler routers are forbidden post-#62 so there is at most one. If somehow none is registered, returns an empty ReplayResponse.

angzarr_client.router.dispatch.dispatch_saga(factories: list[Factory], request: angzarr_client.proto.angzarr.SagaHandleRequest) angzarr_client.proto.angzarr.SagaResponse

Dispatch a source event through registered saga handlers.

The trigger is the last event in request.source; sagas whose @saga(source=...) matches the source book’s domain AND whose @handles(Evt) matches the trigger’s proto type are all invoked in registration order. Emitted commands + fact books are merged.

angzarr_client.router.dispatch.dispatch_process_manager(factories: list[Factory], request: Any) angzarr_client.proto.angzarr.ProcessManagerHandleResponse

Dispatch a trigger event through registered process-manager handlers.

Trigger is the last event in request.trigger; PMs whose sources include the trigger’s domain AND whose @handles(Evt) matches the trigger type are invoked in registration order. Each PM rebuilds its own state from request.process_state via its @applies methods. ProcessManagerResponse returns merged into a single ProcessManagerHandleResponse.

angzarr_client.router.dispatch.dispatch_projector(factories: list[Factory], events: angzarr_client.proto.angzarr.EventBook) angzarr_client.proto.angzarr.Projection

Fan out each event in events to matching @handles methods on all registered projectors that declare the source domain.

Projector handlers are side-effect only — their return value is ignored. Returns a Projection with the source cover copied through for framework bookkeeping.

One handler instance is constructed per matching projector per dispatch call (via its factory) and reused across every event in the book, so a projector can accumulate state within a single projection run.

angzarr_client.router.dispatch.dispatch_upcaster(factories: list[Factory], request: angzarr_client.proto.angzarr.UpcastRequest) angzarr_client.proto.angzarr.UpcastResponse

Transform events via registered @upcaster handlers.

Only handlers whose @upcaster(domain=...) matches request.domain are consulted. Every matching upcaster runs in registration order; the output of one is the input of the next — chained transforms let schema evolution compose across versions (V1 → V2 → V3) without forcing each upcaster to know about every newer version. The @upcasts(From, To) match is exact on the current event’s type_url, so an upcaster only fires when its From matches the incoming/transformed value. Events with no matching transform pass through unchanged.

Audit finding #43 (cucumber C-0136 / C-0137): mirrors Rust’s router/upcaster.rs::dispatch semantics. Was previously first-match-wins (broke after first transform).