add event_bus_typed

This commit is contained in:
2026-09-23 10:36:14 +02:00
parent 3780092fa6
commit ab97dcc8d9
35 changed files with 1376 additions and 3228 deletions
+410 -401
View File
@@ -1,420 +1,90 @@
use std::any::Any;
use std::future::Future;
use std::any::{Any, TypeId};
use std::collections::HashMap;
use std::fmt::Debug;
use std::sync::Arc;
use parking_lot::RwLock;
use std::collections::HashMap;
use std::iter;
use tokio::sync::broadcast;
use tokio::task::JoinHandle;
// use tracing::log::kv::{Key, Value};
use tracing::{debug, trace, warn};
use uuid::Uuid;
/// Raw event type: an atomic reference-counted pointer to any value.
pub type AnyEvent = Arc<dyn Any + Send + Sync>;
use crate::handler::{AsyncHandler, AsyncHandlerWith};
/// Default buffer capacity for each broadcast channel.
const DEFAULT_CAPACITY: usize = 64;
pub const DEFAULT_CAPACITY: usize = 1024;
#[derive(Debug, Clone, PartialEq, Eq)]
pub enum ScopeValue {
String(String),
Uuid(Uuid),
}
impl ScopeValue {
fn into_string(self) -> String {
match self {
Self::String(value) => value,
Self::Uuid(value) => value.to_string(),
}
}
}
/// Marker trait for events dispatched through [`EventBus`].
///
/// Any type implementing `Clone + Send + Sync + 'static` automatically
/// implements `Event`.
pub trait Event: Clone + Send + Sync + 'static {}
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct Scope {
pub key: String,
pub value: ScopeValue,
}
impl<T: Clone + Send + Sync + 'static> Event for T {}
impl Scope {
pub fn new(key: impl Into<String>, value: ScopeValue) -> Self {
Self {
key: key.into(),
value,
}
}
pub fn uuid(key: impl Into<String>, value: Uuid) -> Self {
Self::new(key, ScopeValue::Uuid(value))
}
pub fn string(key: impl Into<String>, value: impl Into<String>) -> Self {
Self::new(key, ScopeValue::String(value.into()))
}
}
impl IntoIterator for Scope {
type Item = Scope;
type IntoIter = iter::Once<Scope>;
fn into_iter(self) -> Self::IntoIter {
iter::once(self)
}
}
/// The central event bus.
///
/// Share it via `Arc<EventBus>` across modules.
/// Each topic has its own broadcast channel: only subscribers of the matching
/// topic are woken up on `emit` (targeted wake-up).
///
/// # Minimal example — sync callback
/// ```rust,no_run
/// use std::sync::Arc;
/// use oxspeak_server_lib::event_bus::EventBus;
///
/// #[derive(Clone, Debug)]
/// struct User { name: String }
///
/// # tokio_test::block_on(async {
/// let bus = Arc::new(EventBus::new());
///
/// bus.on::<User>("user-connected", |user| {
/// println!("Connected: {:?}", user);
/// });
///
/// bus.emit("user-connected", User { name: "Alice".into() });
/// # tokio::time::sleep(std::time::Duration::from_millis(10)).await;
/// # });
/// ```
///
/// # Example — async callback
/// ```rust,no_run
/// use std::sync::Arc;
/// use oxspeak_server_lib::event_bus::EventBus;
///
/// #[derive(Clone, Debug)]
/// struct User { name: String }
///
/// # tokio_test::block_on(async {
/// let bus = Arc::new(EventBus::new());
///
/// bus.on_async::<User, _, _>("user-connected", |user| async move {
/// println!("(async) Connected: {:?}", user);
/// });
///
/// bus.emit("user-connected", User { name: "Bob".into() });
/// # tokio::time::sleep(std::time::Duration::from_millis(10)).await;
/// # });
/// ```
#[derive(Debug)]
pub struct EventBus {
/// Channels indexed by exact topic.
channels: RwLock<HashMap<String, broadcast::Sender<AnyEvent>>>,
struct EventBusInner {
channels: RwLock<HashMap<TypeId, Box<dyn Any + Send + Sync>>>,
capacity: usize,
}
impl EventBus {
/// Creates a bus with the default capacity (64 messages per channel).
pub fn new() -> Self {
debug!(
"EventBus created with default capacity ({})",
DEFAULT_CAPACITY
);
Self {
channels: RwLock::new(HashMap::new()),
capacity: DEFAULT_CAPACITY,
}
}
/// Creates a bus with a custom buffer capacity.
pub fn with_capacity(capacity: usize) -> Self {
debug!("EventBus created with capacity {}", capacity);
Self {
channels: RwLock::new(HashMap::new()),
capacity,
}
}
// ─────────────────────────────────────────────────────────────────────────
// Internal
// ─────────────────────────────────────────────────────────────────────────
fn get_or_create_sender(&self, topic: &str) -> broadcast::Sender<AnyEvent> {
{
let channels = self.channels.read();
if let Some(tx) = channels.get(topic) {
return tx.clone();
}
}
let mut channels = self.channels.write();
let created = !channels.contains_key(topic);
let tx = channels
.entry(topic.to_string())
.or_insert_with(|| {
let (tx, _) = broadcast::channel(self.capacity);
tx
})
.clone();
if created {
debug!(topic, "New broadcast channel created");
}
tx
}
// ─────────────────────────────────────────────────────────────────────────
// Emission
// ─────────────────────────────────────────────────────────────────────────
/// Emits an event on a topic.
///
/// - Pushes the event into the exact-topic channel (if subscribers exist).
/// - If nobody is listening, the event is silently dropped.
///
/// # Example
/// ```rust,no_run
/// # use std::sync::Arc;
/// # use oxspeak_server_lib::event_bus::EventBus;
/// # #[derive(Clone)] struct User;
/// # let bus = Arc::new(EventBus::new());
/// bus.emit("user-connected", User);
/// bus.emit("user-deleted", uuid::Uuid::new_v4());
/// ```
pub fn emit<T: Any + Send + Sync + 'static>(&self, topic: &str, event: T) {
trace!(topic, "Emitting event");
let event: AnyEvent = Arc::new(event);
self.emit_arc(topic, event);
}
// todo : undocumented...
pub fn emit_scoped<T>(&self, topic: &str, scopes: impl IntoIterator<Item = Scope>, event: T)
where
T: Any + Send + Sync + 'static,
{
let event: AnyEvent = Arc::new(event);
// Émission sur le topic général.
self.emit_arc(topic, Arc::clone(&event));
// Émission sur chaque topic scoped.
for scope in scopes {
let scoped_topic = format!("{}:{}:{}", scope.key, scope.value.into_string(), topic);
self.emit_arc(&scoped_topic, Arc::clone(&event));
}
}
// todo : undocumented...
fn emit_arc(&self, topic: &str, event: AnyEvent) {
trace!(topic, "Emitting event");
if let Some(tx) = self.channels.read().get(topic) {
let receiver_count = tx.receiver_count();
let _ = tx.send(event);
trace!(topic, receiver_count, "Event delivered to channel");
}
}
// ─────────────────────────────────────────────────────────────────────────
// Subscription — callbacks (main API)
// ─────────────────────────────────────────────────────────────────────────
/// Subscribes to a topic and calls `handler` on each event of type `T`.
///
/// The handler runs in a dedicated Tokio task (fire-and-forget).
/// Events of a different type are silently ignored.
/// Returns a [`JoinHandle`] to cancel the subscription if needed.
///
/// # Example
/// ```rust,no_run
/// # use std::sync::Arc;
/// # use oxspeak_server_lib::event_bus::EventBus;
/// # #[derive(Clone, Debug)] struct User { name: String }
/// # let bus = Arc::new(EventBus::new());
/// bus.on::<User>("user-connected", |user| {
/// println!("Connected: {:?}", user);
/// });
/// ```
pub fn on<T, F>(&self, topic: &str, handler: F) -> JoinHandle<()>
where
T: Any + Send + Sync + Clone + 'static,
F: Fn(T) + Send + Sync + 'static,
{
let mut rx = self.get_or_create_sender(topic).subscribe();
let topic_owned = topic.to_string();
debug!(topic, "Sync subscriber registered");
tokio::spawn(async move {
loop {
match rx.recv().await {
Ok(evt) => {
if let Some(typed) = evt.downcast_ref::<T>() {
trace!(topic = topic_owned, "Sync handler invoked");
handler(typed.clone());
}
}
Err(broadcast::error::RecvError::Lagged(n)) => {
warn!(
topic = topic_owned,
skipped = n,
"Subscriber lagged, messages dropped"
);
}
Err(broadcast::error::RecvError::Closed) => {
debug!(
topic = topic_owned,
"Channel closed, sync subscriber exiting"
);
break;
}
}
}
})
}
/// Subscribes to a topic and calls an **async** handler on each event of type `T`.
///
/// Ideal for performing async operations in the handler
/// (DB query, HTTP call, WebSocket broadcast, …).
/// Returns a [`JoinHandle`] to cancel the subscription if needed.
///
/// # Example
/// ```rust,no_run
/// # use std::sync::Arc;
/// # use oxspeak_server_lib::event_bus::EventBus;
/// # #[derive(Clone, Debug)] struct User { name: String }
/// # let bus = Arc::new(EventBus::new());
/// bus.on_async::<User, _, _>("user-connected", |user| async move {
/// println!("(async) Connected: {:?}", user);
/// // async work here: DB query, HTTP, etc.
/// });
/// ```
pub fn on_async<T, F, Fut>(&self, topic: &str, handler: F) -> JoinHandle<()>
where
T: Any + Send + Sync + Clone + 'static,
F: Fn(T) -> Fut + Send + Sync + 'static,
Fut: Future<Output = ()> + Send + 'static,
{
let mut rx = self.get_or_create_sender(topic).subscribe();
let topic_owned = topic.to_string();
debug!(topic, "Async subscriber registered");
tokio::spawn(async move {
loop {
match rx.recv().await {
Ok(evt) => {
if let Some(typed) = evt.downcast_ref::<T>() {
trace!(topic = topic_owned, "Async handler invoked");
handler(typed.clone()).await;
}
}
Err(broadcast::error::RecvError::Lagged(n)) => {
warn!(
topic = topic_owned,
skipped = n,
"Subscriber lagged, messages dropped"
);
}
Err(broadcast::error::RecvError::Closed) => {
debug!(
topic = topic_owned,
"Channel closed, async subscriber exiting"
);
break;
}
}
}
})
}
// todo : Undocumented
pub fn on_async_with<T, C, F, Fut>(&self, topic: &str, context: C, handler: F) -> JoinHandle<()>
where
T: Any + Send + Sync + Clone + 'static,
C: Clone + Send + Sync + 'static,
F: Fn(C, T) -> Fut + Send + Sync + 'static,
Fut: Future<Output = ()> + Send + 'static,
{
let mut rx = self.get_or_create_sender(topic).subscribe();
let topic_owned = topic.to_string();
debug!(topic, "Async subscriber registered");
tokio::spawn(async move {
loop {
match rx.recv().await {
Ok(evt) => {
if let Some(typed) = evt.downcast_ref::<T>() {
trace!(topic = topic_owned, "Async handler invoked");
handler(context.clone(), typed.clone()).await;
}
}
Err(broadcast::error::RecvError::Lagged(n)) => {
warn!(
topic = topic_owned,
skipped = n,
"Subscriber lagged, messages dropped"
);
}
Err(broadcast::error::RecvError::Closed) => {
debug!(
topic = topic_owned,
"Channel closed, async subscriber exiting"
);
break;
}
}
}
})
}
// ─────────────────────────────────────────────────────────────────────────
// Subscription — low-level access (advanced use cases)
// ─────────────────────────────────────────────────────────────────────────
/// Returns a raw [`AnyEvent`] receiver to manage the loop yourself.
///
/// Useful with the [`match_event!`][crate::match_event] macro to handle
/// multiple different types on the same topic.
///
/// # Example
/// ```rust,no_run
/// # use std::sync::Arc;
/// # use oxspeak_server_lib::event_bus::EventBus;
/// # use oxspeak_server_lib::match_event;
/// # #[derive(Clone, Debug)] struct User { name: String }
/// # #[derive(Clone, Debug)] struct UdpMetric { value: f32 }
/// # let bus = Arc::new(EventBus::new());
/// # tokio_test::block_on(async {
/// let mut rx = bus.on_raw("user-connected");
/// bus.emit("user-connected", User { name: "Alice".into() });
///
/// if let Ok(evt) = rx.recv().await {
/// match_event!(evt,
/// User => |u| println!("User: {:?}", u),
/// UdpMetric => |m| println!("Metric: {:?}", m),
/// );
/// }
/// # });
/// ```
pub fn on_raw(&self, topic: &str) -> broadcast::Receiver<AnyEvent> {
debug!(topic, "Raw subscriber registered");
self.get_or_create_sender(topic).subscribe()
}
// ─────────────────────────────────────────────────────────────────────────
// Utilities
// ─────────────────────────────────────────────────────────────────────────
/// Returns the list of currently registered topics.
pub fn topics(&self) -> Vec<String> {
self.channels.read().keys().cloned().collect()
}
/// A central, strongly-typed broadcast event bus.
///
/// Unlike string/topic-based event buses, [`EventBus`] dispatches events directly
/// based on the concrete Rust type of the event (using [`TypeId`]).
///
/// Under the hood, each event type is backed by an independent [`tokio::sync::broadcast`]
/// ring buffer. Dispatching an event via [`emit`](Self::emit) is $O(1)$ and non-blocking.
///
/// # Sync callback example
/// ```rust,no_run
/// use event_bus::EventBus;
///
/// #[derive(Clone, Debug)]
/// struct MessageCreatedEvent {
/// content: String,
/// }
///
/// #[tokio::main]
/// async fn main() {
/// let bus = EventBus::new();
///
/// bus.on::<MessageCreatedEvent>(|event| {
/// println!("Created message: {:?}", event);
/// });
///
/// bus.emit(MessageCreatedEvent {
/// content: "Hello!".into(),
/// });
/// }
/// ```
///
/// # Async callback example
/// ```rust,no_run
/// use event_bus::EventBus;
///
/// #[derive(Clone, Debug)]
/// struct MessageUpdatedEvent {
/// id: u64,
/// content: String,
/// }
///
/// #[tokio::main]
/// async fn main() {
/// let bus = EventBus::new();
///
/// bus.on_async::<MessageUpdatedEvent>(|event| async move {
/// println!("(async) Updated message: {:?}", event);
/// });
///
/// bus.emit(MessageUpdatedEvent {
/// id: 42,
/// content: "World!".into(),
/// });
/// }
/// ```
#[derive(Debug, Clone)]
pub struct EventBus {
inner: Arc<EventBusInner>,
}
impl Default for EventBus {
@@ -422,3 +92,342 @@ impl Default for EventBus {
Self::new()
}
}
impl EventBus {
/// Creates a new `EventBus` with default capacity (1024 messages per channel).
pub fn new() -> Self {
Self::with_capacity(DEFAULT_CAPACITY)
}
/// Creates a new `EventBus` with custom buffer capacity per channel.
pub fn with_capacity(capacity: usize) -> Self {
debug!(capacity, "EventBus created");
Self {
inner: Arc::new(EventBusInner {
channels: RwLock::new(HashMap::new()),
capacity,
}),
}
}
// ─────────────────────────────────────────────────────────────────────────
// Internal Channel Management
// ─────────────────────────────────────────────────────────────────────────
fn get_or_create_sender<E: Event>(&self) -> broadcast::Sender<E> {
let type_id = TypeId::of::<E>();
if let Some(tx) = self
.inner
.channels
.read()
.get(&type_id)
.and_then(|entry| entry.downcast_ref::<broadcast::Sender<E>>())
{
return tx.clone();
}
let mut channels = self.inner.channels.write();
if let Some(tx) = channels
.get(&type_id)
.and_then(|entry| entry.downcast_ref::<broadcast::Sender<E>>())
{
return tx.clone();
}
let (tx, _) = broadcast::channel::<E>(self.inner.capacity);
debug!(
event_type = std::any::type_name::<E>(),
capacity = self.inner.capacity,
"New broadcast channel created"
);
channels.insert(type_id, Box::new(tx.clone()));
tx
}
// ─────────────────────────────────────────────────────────────────────────
// Emission
// ─────────────────────────────────────────────────────────────────────────
/// Emits a strongly-typed event to all subscribers of `E` without blocking ($O(1)$).
///
/// If no subscribers exist for this event type, the event is dropped immediately
/// without cloning or allocating.
///
/// # Example
/// ```rust,no_run
/// # use event_bus::EventBus;
/// # #[derive(Clone)] struct UserConnected { id: u64 }
/// # let bus = EventBus::new();
/// bus.emit(UserConnected { id: 1 });
/// ```
pub fn emit<E: Event>(&self, event: E) {
trace!(
event_type = std::any::type_name::<E>(),
"Emitting event"
);
let type_id = TypeId::of::<E>();
if let Some(tx) = self
.inner
.channels
.read()
.get(&type_id)
.and_then(|entry| entry.downcast_ref::<broadcast::Sender<E>>())
.filter(|tx| tx.receiver_count() > 0)
{
let _ = tx.send(event);
}
}
// ─────────────────────────────────────────────────────────────────────────
// Subscriptions — Callbacks
// ─────────────────────────────────────────────────────────────────────────
/// Registers a synchronous callback for events of type `E`.
///
/// The handler runs in a dedicated background Tokio task.
/// Returns a [`JoinHandle`] allowing to cancel the subscription via [`.abort()`](JoinHandle::abort).
pub fn on<E: Event>(&self, handler: impl Fn(E) + Send + Sync + 'static) -> JoinHandle<()> {
let mut rx = self.subscribe::<E>();
let type_name = std::any::type_name::<E>();
debug!(event_type = type_name, "Sync subscriber registered");
tokio::spawn(async move {
loop {
match rx.recv().await {
Ok(event) => {
trace!(event_type = type_name, "Sync handler invoked");
handler(event);
}
Err(broadcast::error::RecvError::Lagged(skipped)) => {
warn!(
event_type = type_name,
skipped,
"Subscriber lagged behind and skipped messages"
);
}
Err(broadcast::error::RecvError::Closed) => {
debug!(event_type = type_name, "Channel closed, subscriber exiting");
break;
}
}
}
})
}
/// Registers an asynchronous callback for events of type `E`.
///
/// The handler runs in a dedicated background Tokio task.
/// Returns a [`JoinHandle`] allowing to cancel the subscription via [`.abort()`](JoinHandle::abort).
///
/// # Example
/// ```rust,no_run
/// # use event_bus::EventBus;
/// # #[derive(Clone)] struct MessageUpdatedEvent { id: u64 }
/// # let bus = EventBus::new();
/// bus.on_async::<MessageUpdatedEvent>(|event| async move {
/// println!("Updated: {:?}", event.id);
/// });
/// ```
pub fn on_async<E: Event>(&self, handler: impl AsyncHandler<E>) -> JoinHandle<()> {
let mut rx = self.subscribe::<E>();
let type_name = std::any::type_name::<E>();
debug!(event_type = type_name, "Async subscriber registered");
tokio::spawn(async move {
loop {
match rx.recv().await {
Ok(event) => {
trace!(event_type = type_name, "Async handler invoked");
handler(event).await;
}
Err(broadcast::error::RecvError::Lagged(skipped)) => {
warn!(
event_type = type_name,
skipped,
"Subscriber lagged behind and skipped messages"
);
}
Err(broadcast::error::RecvError::Closed) => {
debug!(event_type = type_name, "Channel closed, subscriber exiting");
break;
}
}
}
})
}
/// Registers an asynchronous callback for events of type `E` with an injected context `C`.
pub fn on_async_with<E: Event, C: Clone + Send + Sync + 'static>(
&self,
context: C,
handler: impl AsyncHandlerWith<E, C>,
) -> JoinHandle<()> {
let mut rx = self.subscribe::<E>();
let type_name = std::any::type_name::<E>();
debug!(event_type = type_name, "Async subscriber with context registered");
tokio::spawn(async move {
loop {
match rx.recv().await {
Ok(event) => {
trace!(event_type = type_name, "Async handler with context invoked");
handler(context.clone(), event).await;
}
Err(broadcast::error::RecvError::Lagged(skipped)) => {
warn!(
event_type = type_name,
skipped,
"Subscriber lagged behind and skipped messages"
);
}
Err(broadcast::error::RecvError::Closed) => {
debug!(event_type = type_name, "Channel closed, subscriber exiting");
break;
}
}
}
})
}
// ─────────────────────────────────────────────────────────────────────────
// Low-Level Subscription (Direct Stream / Receiver)
// ─────────────────────────────────────────────────────────────────────────
/// Returns a direct [`broadcast::Receiver<E>`] for events of type `E`.
///
/// Allows writing custom event processing loops without callback wrappers.
///
/// # Example
/// ```rust,no_run
/// use event_bus::EventBus;
///
/// #[derive(Clone)]
/// struct MyEvent;
///
/// #[tokio::main]
/// async fn main() {
/// let bus = EventBus::new();
/// let mut rx = bus.subscribe::<MyEvent>();
/// bus.emit(MyEvent);
/// if let Ok(event) = rx.recv().await {
/// // direct typed `event`
/// }
/// }
/// ```
pub fn subscribe<E: Event>(&self) -> broadcast::Receiver<E> {
self.get_or_create_sender::<E>().subscribe()
}
/// Waits for the next event of type `E` to be emitted.
///
/// Creates a temporary one-shot subscription and resolves as soon as an event of type `E`
/// is emitted. The subscription is automatically dropped after receiving the event.
///
/// # Example
/// ```rust,no_run
/// # use event_bus::EventBus;
/// # #[derive(Clone)] struct MyEvent { id: u64 }
/// #[tokio::main]
/// async fn main() {
/// let bus = EventBus::new();
/// let event = bus.wait_next::<MyEvent>().await.unwrap();
/// println!("Received next event: {}", event.id);
/// }
/// ```
pub async fn wait_next<E: Event>(&self) -> Result<E, broadcast::error::RecvError> {
let mut rx = self.subscribe::<E>();
rx.recv().await
}
/// Waits for an event of type `E` satisfying the given predicate to be emitted.
///
/// Creates a temporary subscription, receives events of type `E`, and resolves
/// when the predicate returns `true`. The subscription is automatically dropped afterwards.
///
/// # Example
/// ```rust,no_run
/// # use event_bus::EventBus;
/// # #[derive(Clone)] struct MessageSaved { id: u64 }
/// #[tokio::main]
/// async fn main() {
/// let bus = EventBus::new();
/// let target_id = 42;
/// let event = bus.wait_for::<MessageSaved>(|e| e.id == target_id).await.unwrap();
/// println!("Saved message confirmed: {}", event.id);
/// }
/// ```
pub async fn wait_for<E: Event>(
&self,
mut predicate: impl FnMut(&E) -> bool,
) -> Result<E, broadcast::error::RecvError> {
let mut rx = self.subscribe::<E>();
loop {
match rx.recv().await {
Ok(event) => {
if predicate(&event) {
return Ok(event);
}
}
Err(broadcast::error::RecvError::Lagged(skipped)) => {
warn!(
event_type = std::any::type_name::<E>(),
skipped,
"wait_for subscriber lagged behind and skipped messages"
);
}
Err(broadcast::error::RecvError::Closed) => {
return Err(broadcast::error::RecvError::Closed);
}
}
}
}
// ─────────────────────────────────────────────────────────────────────────
// Metrics & Utilities
// ─────────────────────────────────────────────────────────────────────────
/// Returns the total number of active subscribers for event type `E`.
pub fn subscriber_count<E: Event>(&self) -> usize {
let type_id = TypeId::of::<E>();
self.inner
.channels
.read()
.get(&type_id)
.and_then(|e| e.downcast_ref::<broadcast::Sender<E>>())
.map(|tx| tx.receiver_count())
.unwrap_or(0)
}
/// Returns `true` if there are any active subscribers for event type `E`.
pub fn has_subscribers<E: Event>(&self) -> bool {
self.subscriber_count::<E>() > 0
}
/// Returns the total number of broadcast channels currently instantiated in the bus.
pub fn channel_count(&self) -> usize {
self.inner.channels.read().len()
}
/// Returns `true` if no channels are currently registered in the bus.
pub fn is_empty(&self) -> bool {
self.inner.channels.read().is_empty()
}
/// Removes and drops all channels in the event bus.
pub fn clear(&self) {
self.inner.channels.write().clear();
}
/// Removes and drops the channel associated with event type `E`.
pub fn clear_type<E: Event>(&self) {
let type_id = TypeId::of::<E>();
self.inner.channels.write().remove(&type_id);
}
}
+47
View File
@@ -0,0 +1,47 @@
use std::future::Future;
/// Handler trait for asynchronous event callbacks.
///
/// This trait is automatically implemented for any closure matching
/// `Fn(E) -> Future<Output = ()> + Send + Sync + 'static`.
///
/// By using `Fn(E) -> <Self as AsyncHandler<E>>::Fut` as a supertrait,
/// Rust propagates the event type `E` directly into the closure's parameter,
/// enabling seamless turbofish syntax with clean field access:
///
/// ```rust,no_run
/// # use event_bus::EventBus;
/// # #[derive(Clone)] struct MessageUpdatedEvent { id: u64 }
/// # let bus = EventBus::new();
/// bus.on_async::<MessageUpdatedEvent>(|event| async move {
/// println!("Updated: {:?}", event.id);
/// });
/// ```
pub trait AsyncHandler<E>: Fn(E) -> <Self as AsyncHandler<E>>::Fut + Send + Sync + 'static {
type Fut: Future<Output = ()> + Send + 'static;
}
impl<E, F, Fut> AsyncHandler<E> for F
where
F: Fn(E) -> Fut + Send + Sync + 'static,
Fut: Future<Output = ()> + Send + 'static,
{
type Fut = Fut;
}
/// Handler trait for asynchronous event callbacks with an injected context.
///
/// Automatically implemented for closures matching
/// `Fn(C, E) -> Future<Output = ()> + Send + Sync + 'static`.
pub trait AsyncHandlerWith<E, C>: Fn(C, E) -> <Self as AsyncHandlerWith<E, C>>::Fut + Send + Sync + 'static {
type Fut: Future<Output = ()> + Send + 'static;
}
impl<E, C, F, Fut> AsyncHandlerWith<E, C> for F
where
C: Clone + Send + Sync + 'static,
F: Fn(C, E) -> Fut + Send + Sync + 'static,
Fut: Future<Output = ()> + Send + 'static,
{
type Fut = Fut;
}
+44 -44
View File
@@ -1,49 +1,49 @@
/// Downcasts an [`AnyEvent`] to one or more concrete types and executes
/// the matching closure if the type matches.
///
/// Non-matching branches are silently ignored.
///
/// # Syntax
/// ```text
/// match_event!(evt, Type1 => |val| { ... }, Type2 => |val| { ... })
/// ```
///
/// # Example
/// ```rust,no_run
/// # use std::sync::Arc;
/// # use oxspeak_server_lib::event_bus::EventBus;
/// # use oxspeak_server_lib::match_event;
/// # #[derive(Clone, Debug)] struct User { name: String }
/// # #[derive(Clone, Debug)] struct UdpMetric { value: f32 }
/// # let bus = Arc::new(EventBus::new());
/// # tokio_test::block_on(async {
/// let mut rx = bus.on_raw("user-connected");
/// bus.emit("user-connected", User { name: "Alice".into() });
///
/// if let Ok(evt) = rx.recv().await {
/// match_event!(evt,
/// User => |u| println!("User: {:?}", u),
/// UdpMetric => |m| println!("Metric: {:?}", m),
/// );
/// }
/// # });
/// ```
#[macro_export]
macro_rules! match_event {
($evt:expr, $($type:ty => $handler:expr),+ $(,)?) => {
$(
if let Some(val) = ($evt).downcast_ref::<$type>() {
($handler)(val.clone());
} else
)+
{
// No matching type → silently ignored
}
};
}
//! # event_bus
//!
//! A strongly-typed, high-performance in-memory event bus for Tokio.
//!
//! ## Overview
//!
//! Unlike string/topic-based event buses, `event_bus` routes events using
//! their concrete Rust types ([`std::any::TypeId`]).
//!
//! - **Strong typing**: No string keys required for event types, no manual `match_event!`
//! macros, and no runtime downcasting (`downcast_ref`) inside the subscriber loops.
//! - **Ergonomic async subscribers**: Handlers can be registered with clean turbofish syntax:
//! `bus.on_async::<MessageUpdatedEvent>(|event| async move { ... })`.
//! - **Targeted wake-up**: Tokio broadcast channels are isolated per event type.
//!
//! ## Example
//!
//! ```rust,no_run
//! use event_bus::EventBus;
//!
//! #[derive(Clone, Debug, PartialEq)]
//! struct MessageCreatedEvent {
//! content: String,
//! }
//!
//! #[tokio::main]
//! async fn main() {
//! let bus = EventBus::new();
//!
//! // Async subscriber
//! bus.on_async::<MessageCreatedEvent>(|event| async move {
//! println!("Received message: {}", event.content);
//! });
//!
//! // Emit event
//! bus.emit(MessageCreatedEvent {
//! content: "Hello from typed event bus!".into(),
//! });
//! }
//! ```
mod bus;
pub use bus::{AnyEvent, EventBus, Scope, ScopeValue};
mod handler;
pub use bus::{DEFAULT_CAPACITY, Event, EventBus};
pub use handler::{AsyncHandler, AsyncHandlerWith};
#[cfg(test)]
mod tests;
+314 -142
View File
@@ -1,192 +1,364 @@
use crate::{EventBus, match_event};
use std::sync::Arc;
use std::sync::atomic::{AtomicBool, AtomicU32, Ordering};
use uuid::Uuid;
use crate::EventBus;
#[derive(Clone, Debug, PartialEq)]
struct User {
name: String,
struct MessageCreatedEvent {
channel_id: Uuid,
content: String,
}
#[derive(Clone, Debug, PartialEq)]
struct UdpMetric {
struct MessageUpdatedEvent {
id: u64,
content: String,
}
#[derive(Clone, Debug, PartialEq)]
struct MessageDeletedEvent {
id: u64,
}
#[derive(Clone, Debug, PartialEq)]
struct UdpMetricEvent {
value: f32,
}
// ── on (callback sync) ────────────────────────────────────────────────────
// ── Sync Callbacks ──────────────────────────────────────────────────────────
#[tokio::test]
async fn test_on_callback_sync() {
let bus = Arc::new(EventBus::new());
let bus = EventBus::new();
let received = Arc::new(AtomicBool::new(false));
let flag = Arc::clone(&received);
bus.on::<User, _>("user-connected", move |user| {
if user.name == "Alice" {
bus.on::<MessageCreatedEvent>(move |event| {
if event.content == "Hello" {
flag.store(true, Ordering::SeqCst);
}
});
bus.emit(
"user-connected",
User {
name: "Alice".into(),
},
);
tokio::time::sleep(std::time::Duration::from_millis(20)).await;
bus.emit(MessageCreatedEvent {
channel_id: Uuid::new_v4(),
content: "Hello".into(),
});
tokio::time::sleep(std::time::Duration::from_millis(20)).await;
assert!(received.load(Ordering::SeqCst));
}
#[tokio::test]
async fn test_on_targeted_wakeup() {
// Émettre sur "user-connected" ne doit pas réveiller "udp-metrics-updated"
let bus = Arc::new(EventBus::new());
let metric_called = Arc::new(AtomicBool::new(false));
let flag = Arc::clone(&metric_called);
bus.on::<UdpMetric, _>("udp-metrics-updated", move |_| {
flag.store(true, Ordering::SeqCst);
});
bus.emit(
"user-connected",
User {
name: "Carol".into(),
},
);
tokio::time::sleep(std::time::Duration::from_millis(20)).await;
assert!(!metric_called.load(Ordering::SeqCst));
}
// ── Async Callbacks (Exact User Requirement) ────────────────────────────────
#[tokio::test]
async fn test_on_type_mismatch_ignored() {
// Émettre un UdpMetric sur un topic écouté en User → handler pas appelé
let bus = Arc::new(EventBus::new());
let called = Arc::new(AtomicBool::new(false));
let flag = Arc::clone(&called);
async fn test_on_async_callback_turbofish() {
let bus = EventBus::new();
let received_content = Arc::new(tokio::sync::Mutex::new(String::new()));
let rc = Arc::clone(&received_content);
bus.on::<User, _>("mixed-topic", move |_| {
flag.store(true, Ordering::SeqCst);
});
bus.emit("mixed-topic", UdpMetric { value: 1.0 });
tokio::time::sleep(std::time::Duration::from_millis(20)).await;
assert!(!called.load(Ordering::SeqCst));
}
#[tokio::test]
async fn test_on_multiple_subscribers_same_topic() {
let bus = Arc::new(EventBus::new());
let count = Arc::new(AtomicU32::new(0));
for _ in 0..3 {
let c = Arc::clone(&count);
bus.on::<User, _>("user-connected", move |_| {
c.fetch_add(1, Ordering::SeqCst);
});
}
bus.emit(
"user-connected",
User {
name: "Grace".into(),
},
);
tokio::time::sleep(std::time::Duration::from_millis(20)).await;
assert_eq!(count.load(Ordering::SeqCst), 3);
}
// ── on_async ──────────────────────────────────────────────────────────────
#[tokio::test]
async fn test_on_async_callback() {
let bus = Arc::new(EventBus::new());
let received = Arc::new(AtomicBool::new(false));
let flag = Arc::clone(&received);
bus.on_async::<User, _, _>("user-connected", move |user| {
let f = Arc::clone(&flag);
// Exact syntax specified by the user:
// event_bus.on_async::<MessageUpdatedEvent>(|event| async move { ... });
bus.on_async::<MessageUpdatedEvent>(move |event| {
let rc = Arc::clone(&rc);
async move {
if user.name == "Async" {
let mut lock = rc.lock().await;
*lock = event.content;
}
});
// Exact syntax specified by the user:
// event_bus.emit(MessageUpdatedEvent { ... });
bus.emit(MessageUpdatedEvent {
id: 42,
content: "Updated message content".into(),
});
tokio::time::sleep(std::time::Duration::from_millis(20)).await;
let result = received_content.lock().await.clone();
assert_eq!(result, "Updated message content");
}
#[tokio::test]
async fn test_on_async_callback_type_inferred() {
let bus = EventBus::new();
let flag = Arc::new(AtomicBool::new(false));
let f = Arc::clone(&flag);
// Also supports inferring the event type from closure parameter
bus.on_async(move |event: MessageUpdatedEvent| {
let f = Arc::clone(&f);
async move {
if event.id == 99 {
f.store(true, Ordering::SeqCst);
}
}
});
bus.emit(
"user-connected",
User {
name: "Async".into(),
},
);
tokio::time::sleep(std::time::Duration::from_millis(20)).await;
bus.emit(MessageUpdatedEvent {
id: 99,
content: "Inferred".into(),
});
assert!(received.load(Ordering::SeqCst));
tokio::time::sleep(std::time::Duration::from_millis(20)).await;
assert!(flag.load(Ordering::SeqCst));
}
// ── on_raw + match_event! ─────────────────────────────────────────────────
// ── Targeted Wake-Up & Type Isolation ───────────────────────────────────────
#[tokio::test]
async fn test_on_raw_and_match_event_macro() {
let bus = Arc::new(EventBus::new());
let mut rx = bus.on_raw("user-connected");
bus.emit(
"user-connected",
User {
name: "Frank".into(),
},
);
let evt = rx.recv().await.unwrap();
let mut received_name = String::new();
match_event!(evt,
User => |u: User| { received_name = u.name.clone(); },
UdpMetric => |_m: UdpMetric| { panic!("mauvais type"); }
);
assert_eq!(received_name, "Frank");
}
// ── Utilitaires ────────────────────────────────────────────────────────────
#[test]
fn test_topics_list() {
async fn test_on_targeted_wakeup() {
let bus = EventBus::new();
// on_raw enregistre le canal (get_or_create)
let _rx1 = bus.on_raw("user-connected");
let _rx2 = bus.on_raw("udp-metrics-updated");
let metric_called = Arc::new(AtomicBool::new(false));
let flag = Arc::clone(&metric_called);
let mut topics = bus.topics();
topics.sort();
assert_eq!(topics, vec!["udp-metrics-updated", "user-connected"]);
bus.on::<UdpMetricEvent>(move |_| {
flag.store(true, Ordering::SeqCst);
});
// Emitting MessageCreatedEvent must never wake up UdpMetricEvent subscribers
bus.emit(MessageCreatedEvent {
channel_id: Uuid::new_v4(),
content: "Ignore me".into(),
});
tokio::time::sleep(std::time::Duration::from_millis(20)).await;
assert!(!metric_called.load(Ordering::SeqCst));
}
#[tokio::test]
async fn test_emit_multiple_types_same_bus() {
let bus = Arc::new(EventBus::new());
let user_ok = Arc::new(AtomicBool::new(false));
let metric_ok = Arc::new(AtomicBool::new(false));
let u = Arc::clone(&user_ok);
let m = Arc::clone(&metric_ok);
async fn test_multiple_subscribers_same_type() {
let bus = EventBus::new();
let count = Arc::new(AtomicU32::new(0));
bus.on::<User, _>("user-connected", move |user| {
if user.name == "Bob" {
u.store(true, Ordering::SeqCst);
}
});
bus.on::<UdpMetric, _>("udp-metrics-updated", move |metric| {
if (metric.value - 3.14).abs() < 0.001 {
m.store(true, Ordering::SeqCst);
}
for _ in 0..3 {
let c = Arc::clone(&count);
bus.on::<MessageCreatedEvent>(move |_| {
c.fetch_add(1, Ordering::SeqCst);
});
}
bus.emit(MessageCreatedEvent {
channel_id: Uuid::new_v4(),
content: "Broadcast".into(),
});
bus.emit("user-connected", User { name: "Bob".into() });
bus.emit("udp-metrics-updated", UdpMetric { value: 3.14 });
tokio::time::sleep(std::time::Duration::from_millis(20)).await;
assert_eq!(count.load(Ordering::SeqCst), 3);
}
assert!(user_ok.load(Ordering::SeqCst));
#[tokio::test]
async fn test_multiple_different_types_same_bus() {
let bus = EventBus::new();
let msg_ok = Arc::new(AtomicBool::new(false));
let metric_ok = Arc::new(AtomicBool::new(false));
let m_flag = Arc::clone(&msg_ok);
let u_flag = Arc::clone(&metric_ok);
bus.on::<MessageCreatedEvent>(move |event| {
if event.content == "Test" {
m_flag.store(true, Ordering::SeqCst);
}
});
bus.on::<UdpMetricEvent>(move |metric| {
if (metric.value - 42.5).abs() < 0.001 {
u_flag.store(true, Ordering::SeqCst);
}
});
bus.emit(MessageCreatedEvent {
channel_id: Uuid::new_v4(),
content: "Test".into(),
});
bus.emit(UdpMetricEvent { value: 42.5 });
tokio::time::sleep(std::time::Duration::from_millis(20)).await;
assert!(msg_ok.load(Ordering::SeqCst));
assert!(metric_ok.load(Ordering::SeqCst));
}
// ── Direct Stream / Receiver (No match_event! needed) ───────────────────────
#[tokio::test]
async fn test_subscribe_direct_typed_receiver() {
let bus = EventBus::new();
let mut rx = bus.subscribe::<MessageUpdatedEvent>();
bus.emit(MessageUpdatedEvent {
id: 123,
content: "Direct typed".into(),
});
let event = rx.recv().await.expect("failed to receive event");
// event is directly of type MessageUpdatedEvent, no downcast needed!
assert_eq!(event.id, 123);
assert_eq!(event.content, "Direct typed");
}
// ── In-Handler Filtering (Direct Field Access) ──────────────────────────────
#[tokio::test]
async fn test_filter_by_field_in_subscriber() {
let bus = EventBus::new();
let channel_a = Uuid::new_v4();
let channel_b = Uuid::new_v4();
let count_a = Arc::new(AtomicU32::new(0));
let count_b = Arc::new(AtomicU32::new(0));
let count_global = Arc::new(AtomicU32::new(0));
let ca = Arc::clone(&count_a);
bus.on_async::<MessageCreatedEvent>(move |event| {
let ca = Arc::clone(&ca);
async move {
if event.channel_id == channel_a {
ca.fetch_add(1, Ordering::SeqCst);
}
}
});
let cb = Arc::clone(&count_b);
bus.on_async::<MessageCreatedEvent>(move |event| {
let cb = Arc::clone(&cb);
async move {
if event.channel_id == channel_b {
cb.fetch_add(1, Ordering::SeqCst);
}
}
});
let cg = Arc::clone(&count_global);
bus.on::<MessageCreatedEvent>(move |_| {
cg.fetch_add(1, Ordering::SeqCst);
});
// Emit event with channel_a
bus.emit(MessageCreatedEvent {
channel_id: channel_a,
content: "For A".into(),
});
tokio::time::sleep(std::time::Duration::from_millis(20)).await;
// Both channel_a handler and global handler processed it, but not channel_b
assert_eq!(count_a.load(Ordering::SeqCst), 1);
assert_eq!(count_b.load(Ordering::SeqCst), 0);
assert_eq!(count_global.load(Ordering::SeqCst), 1);
}
// ── Async with Context ──────────────────────────────────────────────────────
#[tokio::test]
async fn test_on_async_with_context() {
let bus = EventBus::new();
let prefix = Arc::new("Prefix: ".to_string());
let result = Arc::new(tokio::sync::Mutex::new(String::new()));
let r = Arc::clone(&result);
bus.on_async_with::<MessageCreatedEvent, _>(prefix, move |ctx, event| {
let r = Arc::clone(&r);
async move {
let mut lock = r.lock().await;
*lock = format!("{}{}", ctx, event.content);
}
});
bus.emit(MessageCreatedEvent {
channel_id: Uuid::new_v4(),
content: "Hello Context".into(),
});
tokio::time::sleep(std::time::Duration::from_millis(20)).await;
let final_str = result.lock().await.clone();
assert_eq!(final_str, "Prefix: Hello Context");
}
// ── Metrics, Utilities & Cleanup ────────────────────────────────────────────
#[test]
fn test_subscriber_count_and_clear() {
let bus = EventBus::new();
assert_eq!(bus.subscriber_count::<MessageCreatedEvent>(), 0);
assert!(!bus.has_subscribers::<MessageCreatedEvent>());
let _sub = bus.subscribe::<MessageCreatedEvent>();
assert_eq!(bus.subscriber_count::<MessageCreatedEvent>(), 1);
assert!(bus.has_subscribers::<MessageCreatedEvent>());
assert_eq!(bus.channel_count(), 1);
bus.clear_type::<MessageCreatedEvent>();
assert_eq!(bus.subscriber_count::<MessageCreatedEvent>(), 0);
assert_eq!(bus.channel_count(), 0);
}
#[tokio::test]
async fn test_subscription_abort() {
let bus = EventBus::new();
let count = Arc::new(AtomicU32::new(0));
let c = Arc::clone(&count);
let handle = bus.on::<MessageDeletedEvent>(move |_| {
c.fetch_add(1, Ordering::SeqCst);
});
bus.emit(MessageDeletedEvent { id: 1 });
tokio::time::sleep(std::time::Duration::from_millis(20)).await;
assert_eq!(count.load(Ordering::SeqCst), 1);
// Cancel the subscription
handle.abort();
tokio::time::sleep(std::time::Duration::from_millis(10)).await;
bus.emit(MessageDeletedEvent { id: 2 });
tokio::time::sleep(std::time::Duration::from_millis(20)).await;
// Count should not increase after abort
assert_eq!(count.load(Ordering::SeqCst), 1);
}
// ── One-Shot Listeners (wait_next & wait_for) ────────────────────────────────
#[tokio::test]
async fn test_wait_next() {
let bus = EventBus::new();
let b = bus.clone();
tokio::spawn(async move {
tokio::time::sleep(std::time::Duration::from_millis(10)).await;
b.emit(MessageUpdatedEvent {
id: 777,
content: "Next event".into(),
});
});
let event = bus.wait_next::<MessageUpdatedEvent>().await.unwrap();
assert_eq!(event.id, 777);
assert_eq!(event.content, "Next event");
}
#[tokio::test]
async fn test_wait_for() {
let bus = EventBus::new();
let b = bus.clone();
tokio::spawn(async move {
tokio::time::sleep(std::time::Duration::from_millis(10)).await;
b.emit(MessageUpdatedEvent {
id: 1,
content: "Ignore".into(),
});
tokio::time::sleep(std::time::Duration::from_millis(10)).await;
b.emit(MessageUpdatedEvent {
id: 2,
content: "Target".into(),
});
});
let event = bus
.wait_for::<MessageUpdatedEvent>(|e| e.id == 2)
.await
.unwrap();
assert_eq!(event.id, 2);
assert_eq!(event.content, "Target");
}