Skip to main content

Dispatcher

Struct Dispatcher 

Source
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>

Source

pub fn new() -> Self

Create an empty dispatcher with no registered handlers.

Source

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).

Source

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.

Source

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::timeout around 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 from main works 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.

Trait Implementations§

Source§

impl<CallerCtx> Debug for Dispatcher<CallerCtx>

Source§

fn fmt(&self, f: &mut Formatter<'_>) -> Result

Formats the value using the given formatter. Read more
Source§

impl<CallerCtx: Clone + Send + 'static> Default for Dispatcher<CallerCtx>

Source§

fn default() -> Self

Returns the “default value” for a type. Read more

Auto Trait Implementations§

§

impl<CallerCtx> !RefUnwindSafe for Dispatcher<CallerCtx>

§

impl<CallerCtx> !UnwindSafe for Dispatcher<CallerCtx>

§

impl<CallerCtx> Freeze for Dispatcher<CallerCtx>

§

impl<CallerCtx> Send for Dispatcher<CallerCtx>

§

impl<CallerCtx> Sync for Dispatcher<CallerCtx>

§

impl<CallerCtx> Unpin for Dispatcher<CallerCtx>

§

impl<CallerCtx> UnsafeUnpin for Dispatcher<CallerCtx>

Blanket Implementations§

Source§

impl<T> Any for T
where T: 'static + ?Sized,

Source§

fn type_id(&self) -> TypeId

Gets the TypeId of self. Read more
Source§

impl<T> Borrow<T> for T
where T: ?Sized,

Source§

fn borrow(&self) -> &T

Immutably borrows from an owned value. Read more
Source§

impl<T> BorrowMut<T> for T
where T: ?Sized,

Source§

fn borrow_mut(&mut self) -> &mut T

Mutably borrows from an owned value. Read more
Source§

impl<T> From<T> for T

Source§

fn from(t: T) -> T

Returns the argument unchanged.

Source§

impl<T, U> Into<U> for T
where U: From<T>,

Source§

fn into(self) -> U

Calls U::from(self).

That is, this conversion is whatever the implementation of From<T> for U chooses to do.

Source§

impl<T, U> TryFrom<U> for T
where U: Into<T>,

Source§

type Error = Infallible

The type returned in the event of a conversion error.
Source§

fn try_from(value: U) -> Result<T, <T as TryFrom<U>>::Error>

Performs the conversion.
Source§

impl<T, U> TryInto<U> for T
where U: TryFrom<T>,

Source§

type Error = <U as TryFrom<T>>::Error

The type returned in the event of a conversion error.
Source§

fn try_into(self) -> Result<U, <U as TryFrom<T>>::Error>

Performs the conversion.