arcly_http/realtime/gateway.rs
1//! Gateway contracts: the `ArclyGateway` lifecycle trait and the link-time
2//! descriptor / runtime types the `#[Gateway]` macro emits.
3//!
4//! ## Why no `dyn ArclyGateway`
5//!
6//! `ArclyGateway` uses native `async fn` in trait — ergonomic, but not
7//! object-safe. We never need a trait object: the `#[Gateway]` macro generates
8//! *monomorphic* closures over the concrete gateway type (`&'static ChatGateway`)
9//! and type-erases them into [`GatewayRuntime`]. This mirrors the framework's
10//! route-thunk pattern and keeps the developer's trait impl clean.
11
12use std::collections::HashMap;
13
14use futures::future::BoxFuture;
15
16use crate::core::engine::FrozenDiContainer;
17use crate::realtime::connection::WsClient;
18use crate::web::Error;
19
20/// Connection lifecycle for a real-time gateway.
21///
22/// Implement on your gateway struct. Both hooks default to no-ops, so override
23/// only what you need. Handlers for individual events are declared with
24/// `#[Subscribe("event::name")]` inside the `#[Gateway(...)]` impl block.
25pub trait ArclyGateway: Send + Sync + 'static {
26 /// Fired after the socket has upgraded and been registered.
27 fn on_connect(&self, client: WsClient) -> impl std::future::Future<Output = ()> + Send {
28 let _ = client;
29 async {}
30 }
31
32 /// Fired after the socket has closed and just before it is unregistered.
33 fn on_disconnect(&self, client: WsClient) -> impl std::future::Future<Output = ()> + Send {
34 let _ = client;
35 async {}
36 }
37}
38
39/// A type-erased lifecycle hook closure (built by the macro over a concrete
40/// gateway instance).
41pub type LifecycleHook = Box<dyn Fn(WsClient) -> BoxFuture<'static, ()> + Send + Sync>;
42
43/// A type-erased event handler. Receives the originating client and the raw
44/// JSON `data` payload (as borrowed text), returns the handler's `Result`.
45pub type MessageHandler = std::sync::Arc<
46 dyn Fn(WsClient, std::sync::Arc<str>) -> BoxFuture<'static, Result<(), Error>> + Send + Sync,
47>;
48
49/// The fully-constructed, ready-to-serve form of a gateway. Produced once at
50/// launch by [`GatewayDescriptor::build`] and leaked to `&'static`.
51pub struct GatewayRuntime {
52 /// Mount path for the upgrade endpoint, e.g. `/chat-socket`.
53 pub path: &'static str,
54 pub on_connect: LifecycleHook,
55 pub on_disconnect: LifecycleHook,
56 /// Event-name → handler routing table. Lock-free reads (`&HashMap`) on the
57 /// frame-processing hot path.
58 pub dispatch: HashMap<&'static str, MessageHandler>,
59}
60
61impl GatewayRuntime {
62 /// Resolve the handler for an inbound event name, if subscribed.
63 #[inline]
64 pub fn handler(&self, event: &str) -> Option<&MessageHandler> {
65 self.dispatch.get(event)
66 }
67}
68
69/// Link-time gateway registration. One is emitted per `#[Gateway]` impl and
70/// collected via `inventory`. `build` constructs the gateway (wiring its
71/// `Inject<T>` fields from the frozen container) and assembles the runtime.
72pub struct GatewayDescriptor {
73 /// Gateway type name — matched against a module's `gateways(...)` list.
74 pub name: &'static str,
75 pub path: &'static str,
76 pub build: fn(&'static FrozenDiContainer) -> &'static GatewayRuntime,
77}
78
79inventory::collect!(&'static GatewayDescriptor);