pub struct Dispatcher<CallerCtx> { /* private fields */ }Expand description
Dispatches a JmapRequest to registered method handlers.
Register handlers with Dispatcher::register, then call
Dispatcher::dispatch per request. CallerCtx is cloned for each
method call in the batch, so it must be Clone.
CallerCtx must also be 'static because each handler call is spawned as
a tokio::task. To share non-static data (e.g. a database connection),
wrap it in Arc<T> — Arc is Clone + Send + 'static when T: Send + Sync.
§Thread safety
Dispatcher is both Send and Sync. Register handlers on one thread,
then wrap in Arc and share across tasks — dispatch takes &self and is
safe to call concurrently.
Implementations§
Source§impl<CallerCtx: Clone + Send + 'static> Dispatcher<CallerCtx>
impl<CallerCtx: Clone + Send + 'static> Dispatcher<CallerCtx>
Sourcepub fn register(
&mut self,
method: impl Into<String>,
handler: Arc<dyn JmapHandler<CallerCtx>>,
)
pub fn register( &mut self, method: impl Into<String>, handler: Arc<dyn JmapHandler<CallerCtx>>, )
Register a handler for the given method name.
Registering the same name twice silently replaces the earlier
handler. This is a real foot-gun in the workspace pattern where
each extension crate ships a register_*_handlers macro that
registers ~10 method names against a single Dispatcher: a typo
or accidental double-registration drops one binding with no
diagnostic, and the handler that “never fires” is hard to debug
(bd:JMAP-jfia.4). Prefer Dispatcher::try_register in new
code; register is kept for ergonomic call sites where the
silent-overwrite is the deliberate choice (e.g. a test fixture
that overrides a handler for a specific scenario).
Using Arc rather than Box allows the same handler instance to be
shared across multiple method name registrations (via Arc::clone).
Sourcepub fn try_register(
&mut self,
method: impl Into<String>,
handler: Arc<dyn JmapHandler<CallerCtx>>,
) -> Result<(), DuplicateMethodError>
pub fn try_register( &mut self, method: impl Into<String>, handler: Arc<dyn JmapHandler<CallerCtx>>, ) -> Result<(), DuplicateMethodError>
Register a handler for the given method name, returning an error if the method name is already registered.
This is the recommended registration entry point for production
code paths (bd:JMAP-jfia.4). Unlike Dispatcher::register, a
duplicate method name produces
DuplicateMethodError rather than silently replacing the
existing handler, surfacing the collision at the call site that
caused it.
On Err(DuplicateMethodError) the dispatcher’s handler map is
left unchanged — the existing handler is preserved and the
would-be registration is dropped.
Using Arc rather than Box allows the same handler instance to be
shared across multiple method name registrations (via Arc::clone).
§Errors
Returns DuplicateMethodError when method is already
registered.
Sourcepub async fn dispatch(
&self,
request: JmapRequest,
caller: CallerCtx,
session_state: State,
) -> JmapResponse
pub async fn dispatch( &self, request: JmapRequest, caller: CallerCtx, session_state: State, ) -> JmapResponse
Process a validated JmapRequest and return a JmapResponse.
Method calls are processed sequentially per RFC 8620 §3.3. Each
handler runs in a tokio::task::spawn for panic isolation: a panicking
handler returns a serverFail invocation rather than crashing the
connection task.
CallerCtx must be Clone + Send + 'static; see the struct-level doc.
§Runtime requirement (bd:JMAP-jfia.24)
dispatch invokes tokio::task::spawn internally and
therefore requires a Tokio runtime in scope at the call
site. The async-fn signature itself does not advertise this
requirement — there is no ?Send async-trait bound and no
Spawner parameter — but calling dispatch without a Tokio
runtime will panic at the first method call with a
there is no reactor running error.
This is a structural coupling to the Tokio ecosystem.
Production consumers running under tokio::main /
Runtime::block_on already satisfy this; consumers
experimenting with alternative runtimes (async-std,
smol, embassy, etc.) cannot use Dispatcher without
either a Tokio compat shim or a custom dispatcher
re-implementation. Decoupling the spawn mechanism is a
major-bump API change tracked separately.
§Cancellation
Per-request cancellation is not supported in the current API
(bd:JMAP-wlip.23). If this future is dropped while a handler
task is running (e.g., the HTTP connection closes), the spawned
tokio::task runs to completion — tokio does not cancel
tasks when their JoinHandle is dropped. The handler result
is discarded.
Production consequence: a JMAP server with a long-running
backend operation (e.g., Email/query over a 10M-mailbox
account, a slow full-text search, a slow downstream-service
lookup) cannot react to client disconnect. Every disconnect
leaks resource consumption equal to the full backend cost.
Adversarial clients can amplify this into a DoS by opening
many requests and dropping them.
Recommended mitigations until per-request cancellation is wired:
- Bound each backend operation’s runtime at the backend
layer, e.g. by passing a deadline / timeout from the
backend impl’s storage client (
tokio::time::timeoutaround each database call, RPC deadline, etc.). The handler does not need to know about deadlines; the backend impl does. - Server-wide shutdown via
tokio::select!with a broadcast channel frommainworks for the dispatcher loop itself but does NOT propagate into spawned handler tasks. To shut down cleanly, drain the dispatcher first and then let in-flight handler tasks finish.
The “implement cancellation at the handler level” advice
previously given here was unworkable: the spawned task has
no access to the outer dispatch-future’s context (no token,
no shared liveness flag), and the JmapHandler::call
signature carries no cancellation token.
A future revision may add an opt-in cancellation-token shape
(CallerCtx-carried token, or a dispatch-time
cancel: CancellationToken parameter that gets signalled on
future-drop). That is a workspace-architectural decision —
adding tokio_util to the dep allowlist plus threading the
token through every backend trait method — and is tracked
separately.