Skip to main content

Crate ygopro_handler

Crate ygopro_handler 

Source
Expand description

§ygopro-handler

A type-erased, plugin-based message handler framework.

This crate provides the machinery to dispatch messages to registered handlers, extract request data, and combine handler responses. Every incoming message flows through a Processor and is handled by a set of handlers keyed by message type.

The extractor/handler pattern mirrors axum: handlers are plain functions whose parameters are pulled from a Bundle via FromRequest, and whose return value is converted into a response via IntoResponse. Any type implementing FromRequest can be a parameter, and any type implementing IntoResponse can be returned — exactly like axum’s FromRequest and IntoResponse.

Unlike axum, however, a message is not handled by a single handler but by a chain of handlers (all those registered for the message key, plus the globals), and the chain can be aborted mid-way via StopFlag. This makes it easy to layer plugins: each plugin registers its own handlers for a message, and any one of them can stop the chain or replace the message that the rest see.

§Design goals

  • Handlers as closures. A handler is any callable that takes extractable parameters and returns a value convertible into an extract::Response. Up to 16 parameters are supported; each is pulled from a Bundle via FromRequest.
  • Message key dispatch. Each message carries a MessageKey (usually a u8 flag). The Processor routes a message to the handlers registered for that key, plus any global handlers.
  • Chained, abortable processing. A message flows through every handler registered for its key, plus the globals, in priority order. Any handler can abort the chain via StopFlag or replace the message that downstream handlers see. Their outputs are combined through std::ops::Mul on the response, letting one handler replace the message, another swallow it, and another terminate the room.
  • Type-erased handlers. SyncHandler, AsyncHandler and TowerHandler erase the concrete handler type so a heterogeneous set of handlers can live in a single Processor.

§Soundness

This crate is unsafe. You must carefully handle the parameters to prevent undefined behaviour, especially dual mutable references — a handler that takes two &mut parameters can alias the same memory. The extraction uses raw-pointer casts because the Rust compiler cannot properly calculate the required lifetimes, and due to the architecture the crate cannot offer the same guarantees that axum’s FromRequest provides.

§Performance

The crate offers three handler wrappers, trading features against speed:

  1. TowerHandler — the most feature-complete, and the slowest. Between the handler and the processor it inserts a tower Service layer (HandlerService), a boxed future (HandlerServiceFuture), and a BoxCloneService, so every call pays for several layers of boxing and a oneshot dispatch.
  2. AsyncHandler — gives up the tower adaptation layer. It holds the handler in an Arc<dyn Call> and boxes only the future, so it drops the extra Service boxing while staying cloneable through a cheap Arc clone.
  3. SyncHandler — gives up the async nature of Handler: its future is always Ready. In exchange it boxes nothing per call, drops the 'static bound on the request, state, and response, and enables the dual-state trick (handler::sync_handler::WithSubState).

Compared with the C++ original, which dispatches messages with a direct switch or a virtual call, every handler here pays for at least one heap-allocated future and a type-erasure indirection (a trait object or a function pointer) per invocation. Processor::process additionally awaits the whole handler chain per item, moving the Bundle through it. For the hot path (game messages) this is one boxed future per handler, which is acceptable at the rate messages are emitted.

§Example

A processor dispatches a message to the handlers registered for its key:

use ygopro_handler::Processor;
use ygopro_handler::TowerHandler;
use ygopro_handler::Bundle;
use ygopro_handler::State;
use ygopro_handler::extract::Request;
use ygopro_handler::extract::Response;

type Req = Request<u8, ()>;
type Res = Response<u8>;

let mut processor = Processor::<u8, Req, State, Res>::new();
processor.register(7, TowerHandler::new(0, "test", "example", |message: &u8| -> Res {
    if *message == 7 {
        Res::Replace(*message)
    } else {
        Res::Continue
    }
}));

let bundle = Bundle::new(Request { message: 7, extra: () }, State::new(), Res::Continue);
let result = tokio::runtime::Runtime::new().unwrap().block_on(processor.process_bundle(bundle, 7));
assert!(matches!(result.response, Res::Replace(7)));

Re-exports§

pub use handler::tower_handler::TowerHandler;
pub use handler::async_handler::AsyncHandler;
pub use handler::sync_handler::SyncHandler;
pub use handler::*;
pub use processor::*;

Modules§

extract
Practical types used in the ygopro message flow.
handler
The handler abstraction: extract parameters, produce a response, and combine them.
processor
The message dispatcher: routes messages to handlers by key.

Traits§

RoomProvider
Abstract of a duel.