diff --git a/event_bus_typed/.gitignore b/event_bus/.gitignore similarity index 100% rename from event_bus_typed/.gitignore rename to event_bus/.gitignore diff --git a/event_bus/Cargo.toml b/event_bus/Cargo.toml index 3f17cbe..e4618a0 100644 --- a/event_bus/Cargo.toml +++ b/event_bus/Cargo.toml @@ -16,8 +16,8 @@ harness = false tokio = { version = "1.53.1", default-features = false, features = ["rt", "sync"] } parking_lot = "0.12.5" tracing = "0.1" -uuid = { version = "1.26.1", features = ["v4"] } [dev-dependencies] tokio = { version = "1.53.1", default-features = false, features = ["rt", "rt-multi-thread", "macros", "time", "sync"] } criterion = { version = "0.8.2", features = ["async_tokio"] } +uuid = { version = "1.26.1", features = ["v4"] } diff --git a/event_bus_typed/README.md b/event_bus/README.md similarity index 95% rename from event_bus_typed/README.md rename to event_bus/README.md index 3627081..c1bc713 100644 --- a/event_bus_typed/README.md +++ b/event_bus/README.md @@ -1,4 +1,4 @@ -# event_bus_typed +# event_bus Un bus d'événements asynchrone en mémoire pour Tokio, entièrement basé sur le typage fort en Rust (`TypeId`). @@ -11,7 +11,7 @@ Dans l'implémentation initiale (`event_bus`), le modèle était inspiré de Jav - L'émission imposait de spécifier un topic string (`bus.emit("topic", event)`). - La réception nécessitait de spécifier à la fois le type et le topic string (`bus.on_async::("topic", ...)`), puis d'effectuer un déréférencement / downcast dynamique (`downcast_ref::()` ou macro `match_event!`) sur chaque message reçu. -**`event_bus_typed` résout entièrement cette complexité :** +**`event_bus` résout entièrement cette complexité :** - **Typage fort natif** : le routage est directement effectué par l'identifiant de type (`std::any::TypeId`), sans nom de topic requis. - **Zéro downcast / déréférencement à la réception** : le callback reçoit directement la structure d'événement typée. - **Syntaxe ergonomique** : support complet de la syntaxe turbofish demandée `bus.on_async::(|event| async move { ... })` ainsi que de l'inférence automatique `bus.on_async(|event: MessageUpdatedEvent| async move { ... })`. @@ -48,7 +48,7 @@ pub struct MessageUpdatedEvent { ### 2. Émission d'événements ```rust -use event_bus_typed::EventBus; +use event_bus::EventBus; let bus = EventBus::new(); @@ -139,10 +139,10 @@ tokio::spawn(async move { Exécuter les tests du crate : ```bash -cargo test --manifest-path event_bus_typed/Cargo.toml +cargo test --manifest-path event_bus/Cargo.toml ``` Exécuter les benchmarks Criterion : ```bash -cargo bench --manifest-path event_bus_typed/Cargo.toml +cargo bench --manifest-path event_bus/Cargo.toml ``` diff --git a/event_bus/benches/event_bus_throughput.rs b/event_bus/benches/event_bus_throughput.rs index 32b5bb9..1530a86 100644 --- a/event_bus/benches/event_bus_throughput.rs +++ b/event_bus/benches/event_bus_throughput.rs @@ -1,3 +1,5 @@ +#![allow(dead_code)] + use std::sync::{ Arc, atomic::{AtomicU64, Ordering}, @@ -8,8 +10,6 @@ use criterion::{Criterion, Throughput, criterion_group, criterion_main}; use event_bus::EventBus; use tokio::runtime::Runtime; -const TOPIC: &str = "bench-topic"; - #[derive(Clone)] struct SmallEvent { value: u64, @@ -38,57 +38,46 @@ fn runtime() -> Runtime { Runtime::new().expect("failed to create tokio runtime") } -fn wait_until_received( - received: &AtomicU64, - expected: u64, -) -> impl std::future::Future + '_ { - async move { - while received.load(Ordering::Relaxed) < expected { - tokio::task::yield_now().await; - } +async fn wait_until_received(received: &AtomicU64, expected: u64) { + while received.load(Ordering::Relaxed) < expected { + tokio::task::yield_now().await; } } fn bench_emit_no_subscriber(c: &mut Criterion) { - let mut group = c.benchmark_group("event_bus/no_subscriber"); + let mut group = c.benchmark_group("event_bus_typed/no_subscriber"); group.throughput(Throughput::Elements(1)); let bus = EventBus::with_capacity(1024); group.bench_function("u64", |b| { b.iter(|| { - bus.emit(TOPIC, 42_u64); + bus.emit(42_u64); }); }); group.bench_function("small_struct", |b| { b.iter(|| { - bus.emit(TOPIC, SmallEvent { value: 42 }); + bus.emit(SmallEvent { value: 42 }); }); }); group.bench_function("string_struct", |b| { b.iter(|| { - bus.emit( - TOPIC, - StringEvent { - id: 42, - name: "Alice".to_string(), - message: "hello from benchmark".to_string(), - }, - ); + bus.emit(StringEvent { + id: 42, + name: "Alice".to_string(), + message: "hello from benchmark".to_string(), + }); }); }); group.bench_function("vec_payload_1kb", |b| { b.iter(|| { - bus.emit( - TOPIC, - VecEvent { - id: 42, - payload: vec![7_u8; 1024], - }, - ); + bus.emit(VecEvent { + id: 42, + payload: vec![7_u8; 1024], + }); }); }); @@ -96,13 +85,10 @@ fn bench_emit_no_subscriber(c: &mut Criterion) { group.bench_function("arc_payload_1kb", |b| { b.iter(|| { - bus.emit( - TOPIC, - ArcPayloadEvent { - id: 42, - payload: Arc::clone(&shared_payload), - }, - ); + bus.emit(ArcPayloadEvent { + id: 42, + payload: Arc::clone(&shared_payload), + }); }); }); @@ -112,13 +98,13 @@ fn bench_emit_no_subscriber(c: &mut Criterion) { fn bench_raw_subscriber(c: &mut Criterion) { let rt = runtime(); - let mut group = c.benchmark_group("event_bus/raw_subscriber"); + let mut group = c.benchmark_group("event_bus_typed/raw_subscriber"); group.throughput(Throughput::Elements(1)); group.bench_function("u64", |b| { b.to_async(&rt).iter_custom(|iters| async move { - let bus = Arc::new(EventBus::with_capacity(iters as usize + 1024)); - let mut rx = bus.on_raw(TOPIC); + let bus = EventBus::with_capacity(iters as usize + 1024); + let mut rx = bus.subscribe::(); let received = Arc::new(AtomicU64::new(0)); let receiver_count = Arc::clone(&received); @@ -134,23 +120,21 @@ fn bench_raw_subscriber(c: &mut Criterion) { let start = Instant::now(); for i in 0..iters { - bus.emit(TOPIC, i); + bus.emit(i); } wait_until_received(&received, iters).await; let elapsed = start.elapsed(); - receiver.abort(); - elapsed }); }); group.bench_function("small_struct", |b| { b.to_async(&rt).iter_custom(|iters| async move { - let bus = Arc::new(EventBus::with_capacity(iters as usize + 1024)); - let mut rx = bus.on_raw(TOPIC); + let bus = EventBus::with_capacity(iters as usize + 1024); + let mut rx = bus.subscribe::(); let received = Arc::new(AtomicU64::new(0)); let receiver_count = Arc::clone(&received); @@ -166,23 +150,21 @@ fn bench_raw_subscriber(c: &mut Criterion) { let start = Instant::now(); for i in 0..iters { - bus.emit(TOPIC, SmallEvent { value: i }); + bus.emit(SmallEvent { value: i }); } wait_until_received(&received, iters).await; let elapsed = start.elapsed(); - receiver.abort(); - elapsed }); }); group.bench_function("string_struct", |b| { b.to_async(&rt).iter_custom(|iters| async move { - let bus = Arc::new(EventBus::with_capacity(iters as usize + 1024)); - let mut rx = bus.on_raw(TOPIC); + let bus = EventBus::with_capacity(iters as usize + 1024); + let mut rx = bus.subscribe::(); let received = Arc::new(AtomicU64::new(0)); let receiver_count = Arc::clone(&received); @@ -198,30 +180,25 @@ fn bench_raw_subscriber(c: &mut Criterion) { let start = Instant::now(); for i in 0..iters { - bus.emit( - TOPIC, - StringEvent { - id: i, - name: "Alice".to_string(), - message: "hello from benchmark".to_string(), - }, - ); + bus.emit(StringEvent { + id: i, + name: "Alice".to_string(), + message: "hello from benchmark".to_string(), + }); } wait_until_received(&received, iters).await; let elapsed = start.elapsed(); - receiver.abort(); - elapsed }); }); group.bench_function("vec_payload_1kb", |b| { b.to_async(&rt).iter_custom(|iters| async move { - let bus = Arc::new(EventBus::with_capacity(iters as usize + 1024)); - let mut rx = bus.on_raw(TOPIC); + let bus = EventBus::with_capacity(iters as usize + 1024); + let mut rx = bus.subscribe::(); let received = Arc::new(AtomicU64::new(0)); let receiver_count = Arc::clone(&received); @@ -237,62 +214,56 @@ fn bench_raw_subscriber(c: &mut Criterion) { let start = Instant::now(); for i in 0..iters { - bus.emit( - TOPIC, - VecEvent { - id: i, - payload: vec![7_u8; 1024], - }, - ); + bus.emit(VecEvent { + id: i, + payload: vec![7_u8; 1024], + }); } wait_until_received(&received, iters).await; let elapsed = start.elapsed(); - receiver.abort(); - elapsed }); }); + let shared_payload: Arc<[u8]> = Arc::from(vec![7_u8; 1024].into_boxed_slice()); + group.bench_function("arc_payload_1kb", |b| { - b.to_async(&rt).iter_custom(|iters| async move { - let bus = Arc::new(EventBus::with_capacity(iters as usize + 1024)); - let mut rx = bus.on_raw(TOPIC); + let shared = Arc::clone(&shared_payload); + b.to_async(&rt).iter_custom(|iters| { + let payload = Arc::clone(&shared); + async move { + let bus = EventBus::with_capacity(iters as usize + 1024); + let mut rx = bus.subscribe::(); - let payload: Arc<[u8]> = Arc::from(vec![7_u8; 1024].into_boxed_slice()); + let received = Arc::new(AtomicU64::new(0)); + let receiver_count = Arc::clone(&received); - let received = Arc::new(AtomicU64::new(0)); - let receiver_count = Arc::clone(&received); - - let receiver = tokio::spawn(async move { - while receiver_count.load(Ordering::Relaxed) < iters { - if rx.recv().await.is_ok() { - receiver_count.fetch_add(1, Ordering::Relaxed); + let receiver = tokio::spawn(async move { + while receiver_count.load(Ordering::Relaxed) < iters { + if rx.recv().await.is_ok() { + receiver_count.fetch_add(1, Ordering::Relaxed); + } } - } - }); + }); - let start = Instant::now(); + let start = Instant::now(); - for i in 0..iters { - bus.emit( - TOPIC, - ArcPayloadEvent { + for i in 0..iters { + bus.emit(ArcPayloadEvent { id: i, payload: Arc::clone(&payload), - }, - ); + }); + } + + wait_until_received(&received, iters).await; + + let elapsed = start.elapsed(); + receiver.abort(); + elapsed } - - wait_until_received(&received, iters).await; - - let elapsed = start.elapsed(); - - receiver.abort(); - - elapsed }); }); @@ -302,17 +273,17 @@ fn bench_raw_subscriber(c: &mut Criterion) { fn bench_typed_callback(c: &mut Criterion) { let rt = runtime(); - let mut group = c.benchmark_group("event_bus/typed_callback"); + let mut group = c.benchmark_group("event_bus_typed/typed_callback"); group.throughput(Throughput::Elements(1)); group.bench_function("u64", |b| { b.to_async(&rt).iter_custom(|iters| async move { - let bus = Arc::new(EventBus::with_capacity(iters as usize + 1024)); + let bus = EventBus::with_capacity(iters as usize + 1024); let received = Arc::new(AtomicU64::new(0)); let handler_count = Arc::clone(&received); - let subscription = bus.on::(TOPIC, move |event| { + let subscription = bus.on::(move |event| { let _ = event; handler_count.fetch_add(1, Ordering::Relaxed); }); @@ -320,27 +291,25 @@ fn bench_typed_callback(c: &mut Criterion) { let start = Instant::now(); for i in 0..iters { - bus.emit(TOPIC, i); + bus.emit(i); } wait_until_received(&received, iters).await; let elapsed = start.elapsed(); - subscription.abort(); - elapsed }); }); group.bench_function("small_struct", |b| { b.to_async(&rt).iter_custom(|iters| async move { - let bus = Arc::new(EventBus::with_capacity(iters as usize + 1024)); + let bus = EventBus::with_capacity(iters as usize + 1024); let received = Arc::new(AtomicU64::new(0)); let handler_count = Arc::clone(&received); - let subscription = bus.on::(TOPIC, move |event| { + let subscription = bus.on::(move |event| { let _ = event.value; handler_count.fetch_add(1, Ordering::Relaxed); }); @@ -348,27 +317,25 @@ fn bench_typed_callback(c: &mut Criterion) { let start = Instant::now(); for i in 0..iters { - bus.emit(TOPIC, SmallEvent { value: i }); + bus.emit(SmallEvent { value: i }); } wait_until_received(&received, iters).await; let elapsed = start.elapsed(); - subscription.abort(); - elapsed }); }); group.bench_function("string_struct", |b| { b.to_async(&rt).iter_custom(|iters| async move { - let bus = Arc::new(EventBus::with_capacity(iters as usize + 1024)); + let bus = EventBus::with_capacity(iters as usize + 1024); let received = Arc::new(AtomicU64::new(0)); let handler_count = Arc::clone(&received); - let subscription = bus.on::(TOPIC, move |event| { + let subscription = bus.on::(move |event| { let _ = event.id; let _ = event.name.len(); let _ = event.message.len(); @@ -378,34 +345,29 @@ fn bench_typed_callback(c: &mut Criterion) { let start = Instant::now(); for i in 0..iters { - bus.emit( - TOPIC, - StringEvent { - id: i, - name: "Alice".to_string(), - message: "hello from benchmark".to_string(), - }, - ); + bus.emit(StringEvent { + id: i, + name: "Alice".to_string(), + message: "hello from benchmark".to_string(), + }); } wait_until_received(&received, iters).await; let elapsed = start.elapsed(); - subscription.abort(); - elapsed }); }); group.bench_function("vec_payload_1kb", |b| { b.to_async(&rt).iter_custom(|iters| async move { - let bus = Arc::new(EventBus::with_capacity(iters as usize + 1024)); + let bus = EventBus::with_capacity(iters as usize + 1024); let received = Arc::new(AtomicU64::new(0)); let handler_count = Arc::clone(&received); - let subscription = bus.on::(TOPIC, move |event| { + let subscription = bus.on::(move |event| { let _ = event.id; let _ = event.payload.len(); handler_count.fetch_add(1, Ordering::Relaxed); @@ -414,58 +376,53 @@ fn bench_typed_callback(c: &mut Criterion) { let start = Instant::now(); for i in 0..iters { - bus.emit( - TOPIC, - VecEvent { - id: i, - payload: vec![7_u8; 1024], - }, - ); + bus.emit(VecEvent { + id: i, + payload: vec![7_u8; 1024], + }); } wait_until_received(&received, iters).await; let elapsed = start.elapsed(); - subscription.abort(); - elapsed }); }); + let shared_payload: Arc<[u8]> = Arc::from(vec![7_u8; 1024].into_boxed_slice()); + group.bench_function("arc_payload_1kb", |b| { - b.to_async(&rt).iter_custom(|iters| async move { - let bus = Arc::new(EventBus::with_capacity(iters as usize + 1024)); - let payload: Arc<[u8]> = Arc::from(vec![7_u8; 1024].into_boxed_slice()); + let shared = Arc::clone(&shared_payload); + b.to_async(&rt).iter_custom(|iters| { + let payload = Arc::clone(&shared); + async move { + let bus = EventBus::with_capacity(iters as usize + 1024); - let received = Arc::new(AtomicU64::new(0)); - let handler_count = Arc::clone(&received); + let received = Arc::new(AtomicU64::new(0)); + let handler_count = Arc::clone(&received); - let subscription = bus.on::(TOPIC, move |event| { - let _ = event.id; - let _ = event.payload.len(); - handler_count.fetch_add(1, Ordering::Relaxed); - }); + let subscription = bus.on::(move |event| { + let _ = event.id; + let _ = event.payload.len(); + handler_count.fetch_add(1, Ordering::Relaxed); + }); - let start = Instant::now(); + let start = Instant::now(); - for i in 0..iters { - bus.emit( - TOPIC, - ArcPayloadEvent { + for i in 0..iters { + bus.emit(ArcPayloadEvent { id: i, payload: Arc::clone(&payload), - }, - ); + }); + } + + wait_until_received(&received, iters).await; + + let elapsed = start.elapsed(); + subscription.abort(); + elapsed } - - wait_until_received(&received, iters).await; - - let elapsed = start.elapsed(); - - subscription.abort(); - - elapsed }); }); @@ -475,13 +432,13 @@ fn bench_typed_callback(c: &mut Criterion) { fn bench_multiple_subscribers(c: &mut Criterion) { let rt = runtime(); - let mut group = c.benchmark_group("event_bus/multiple_subscribers"); + let mut group = c.benchmark_group("event_bus_typed/multiple_subscribers"); group.throughput(Throughput::Elements(1)); for subscriber_count in [1_u64, 2, 4, 8, 16, 32] { group.bench_function(format!("{subscriber_count}_subscribers"), |b| { b.to_async(&rt).iter_custom(|iters| async move { - let bus = Arc::new(EventBus::with_capacity(iters as usize + 1024)); + let bus = EventBus::with_capacity(iters as usize + 1024); let expected = iters * subscriber_count; let received = Arc::new(AtomicU64::new(0)); @@ -490,7 +447,7 @@ fn bench_multiple_subscribers(c: &mut Criterion) { for _ in 0..subscriber_count { let handler_count = Arc::clone(&received); - let subscription = bus.on::(TOPIC, move |event| { + let subscription = bus.on::(move |event| { let _ = event.value; handler_count.fetch_add(1, Ordering::Relaxed); }); @@ -501,7 +458,7 @@ fn bench_multiple_subscribers(c: &mut Criterion) { let start = Instant::now(); for i in 0..iters { - bus.emit(TOPIC, SmallEvent { value: i }); + bus.emit(SmallEvent { value: i }); } wait_until_received(&received, expected).await; diff --git a/event_bus/src/bus.rs b/event_bus/src/bus.rs index 551f2b6..c0a93db 100644 --- a/event_bus/src/bus.rs +++ b/event_bus/src/bus.rs @@ -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; +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 Event for T {} -impl Scope { - pub fn new(key: impl Into, value: ScopeValue) -> Self { - Self { - key: key.into(), - value, - } - } - - pub fn uuid(key: impl Into, value: Uuid) -> Self { - Self::new(key, ScopeValue::Uuid(value)) - } - - pub fn string(key: impl Into, value: impl Into) -> Self { - Self::new(key, ScopeValue::String(value.into())) - } -} -impl IntoIterator for Scope { - type Item = Scope; - type IntoIter = iter::Once; - - fn into_iter(self) -> Self::IntoIter { - iter::once(self) - } -} - -/// The central event bus. -/// -/// Share it via `Arc` 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-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-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>>, +struct EventBusInner { + channels: RwLock>>, 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 { - { - 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(&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(&self, topic: &str, scopes: impl IntoIterator, 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-connected", |user| { - /// println!("Connected: {:?}", user); - /// }); - /// ``` - pub fn on(&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::() { - 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-connected", |user| async move { - /// println!("(async) Connected: {:?}", user); - /// // async work here: DB query, HTTP, etc. - /// }); - /// ``` - pub fn on_async(&self, topic: &str, handler: F) -> JoinHandle<()> - where - T: Any + Send + Sync + Clone + 'static, - F: Fn(T) -> Fut + Send + Sync + 'static, - Fut: Future + 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::() { - 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(&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 + 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::() { - 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 { - 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 { - 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::(|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::(|event| async move { +/// println!("(async) Updated message: {:?}", event); +/// }); +/// +/// bus.emit(MessageUpdatedEvent { +/// id: 42, +/// content: "World!".into(), +/// }); +/// } +/// ``` +#[derive(Debug, Clone)] +pub struct EventBus { + inner: Arc, } 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(&self) -> broadcast::Sender { + let type_id = TypeId::of::(); + + if let Some(tx) = self + .inner + .channels + .read() + .get(&type_id) + .and_then(|entry| entry.downcast_ref::>()) + { + return tx.clone(); + } + + let mut channels = self.inner.channels.write(); + if let Some(tx) = channels + .get(&type_id) + .and_then(|entry| entry.downcast_ref::>()) + { + return tx.clone(); + } + + let (tx, _) = broadcast::channel::(self.inner.capacity); + debug!( + event_type = std::any::type_name::(), + 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(&self, event: E) { + trace!( + event_type = std::any::type_name::(), + "Emitting event" + ); + + let type_id = TypeId::of::(); + + if let Some(tx) = self + .inner + .channels + .read() + .get(&type_id) + .and_then(|entry| entry.downcast_ref::>()) + .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(&self, handler: impl Fn(E) + Send + Sync + 'static) -> JoinHandle<()> { + let mut rx = self.subscribe::(); + let type_name = std::any::type_name::(); + + 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::(|event| async move { + /// println!("Updated: {:?}", event.id); + /// }); + /// ``` + pub fn on_async(&self, handler: impl AsyncHandler) -> JoinHandle<()> { + let mut rx = self.subscribe::(); + let type_name = std::any::type_name::(); + + 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( + &self, + context: C, + handler: impl AsyncHandlerWith, + ) -> JoinHandle<()> { + let mut rx = self.subscribe::(); + let type_name = std::any::type_name::(); + + 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`] 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::(); + /// bus.emit(MyEvent); + /// if let Ok(event) = rx.recv().await { + /// // direct typed `event` + /// } + /// } + /// ``` + pub fn subscribe(&self) -> broadcast::Receiver { + self.get_or_create_sender::().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::().await.unwrap(); + /// println!("Received next event: {}", event.id); + /// } + /// ``` + pub async fn wait_next(&self) -> Result { + let mut rx = self.subscribe::(); + 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::(|e| e.id == target_id).await.unwrap(); + /// println!("Saved message confirmed: {}", event.id); + /// } + /// ``` + pub async fn wait_for( + &self, + mut predicate: impl FnMut(&E) -> bool, + ) -> Result { + let mut rx = self.subscribe::(); + 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::(), + 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(&self) -> usize { + let type_id = TypeId::of::(); + + self.inner + .channels + .read() + .get(&type_id) + .and_then(|e| e.downcast_ref::>()) + .map(|tx| tx.receiver_count()) + .unwrap_or(0) + } + + /// Returns `true` if there are any active subscribers for event type `E`. + pub fn has_subscribers(&self) -> bool { + self.subscriber_count::() > 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(&self) { + let type_id = TypeId::of::(); + self.inner.channels.write().remove(&type_id); + } +} diff --git a/event_bus_typed/src/handler.rs b/event_bus/src/handler.rs similarity index 97% rename from event_bus_typed/src/handler.rs rename to event_bus/src/handler.rs index b9c5871..004a5cb 100644 --- a/event_bus_typed/src/handler.rs +++ b/event_bus/src/handler.rs @@ -10,7 +10,7 @@ use std::future::Future; /// enabling seamless turbofish syntax with clean field access: /// /// ```rust,no_run -/// # use event_bus_typed::EventBus; +/// # use event_bus::EventBus; /// # #[derive(Clone)] struct MessageUpdatedEvent { id: u64 } /// # let bus = EventBus::new(); /// bus.on_async::(|event| async move { diff --git a/event_bus/src/lib.rs b/event_bus/src/lib.rs index e17a115..c17375a 100644 --- a/event_bus/src/lib.rs +++ b/event_bus/src/lib.rs @@ -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::(|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::(|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; diff --git a/event_bus/src/tests.rs b/event_bus/src/tests.rs index ad76154..89342b4 100644 --- a/event_bus/src/tests.rs +++ b/event_bus/src/tests.rs @@ -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-connected", move |user| { - if user.name == "Alice" { + bus.on::(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::("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::("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-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-connected", move |user| { - let f = Arc::clone(&flag); + // Exact syntax specified by the user: + // event_bus.on_async::(|event| async move { ... }); + bus.on_async::(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::(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-connected", move |user| { - if user.name == "Bob" { - u.store(true, Ordering::SeqCst); - } - }); - bus.on::("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::(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::(move |event| { + if event.content == "Test" { + m_flag.store(true, Ordering::SeqCst); + } + }); + + bus.on::(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::(); + + 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::(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::(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::(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::(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::(), 0); + assert!(!bus.has_subscribers::()); + + let _sub = bus.subscribe::(); + assert_eq!(bus.subscriber_count::(), 1); + assert!(bus.has_subscribers::()); + assert_eq!(bus.channel_count(), 1); + + bus.clear_type::(); + assert_eq!(bus.subscriber_count::(), 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::(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::().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::(|e| e.id == 2) + .await + .unwrap(); + assert_eq!(event.id, 2); + assert_eq!(event.content, "Target"); +} diff --git a/event_bus_typed/Cargo.lock b/event_bus_typed/Cargo.lock deleted file mode 100644 index 950f0ae..0000000 --- a/event_bus_typed/Cargo.lock +++ /dev/null @@ -1,798 +0,0 @@ -# This file is automatically @generated by Cargo. -# It is not intended for manual editing. -version = 4 - -[[package]] -name = "aho-corasick" -version = "1.1.5" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c982642fa9e8606056828ee9a8505737230110bb1099153c79efe865c59d12ba" -dependencies = [ - "memchr", -] - -[[package]] -name = "alloca" -version = "0.4.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e5a7d05ea6aea7e9e64d25b9156ba2fee3fdd659e34e41063cd2fc7cd020d7f4" -dependencies = [ - "cc", -] - -[[package]] -name = "anes" -version = "0.1.6" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4b46cbb362ab8752921c97e041f5e366ee6297bd428a31275b9fcf1e380f7299" - -[[package]] -name = "anstyle" -version = "1.0.14" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "940b3a0ca603d1eade50a4846a2afffd5ef57a9feac2c0e2ec2e14f9ead76000" - -[[package]] -name = "autocfg" -version = "1.5.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f2032f911046de80f0a198e0901378627c33f59ea0ac00e363d481118bd70a53" - -[[package]] -name = "bitflags" -version = "2.13.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3ded4057c258ba199e2d26386d3af3780957ecaee6c4ef4041c6b4b8b97c0b06" - -[[package]] -name = "bumpalo" -version = "3.20.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "72f5acc6cb2ba439de613abc23857ec3d78374d8ed5ac84e9d11336e87da8649" - -[[package]] -name = "cast" -version = "0.3.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "37b2a672a2cb129a2e41c10b1224bb368f9f37a2b16b612598138befd7b37eb5" - -[[package]] -name = "cc" -version = "1.4.7" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "54413ede23c2daf518f35156dfde027feb2374004d63bd497f983c8db9c0e313" -dependencies = [ - "find-msvc-tools", - "shlex", -] - -[[package]] -name = "cfg-if" -version = "1.0.5" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4e7648175b45a9a48536d676f68d918270699102aa8dab5496df06904c914600" - -[[package]] -name = "ciborium" -version = "0.2.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "42e69ffd6f0917f5c029256a24d0161db17cea3997d185db0d35926308770f0e" -dependencies = [ - "ciborium-io", - "ciborium-ll", - "serde", -] - -[[package]] -name = "ciborium-io" -version = "0.2.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "05afea1e0a06c9be33d539b876f1ce3692f4afea2cb41f740e7743225ed1c757" - -[[package]] -name = "ciborium-ll" -version = "0.2.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "57663b653d948a338bfb3eeba9bb2fd5fcfaecb9e199e87e1eda4d9e8b240fd9" -dependencies = [ - "ciborium-io", - "half", -] - -[[package]] -name = "clap" -version = "4.6.7" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "aa8876b300ab35ba921adea3dfd70157a46249b33f95c9084ae5709785478946" -dependencies = [ - "clap_builder", -] - -[[package]] -name = "clap_builder" -version = "4.6.7" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ec0797fb7aeb1406c84efac526901f7ec3ead2124f946b494e72879d4b54704d" -dependencies = [ - "anstyle", - "clap_lex", -] - -[[package]] -name = "clap_lex" -version = "1.1.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1c133bc6a41be0d194c306b5506d15e6feeea7b1d6604bd3f8310dfb2ca96486" - -[[package]] -name = "criterion" -version = "0.8.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "950046b2aa2492f9a536f5f4f9a3de7b9e2476e575e05bd6c333371add4d98f3" -dependencies = [ - "alloca", - "anes", - "cast", - "ciborium", - "clap", - "criterion-plot", - "itertools", - "num-traits", - "oorandom", - "page_size", - "plotters", - "rayon", - "regex", - "serde", - "serde_json", - "tinytemplate", - "tokio", - "walkdir", -] - -[[package]] -name = "criterion-plot" -version = "0.8.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d8d80a2f4f5b554395e47b5d8305bc3d27813bacb73493eb1001e8f76dae29ea" -dependencies = [ - "cast", - "itertools", -] - -[[package]] -name = "crossbeam-deque" -version = "0.8.8" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "622f3fc73690be383c7214310406f28a90e6edeadc3cea882f9d71e495b9711a" -dependencies = [ - "crossbeam-epoch", - "crossbeam-utils", -] - -[[package]] -name = "crossbeam-epoch" -version = "0.9.21" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "dc74980687109a3b14c72fd458107bf0baa1da1a1a805e178d15501ba9b86d9d" -dependencies = [ - "crossbeam-utils", -] - -[[package]] -name = "crossbeam-utils" -version = "0.8.23" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a31eee39dddec8330830986fcd7625edb5a24ec90ea038215273bbc3adb08ac6" - -[[package]] -name = "crunchy" -version = "0.2.4" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "460fbee9c2c2f33933d720630a6a0bac33ba7053db5344fac858d4b8952d77d5" - -[[package]] -name = "either" -version = "1.18.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "252afb9ae5eaa683babdc6a068b3f5726eb19e05070c731f9b2a23a7c3e8ed34" - -[[package]] -name = "event_bus_typed" -version = "0.1.0" -dependencies = [ - "criterion", - "parking_lot", - "tokio", - "tracing", - "uuid", -] - -[[package]] -name = "find-msvc-tools" -version = "0.1.13" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ef25905e51abafe4dcea6c15fec58c57b601cdbd0ee53d22ea1d3016c587d39b" - -[[package]] -name = "futures-core" -version = "0.3.34" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "92d699e522242e69e3003b94ecc1f960f3a5e015aa7c5d7486e65ad01dd94f5e" - -[[package]] -name = "futures-task" -version = "0.3.34" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "cd417de3d1d015fc3bfd2b1ea46dfc7bab72ef86f1cc7cc9c78e728b34a6d1fd" - -[[package]] -name = "futures-util" -version = "0.3.34" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0d50a92467f8ba5dd6e3ee5d4bd04d73ab2e4e1c44474a0674821dfce14b79bc" -dependencies = [ - "futures-core", - "futures-task", - "pin-project-lite", - "slab", -] - -[[package]] -name = "getrandom" -version = "0.4.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "300e883d756b2e4ec94e02791f39b04b522276138852cfc41d9fb7e904106099" -dependencies = [ - "cfg-if", - "libc", - "r-efi", -] - -[[package]] -name = "half" -version = "2.7.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6ea2d84b969582b4b1864a92dc5d27cd2b77b622a8d79306834f1be5ba20d84b" -dependencies = [ - "cfg-if", - "crunchy", - "zerocopy", -] - -[[package]] -name = "itertools" -version = "0.13.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "413ee7dfc52ee1a4949ceeb7dbc8a33f2d6c088194d9f922fb8318faf1f01186" -dependencies = [ - "either", -] - -[[package]] -name = "itoa" -version = "1.0.18" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8f42a60cbdf9a97f5d2305f08a87dc4e09308d1276d28c869c684d7777685682" - -[[package]] -name = "js-sys" -version = "0.3.105" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ce57d20d1ea864ce2ac172ab472d409214f4fd359f0b2a2775abdf522e2af99e" -dependencies = [ - "cfg-if", - "futures-util", - "wasm-bindgen", -] - -[[package]] -name = "libc" -version = "0.2.189" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3eaf3ede3fee6db1a4c2ee091bf8a8b4dccdc6d17f656fb07896ee72867612f2" - -[[package]] -name = "lock_api" -version = "0.4.14" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "224399e74b87b5f3557511d98dff8b14089b3dadafcab6bb93eab67d3aace965" -dependencies = [ - "scopeguard", -] - -[[package]] -name = "memchr" -version = "2.8.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "cf8baf1c55e62ffcace7a9f06f4bd9cd3f0c4beb022d3b367256b91b87513d98" - -[[package]] -name = "num-traits" -version = "0.2.19" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "071dfc062690e90b734c0b2273ce72ad0ffa95f0c74596bc250dcfd960262841" -dependencies = [ - "autocfg", -] - -[[package]] -name = "once_cell" -version = "1.21.4" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9f7c3e4beb33f85d45ae3e3a1792185706c8e16d043238c593331cc7cd313b50" - -[[package]] -name = "oorandom" -version = "11.1.5" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d6790f58c7ff633d8771f42965289203411a5e5c68388703c06e14f24770b41e" - -[[package]] -name = "page_size" -version = "0.6.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "30d5b2194ed13191c1999ae0704b7839fb18384fa22e49b57eeaa97d79ce40da" -dependencies = [ - "libc", - "winapi", -] - -[[package]] -name = "parking_lot" -version = "0.12.5" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "93857453250e3077bd71ff98b6a65ea6621a19bb0f559a85248955ac12c45a1a" -dependencies = [ - "lock_api", - "parking_lot_core", -] - -[[package]] -name = "parking_lot_core" -version = "0.9.12" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2621685985a2ebf1c516881c026032ac7deafcda1a2c9b7850dc81e3dfcb64c1" -dependencies = [ - "cfg-if", - "libc", - "redox_syscall", - "smallvec", - "windows-link", -] - -[[package]] -name = "pin-project-lite" -version = "0.2.17" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a89322df9ebe1c1578d689c92318e070967d1042b512afbe49518723f4e6d5cd" - -[[package]] -name = "plotters" -version = "0.3.7" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5aeb6f403d7a4911efb1e33402027fc44f29b5bf6def3effcc22d7bb75f2b747" -dependencies = [ - "num-traits", - "plotters-backend", - "plotters-svg", - "wasm-bindgen", - "web-sys", -] - -[[package]] -name = "plotters-backend" -version = "0.3.7" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "df42e13c12958a16b3f7f4386b9ab1f3e7933914ecea48da7139435263a4172a" - -[[package]] -name = "plotters-svg" -version = "0.3.7" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "51bae2ac328883f7acdfea3d66a7c35751187f870bc81f94563733a154d7a670" -dependencies = [ - "plotters-backend", -] - -[[package]] -name = "proc-macro2" -version = "1.0.107" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "985e7ec9bb745e6ce6535b544d84d6cd6f7ad8bd711c398938ae983b91a766d9" -dependencies = [ - "unicode-ident", -] - -[[package]] -name = "quote" -version = "1.0.47" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1fbf4db142a473a8d80c26bbf18454ed458bf8d26c8219c331daecfdbd079001" -dependencies = [ - "proc-macro2", -] - -[[package]] -name = "r-efi" -version = "6.0.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f8dcc9c7d52a811697d2151c701e0d08956f92b0e24136cf4cf27b57a6a0d9bf" - -[[package]] -name = "rayon" -version = "1.12.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "fb39b166781f92d482534ef4b4b1b2568f42613b53e5b6c160e24cfbfa30926d" -dependencies = [ - "either", - "rayon-core", -] - -[[package]] -name = "rayon-core" -version = "1.13.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "22e18b0f0062d30d4230b2e85ff77fdfe4326feb054b9783a3460d8435c8ab91" -dependencies = [ - "crossbeam-deque", - "crossbeam-utils", -] - -[[package]] -name = "redox_syscall" -version = "0.5.18" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ed2bf2547551a7053d6fdfafda3f938979645c44812fbfcda098faae3f1a362d" -dependencies = [ - "bitflags", -] - -[[package]] -name = "regex" -version = "1.13.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f020237b6c8eed93db2e2cb53c00c60a8e1bc73da7d073199a1180401450218d" -dependencies = [ - "aho-corasick", - "memchr", - "regex-automata", - "regex-syntax", -] - -[[package]] -name = "regex-automata" -version = "0.4.18" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ad8553b9b26413251cbf30e620595c7a41b3887f03da04579c0e6b0d6a06b4b2" -dependencies = [ - "aho-corasick", - "memchr", - "regex-syntax", -] - -[[package]] -name = "regex-syntax" -version = "0.8.11" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d6f6ff9a378485b298a5286656da665ba74413d36db0979633275d2e708145d4" - -[[package]] -name = "rustversion" -version = "1.0.23" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "cf54715a573b99ac80df0bc206da022bcd442c974952c7b9720069370852e21f" - -[[package]] -name = "same-file" -version = "1.0.6" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "93fc1dc3aaa9bfed95e02e6eadabb4baf7e3078b0bd1b4d7b6b0b68378900502" -dependencies = [ - "winapi-util", -] - -[[package]] -name = "scopeguard" -version = "1.2.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "94143f37725109f92c262ed2cf5e59bce7498c01bcc1502d7b9afe439a4e9f49" - -[[package]] -name = "serde" -version = "1.0.229" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4148590afebada386688f18773da617792bf2ef03ffc1e4cbd2b1d45b023e0ba" -dependencies = [ - "serde_core", - "serde_derive", -] - -[[package]] -name = "serde_core" -version = "1.0.229" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "67dca2c9c51e58a4791a4b1ed58308b39c64224d349a935ab5039aa360942a48" -dependencies = [ - "serde_derive", -] - -[[package]] -name = "serde_derive" -version = "1.0.229" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e7a5d71263a5a7d47b41f6b3f06ba276f10cc18b0931f1799f710578e2309348" -dependencies = [ - "proc-macro2", - "quote", - "syn 3.0.6", -] - -[[package]] -name = "serde_json" -version = "1.0.151" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c841b55ecdae098c80dcae9cf767f6f8a0c2cdb3416bbef72181df4d0fe73f14" -dependencies = [ - "itoa", - "memchr", - "serde", - "serde_core", - "zmij", -] - -[[package]] -name = "shlex" -version = "2.0.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f8fadd59c855ef2080decdef8ff161eb6661b86933c9d82e5ba29dc602a55aba" - -[[package]] -name = "slab" -version = "0.4.12" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0c790de23124f9ab44544d7ac05d60440adc586479ce501c1d6d7da3cd8c9cf5" - -[[package]] -name = "smallvec" -version = "1.16.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ba467056f1b547ed52077911161fc86985becbc60e8e1857c8a144dab0def891" - -[[package]] -name = "syn" -version = "2.0.119" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "872831b642d1a07999a962a351ed35b955ea2cfc8f3862091e2a240a84f17297" -dependencies = [ - "proc-macro2", - "quote", - "unicode-ident", -] - -[[package]] -name = "syn" -version = "3.0.6" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8593e8e72159ed2257d083c7a454a85cbf854f37a0966d8d483aff8c8a3ebcee" -dependencies = [ - "proc-macro2", - "quote", - "unicode-ident", -] - -[[package]] -name = "tinytemplate" -version = "1.2.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "be4d6b5f19ff7664e8c98d03e2139cb510db9b0a60b55f8e8709b689d939b6bc" -dependencies = [ - "serde", - "serde_json", -] - -[[package]] -name = "tokio" -version = "1.53.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "202caea871b69668250d242070849eb495be178ed697a3e98aebce5bc81a0bed" -dependencies = [ - "pin-project-lite", - "tokio-macros", -] - -[[package]] -name = "tokio-macros" -version = "2.7.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "78773a2a397f451582ce068015985c33193cf6dea8b74d2a639fe457b2f07b0e" -dependencies = [ - "proc-macro2", - "quote", - "syn 3.0.6", -] - -[[package]] -name = "tracing" -version = "0.1.44" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "63e71662fa4b2a2c3a26f570f037eb95bb1f85397f3cd8076caed2f026a6d100" -dependencies = [ - "pin-project-lite", - "tracing-attributes", - "tracing-core", -] - -[[package]] -name = "tracing-attributes" -version = "0.1.31" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7490cfa5ec963746568740651ac6781f701c9c5ea257c58e057f3ba8cf69e8da" -dependencies = [ - "proc-macro2", - "quote", - "syn 2.0.119", -] - -[[package]] -name = "tracing-core" -version = "0.1.36" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "db97caf9d906fbde555dd62fa95ddba9eecfd14cb388e4f491a66d74cd5fb79a" -dependencies = [ - "once_cell", -] - -[[package]] -name = "unicode-ident" -version = "1.0.26" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d245f478577f809a851594d02313b640fb437e0bb33866753cff937863096954" - -[[package]] -name = "uuid" -version = "1.26.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2ef6dac1e96601b4fb3acccccff2139741fcb757cb9a36089bf5be91cfb285ce" -dependencies = [ - "getrandom", - "js-sys", - "wasm-bindgen", -] - -[[package]] -name = "walkdir" -version = "2.5.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "29790946404f91d9c5d06f9874efddea1dc06c5efe94541a7d6863108e3a5e4b" -dependencies = [ - "same-file", - "winapi-util", -] - -[[package]] -name = "wasm-bindgen" -version = "0.2.128" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "aecb87a33d3b0c5e3b7aa46336eaf486cffafbd281b195e4c8b80d50df2351bf" -dependencies = [ - "cfg-if", - "once_cell", - "rustversion", - "wasm-bindgen-macro", - "wasm-bindgen-shared", -] - -[[package]] -name = "wasm-bindgen-macro" -version = "0.2.128" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a690d511e3c1a8b3a55e33511e3c2c00c78415cd23650f32b808627f5696b9ed" -dependencies = [ - "quote", - "wasm-bindgen-macro-support", -] - -[[package]] -name = "wasm-bindgen-macro-support" -version = "0.2.128" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "411e4887f0071ef2d2164a9d5fdf2d20efbef78fccd3a78b0c10a1dc5295e48a" -dependencies = [ - "bumpalo", - "proc-macro2", - "quote", - "syn 3.0.6", - "wasm-bindgen-shared", -] - -[[package]] -name = "wasm-bindgen-shared" -version = "0.2.128" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "81941cd78d0c92026c33e5e01312845a4cb1e9af3407f9134b100dd03144103e" -dependencies = [ - "unicode-ident", -] - -[[package]] -name = "web-sys" -version = "0.3.105" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9fbddc4a036f00ec4f18c83445bd3115cb306a91da554919a099d9222fe4a7f8" -dependencies = [ - "js-sys", - "wasm-bindgen", -] - -[[package]] -name = "winapi" -version = "0.3.9" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5c839a674fcd7a98952e593242ea400abe93992746761e38641405d28b00f419" -dependencies = [ - "winapi-i686-pc-windows-gnu", - "winapi-x86_64-pc-windows-gnu", -] - -[[package]] -name = "winapi-i686-pc-windows-gnu" -version = "0.4.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ac3b87c63620426dd9b991e5ce0329eff545bccbbb34f3be09ff6fb6ab51b7b6" - -[[package]] -name = "winapi-util" -version = "0.1.11" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c2a7b1c03c876122aa43f3020e6c3c3ee5c05081c9a00739faf7503aeba10d22" -dependencies = [ - "windows-sys", -] - -[[package]] -name = "winapi-x86_64-pc-windows-gnu" -version = "0.4.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "712e227841d057c1ee1cd2fb22fa7e5a5461ae8e48fa2ca79ec42cfc1931183f" - -[[package]] -name = "windows-link" -version = "0.2.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f0805222e57f7521d6a62e36fa9163bc891acd422f971defe97d64e70d0a4fe5" - -[[package]] -name = "windows-sys" -version = "0.61.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ae137229bcbd6cdf0f7b80a31df61766145077ddf49416a728b02cb3921ff3fc" -dependencies = [ - "windows-link", -] - -[[package]] -name = "zerocopy" -version = "0.8.57" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d35102a9f36d089ccae9e4c6802bc118be4487b80aaffc0ab4e0cf5ce92d2873" -dependencies = [ - "zerocopy-derive", -] - -[[package]] -name = "zerocopy-derive" -version = "0.8.57" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "146c01f5ab44258da43cf276c74a2763db2ff3969c9c652c3f2de07041d0b2bc" -dependencies = [ - "proc-macro2", - "quote", - "syn 2.0.119", -] - -[[package]] -name = "zmij" -version = "1.0.23" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "29666d0abbfad1e3dc4dcf6144730dd3a3ab225bbbdac83319345b1b44ccfc1b" diff --git a/event_bus_typed/Cargo.toml b/event_bus_typed/Cargo.toml deleted file mode 100644 index 9e6f23f..0000000 --- a/event_bus_typed/Cargo.toml +++ /dev/null @@ -1,25 +0,0 @@ -[package] -name = "event_bus_typed" -version = "0.1.0" -edition = "2024" -publish = false - -[workspace] - -[lib] -name = "event_bus_typed" -path = "src/lib.rs" - -[[bench]] -name = "event_bus_throughput" -harness = false - -[dependencies] -tokio = { version = "1.53.1", default-features = false, features = ["rt", "sync"] } -parking_lot = "0.12.5" -tracing = "0.1" - -[dev-dependencies] -tokio = { version = "1.53.1", default-features = false, features = ["rt", "rt-multi-thread", "macros", "time", "sync"] } -criterion = { version = "0.8.2", features = ["async_tokio"] } -uuid = { version = "1.26.1", features = ["v4"] } diff --git a/event_bus_typed/benches/event_bus_throughput.rs b/event_bus_typed/benches/event_bus_throughput.rs deleted file mode 100644 index 38e8ff2..0000000 --- a/event_bus_typed/benches/event_bus_throughput.rs +++ /dev/null @@ -1,488 +0,0 @@ -#![allow(dead_code)] - -use std::sync::{ - Arc, - atomic::{AtomicU64, Ordering}, -}; -use std::time::Instant; - -use criterion::{Criterion, Throughput, criterion_group, criterion_main}; -use event_bus_typed::EventBus; -use tokio::runtime::Runtime; - -#[derive(Clone)] -struct SmallEvent { - value: u64, -} - -#[derive(Clone)] -struct StringEvent { - id: u64, - name: String, - message: String, -} - -#[derive(Clone)] -struct VecEvent { - id: u64, - payload: Vec, -} - -#[derive(Clone)] -struct ArcPayloadEvent { - id: u64, - payload: Arc<[u8]>, -} - -fn runtime() -> Runtime { - Runtime::new().expect("failed to create tokio runtime") -} - -async fn wait_until_received(received: &AtomicU64, expected: u64) { - while received.load(Ordering::Relaxed) < expected { - tokio::task::yield_now().await; - } -} - -fn bench_emit_no_subscriber(c: &mut Criterion) { - let mut group = c.benchmark_group("event_bus_typed/no_subscriber"); - group.throughput(Throughput::Elements(1)); - - let bus = EventBus::with_capacity(1024); - - group.bench_function("u64", |b| { - b.iter(|| { - bus.emit(42_u64); - }); - }); - - group.bench_function("small_struct", |b| { - b.iter(|| { - bus.emit(SmallEvent { value: 42 }); - }); - }); - - group.bench_function("string_struct", |b| { - b.iter(|| { - bus.emit(StringEvent { - id: 42, - name: "Alice".to_string(), - message: "hello from benchmark".to_string(), - }); - }); - }); - - group.bench_function("vec_payload_1kb", |b| { - b.iter(|| { - bus.emit(VecEvent { - id: 42, - payload: vec![7_u8; 1024], - }); - }); - }); - - let shared_payload: Arc<[u8]> = Arc::from(vec![7_u8; 1024].into_boxed_slice()); - - group.bench_function("arc_payload_1kb", |b| { - b.iter(|| { - bus.emit(ArcPayloadEvent { - id: 42, - payload: Arc::clone(&shared_payload), - }); - }); - }); - - group.finish(); -} - -fn bench_raw_subscriber(c: &mut Criterion) { - let rt = runtime(); - - let mut group = c.benchmark_group("event_bus_typed/raw_subscriber"); - group.throughput(Throughput::Elements(1)); - - group.bench_function("u64", |b| { - b.to_async(&rt).iter_custom(|iters| async move { - let bus = EventBus::with_capacity(iters as usize + 1024); - let mut rx = bus.subscribe::(); - - let received = Arc::new(AtomicU64::new(0)); - let receiver_count = Arc::clone(&received); - - let receiver = tokio::spawn(async move { - while receiver_count.load(Ordering::Relaxed) < iters { - if rx.recv().await.is_ok() { - receiver_count.fetch_add(1, Ordering::Relaxed); - } - } - }); - - let start = Instant::now(); - - for i in 0..iters { - bus.emit(i); - } - - wait_until_received(&received, iters).await; - - let elapsed = start.elapsed(); - receiver.abort(); - elapsed - }); - }); - - group.bench_function("small_struct", |b| { - b.to_async(&rt).iter_custom(|iters| async move { - let bus = EventBus::with_capacity(iters as usize + 1024); - let mut rx = bus.subscribe::(); - - let received = Arc::new(AtomicU64::new(0)); - let receiver_count = Arc::clone(&received); - - let receiver = tokio::spawn(async move { - while receiver_count.load(Ordering::Relaxed) < iters { - if rx.recv().await.is_ok() { - receiver_count.fetch_add(1, Ordering::Relaxed); - } - } - }); - - let start = Instant::now(); - - for i in 0..iters { - bus.emit(SmallEvent { value: i }); - } - - wait_until_received(&received, iters).await; - - let elapsed = start.elapsed(); - receiver.abort(); - elapsed - }); - }); - - group.bench_function("string_struct", |b| { - b.to_async(&rt).iter_custom(|iters| async move { - let bus = EventBus::with_capacity(iters as usize + 1024); - let mut rx = bus.subscribe::(); - - let received = Arc::new(AtomicU64::new(0)); - let receiver_count = Arc::clone(&received); - - let receiver = tokio::spawn(async move { - while receiver_count.load(Ordering::Relaxed) < iters { - if rx.recv().await.is_ok() { - receiver_count.fetch_add(1, Ordering::Relaxed); - } - } - }); - - let start = Instant::now(); - - for i in 0..iters { - bus.emit(StringEvent { - id: i, - name: "Alice".to_string(), - message: "hello from benchmark".to_string(), - }); - } - - wait_until_received(&received, iters).await; - - let elapsed = start.elapsed(); - receiver.abort(); - elapsed - }); - }); - - group.bench_function("vec_payload_1kb", |b| { - b.to_async(&rt).iter_custom(|iters| async move { - let bus = EventBus::with_capacity(iters as usize + 1024); - let mut rx = bus.subscribe::(); - - let received = Arc::new(AtomicU64::new(0)); - let receiver_count = Arc::clone(&received); - - let receiver = tokio::spawn(async move { - while receiver_count.load(Ordering::Relaxed) < iters { - if rx.recv().await.is_ok() { - receiver_count.fetch_add(1, Ordering::Relaxed); - } - } - }); - - let start = Instant::now(); - - for i in 0..iters { - bus.emit(VecEvent { - id: i, - payload: vec![7_u8; 1024], - }); - } - - wait_until_received(&received, iters).await; - - let elapsed = start.elapsed(); - receiver.abort(); - elapsed - }); - }); - - let shared_payload: Arc<[u8]> = Arc::from(vec![7_u8; 1024].into_boxed_slice()); - - group.bench_function("arc_payload_1kb", |b| { - let shared = Arc::clone(&shared_payload); - b.to_async(&rt).iter_custom(|iters| { - let payload = Arc::clone(&shared); - async move { - let bus = EventBus::with_capacity(iters as usize + 1024); - let mut rx = bus.subscribe::(); - - let received = Arc::new(AtomicU64::new(0)); - let receiver_count = Arc::clone(&received); - - let receiver = tokio::spawn(async move { - while receiver_count.load(Ordering::Relaxed) < iters { - if rx.recv().await.is_ok() { - receiver_count.fetch_add(1, Ordering::Relaxed); - } - } - }); - - let start = Instant::now(); - - for i in 0..iters { - bus.emit(ArcPayloadEvent { - id: i, - payload: Arc::clone(&payload), - }); - } - - wait_until_received(&received, iters).await; - - let elapsed = start.elapsed(); - receiver.abort(); - elapsed - } - }); - }); - - group.finish(); -} - -fn bench_typed_callback(c: &mut Criterion) { - let rt = runtime(); - - let mut group = c.benchmark_group("event_bus_typed/typed_callback"); - group.throughput(Throughput::Elements(1)); - - group.bench_function("u64", |b| { - b.to_async(&rt).iter_custom(|iters| async move { - let bus = EventBus::with_capacity(iters as usize + 1024); - - let received = Arc::new(AtomicU64::new(0)); - let handler_count = Arc::clone(&received); - - let subscription = bus.on::(move |event| { - let _ = event; - handler_count.fetch_add(1, Ordering::Relaxed); - }); - - let start = Instant::now(); - - for i in 0..iters { - bus.emit(i); - } - - wait_until_received(&received, iters).await; - - let elapsed = start.elapsed(); - subscription.abort(); - elapsed - }); - }); - - group.bench_function("small_struct", |b| { - b.to_async(&rt).iter_custom(|iters| async move { - let bus = EventBus::with_capacity(iters as usize + 1024); - - let received = Arc::new(AtomicU64::new(0)); - let handler_count = Arc::clone(&received); - - let subscription = bus.on::(move |event| { - let _ = event.value; - handler_count.fetch_add(1, Ordering::Relaxed); - }); - - let start = Instant::now(); - - for i in 0..iters { - bus.emit(SmallEvent { value: i }); - } - - wait_until_received(&received, iters).await; - - let elapsed = start.elapsed(); - subscription.abort(); - elapsed - }); - }); - - group.bench_function("string_struct", |b| { - b.to_async(&rt).iter_custom(|iters| async move { - let bus = EventBus::with_capacity(iters as usize + 1024); - - let received = Arc::new(AtomicU64::new(0)); - let handler_count = Arc::clone(&received); - - let subscription = bus.on::(move |event| { - let _ = event.id; - let _ = event.name.len(); - let _ = event.message.len(); - handler_count.fetch_add(1, Ordering::Relaxed); - }); - - let start = Instant::now(); - - for i in 0..iters { - bus.emit(StringEvent { - id: i, - name: "Alice".to_string(), - message: "hello from benchmark".to_string(), - }); - } - - wait_until_received(&received, iters).await; - - let elapsed = start.elapsed(); - subscription.abort(); - elapsed - }); - }); - - group.bench_function("vec_payload_1kb", |b| { - b.to_async(&rt).iter_custom(|iters| async move { - let bus = EventBus::with_capacity(iters as usize + 1024); - - let received = Arc::new(AtomicU64::new(0)); - let handler_count = Arc::clone(&received); - - let subscription = bus.on::(move |event| { - let _ = event.id; - let _ = event.payload.len(); - handler_count.fetch_add(1, Ordering::Relaxed); - }); - - let start = Instant::now(); - - for i in 0..iters { - bus.emit(VecEvent { - id: i, - payload: vec![7_u8; 1024], - }); - } - - wait_until_received(&received, iters).await; - - let elapsed = start.elapsed(); - subscription.abort(); - elapsed - }); - }); - - let shared_payload: Arc<[u8]> = Arc::from(vec![7_u8; 1024].into_boxed_slice()); - - group.bench_function("arc_payload_1kb", |b| { - let shared = Arc::clone(&shared_payload); - b.to_async(&rt).iter_custom(|iters| { - let payload = Arc::clone(&shared); - async move { - let bus = EventBus::with_capacity(iters as usize + 1024); - - let received = Arc::new(AtomicU64::new(0)); - let handler_count = Arc::clone(&received); - - let subscription = bus.on::(move |event| { - let _ = event.id; - let _ = event.payload.len(); - handler_count.fetch_add(1, Ordering::Relaxed); - }); - - let start = Instant::now(); - - for i in 0..iters { - bus.emit(ArcPayloadEvent { - id: i, - payload: Arc::clone(&payload), - }); - } - - wait_until_received(&received, iters).await; - - let elapsed = start.elapsed(); - subscription.abort(); - elapsed - } - }); - }); - - group.finish(); -} - -fn bench_multiple_subscribers(c: &mut Criterion) { - let rt = runtime(); - - let mut group = c.benchmark_group("event_bus_typed/multiple_subscribers"); - group.throughput(Throughput::Elements(1)); - - for subscriber_count in [1_u64, 2, 4, 8, 16, 32] { - group.bench_function(format!("{subscriber_count}_subscribers"), |b| { - b.to_async(&rt).iter_custom(|iters| async move { - let bus = EventBus::with_capacity(iters as usize + 1024); - - let expected = iters * subscriber_count; - let received = Arc::new(AtomicU64::new(0)); - let mut subscriptions = Vec::with_capacity(subscriber_count as usize); - - for _ in 0..subscriber_count { - let handler_count = Arc::clone(&received); - - let subscription = bus.on::(move |event| { - let _ = event.value; - handler_count.fetch_add(1, Ordering::Relaxed); - }); - - subscriptions.push(subscription); - } - - let start = Instant::now(); - - for i in 0..iters { - bus.emit(SmallEvent { value: i }); - } - - wait_until_received(&received, expected).await; - - let elapsed = start.elapsed(); - - for subscription in subscriptions { - subscription.abort(); - } - - elapsed - }); - }); - } - - group.finish(); -} - -criterion_group!( - benches, - bench_emit_no_subscriber, - bench_raw_subscriber, - bench_typed_callback, - bench_multiple_subscribers, -); - -criterion_main!(benches); diff --git a/event_bus_typed/src/bus.rs b/event_bus_typed/src/bus.rs deleted file mode 100644 index 2ad23a1..0000000 --- a/event_bus_typed/src/bus.rs +++ /dev/null @@ -1,433 +0,0 @@ -use std::any::{Any, TypeId}; -use std::collections::HashMap; -use std::fmt::Debug; -use std::sync::Arc; - -use parking_lot::RwLock; -use tokio::sync::broadcast; -use tokio::task::JoinHandle; -use tracing::{debug, trace, warn}; - -use crate::handler::{AsyncHandler, AsyncHandlerWith}; - -/// Default buffer capacity for each broadcast channel. -pub const DEFAULT_CAPACITY: usize = 1024; - -/// Marker trait for events dispatched through [`EventBus`]. -/// -/// Any type implementing `Clone + Send + Sync + 'static` automatically -/// implements `Event`. -pub trait Event: Clone + Send + Sync + 'static {} - -impl Event for T {} - -#[derive(Debug)] -struct EventBusInner { - channels: RwLock>>, - capacity: usize, -} - -/// 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_typed::EventBus; -/// -/// #[derive(Clone, Debug)] -/// struct MessageCreatedEvent { -/// content: String, -/// } -/// -/// #[tokio::main] -/// async fn main() { -/// let bus = EventBus::new(); -/// -/// bus.on::(|event| { -/// println!("Created message: {:?}", event); -/// }); -/// -/// bus.emit(MessageCreatedEvent { -/// content: "Hello!".into(), -/// }); -/// } -/// ``` -/// -/// # Async callback example -/// ```rust,no_run -/// use event_bus_typed::EventBus; -/// -/// #[derive(Clone, Debug)] -/// struct MessageUpdatedEvent { -/// id: u64, -/// content: String, -/// } -/// -/// #[tokio::main] -/// async fn main() { -/// let bus = EventBus::new(); -/// -/// bus.on_async::(|event| async move { -/// println!("(async) Updated message: {:?}", event); -/// }); -/// -/// bus.emit(MessageUpdatedEvent { -/// id: 42, -/// content: "World!".into(), -/// }); -/// } -/// ``` -#[derive(Debug, Clone)] -pub struct EventBus { - inner: Arc, -} - -impl Default for EventBus { - fn default() -> Self { - 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(&self) -> broadcast::Sender { - let type_id = TypeId::of::(); - - if let Some(tx) = self - .inner - .channels - .read() - .get(&type_id) - .and_then(|entry| entry.downcast_ref::>()) - { - return tx.clone(); - } - - let mut channels = self.inner.channels.write(); - if let Some(tx) = channels - .get(&type_id) - .and_then(|entry| entry.downcast_ref::>()) - { - return tx.clone(); - } - - let (tx, _) = broadcast::channel::(self.inner.capacity); - debug!( - event_type = std::any::type_name::(), - 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_typed::EventBus; - /// # #[derive(Clone)] struct UserConnected { id: u64 } - /// # let bus = EventBus::new(); - /// bus.emit(UserConnected { id: 1 }); - /// ``` - pub fn emit(&self, event: E) { - trace!( - event_type = std::any::type_name::(), - "Emitting event" - ); - - let type_id = TypeId::of::(); - - if let Some(tx) = self - .inner - .channels - .read() - .get(&type_id) - .and_then(|entry| entry.downcast_ref::>()) - .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(&self, handler: impl Fn(E) + Send + Sync + 'static) -> JoinHandle<()> { - let mut rx = self.subscribe::(); - let type_name = std::any::type_name::(); - - 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_typed::EventBus; - /// # #[derive(Clone)] struct MessageUpdatedEvent { id: u64 } - /// # let bus = EventBus::new(); - /// bus.on_async::(|event| async move { - /// println!("Updated: {:?}", event.id); - /// }); - /// ``` - pub fn on_async(&self, handler: impl AsyncHandler) -> JoinHandle<()> { - let mut rx = self.subscribe::(); - let type_name = std::any::type_name::(); - - 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( - &self, - context: C, - handler: impl AsyncHandlerWith, - ) -> JoinHandle<()> { - let mut rx = self.subscribe::(); - let type_name = std::any::type_name::(); - - 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`] for events of type `E`. - /// - /// Allows writing custom event processing loops without callback wrappers. - /// - /// # Example - /// ```rust,no_run - /// use event_bus_typed::EventBus; - /// - /// #[derive(Clone)] - /// struct MyEvent; - /// - /// #[tokio::main] - /// async fn main() { - /// let bus = EventBus::new(); - /// let mut rx = bus.subscribe::(); - /// bus.emit(MyEvent); - /// if let Ok(event) = rx.recv().await { - /// // direct typed `event` - /// } - /// } - /// ``` - pub fn subscribe(&self) -> broadcast::Receiver { - self.get_or_create_sender::().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_typed::EventBus; - /// # #[derive(Clone)] struct MyEvent { id: u64 } - /// #[tokio::main] - /// async fn main() { - /// let bus = EventBus::new(); - /// let event = bus.wait_next::().await.unwrap(); - /// println!("Received next event: {}", event.id); - /// } - /// ``` - pub async fn wait_next(&self) -> Result { - let mut rx = self.subscribe::(); - 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_typed::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::(|e| e.id == target_id).await.unwrap(); - /// println!("Saved message confirmed: {}", event.id); - /// } - /// ``` - pub async fn wait_for( - &self, - mut predicate: impl FnMut(&E) -> bool, - ) -> Result { - let mut rx = self.subscribe::(); - 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::(), - 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(&self) -> usize { - let type_id = TypeId::of::(); - - self.inner - .channels - .read() - .get(&type_id) - .and_then(|e| e.downcast_ref::>()) - .map(|tx| tx.receiver_count()) - .unwrap_or(0) - } - - /// Returns `true` if there are any active subscribers for event type `E`. - pub fn has_subscribers(&self) -> bool { - self.subscriber_count::() > 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(&self) { - let type_id = TypeId::of::(); - self.inner.channels.write().remove(&type_id); - } -} diff --git a/event_bus_typed/src/lib.rs b/event_bus_typed/src/lib.rs deleted file mode 100644 index 2165cce..0000000 --- a/event_bus_typed/src/lib.rs +++ /dev/null @@ -1,49 +0,0 @@ -//! # event_bus_typed -//! -//! A strongly-typed, high-performance in-memory event bus for Tokio. -//! -//! ## Overview -//! -//! Unlike string/topic-based event buses, `event_bus_typed` 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::(|event| async move { ... })`. -//! - **Targeted wake-up**: Tokio broadcast channels are isolated per event type. -//! -//! ## Example -//! -//! ```rust,no_run -//! use event_bus_typed::EventBus; -//! -//! #[derive(Clone, Debug, PartialEq)] -//! struct MessageCreatedEvent { -//! content: String, -//! } -//! -//! #[tokio::main] -//! async fn main() { -//! let bus = EventBus::new(); -//! -//! // Async subscriber -//! bus.on_async::(|event| async move { -//! println!("Received message: {}", event.content); -//! }); -//! -//! // Emit event -//! bus.emit(MessageCreatedEvent { -//! content: "Hello from typed event bus!".into(), -//! }); -//! } -//! ``` - -mod bus; -mod handler; - -pub use bus::{DEFAULT_CAPACITY, Event, EventBus}; -pub use handler::{AsyncHandler, AsyncHandlerWith}; - -#[cfg(test)] -mod tests; diff --git a/event_bus_typed/src/tests.rs b/event_bus_typed/src/tests.rs deleted file mode 100644 index 89342b4..0000000 --- a/event_bus_typed/src/tests.rs +++ /dev/null @@ -1,364 +0,0 @@ -use std::sync::Arc; -use std::sync::atomic::{AtomicBool, AtomicU32, Ordering}; -use uuid::Uuid; - -use crate::EventBus; - -#[derive(Clone, Debug, PartialEq)] -struct MessageCreatedEvent { - channel_id: Uuid, - content: String, -} - -#[derive(Clone, Debug, PartialEq)] -struct MessageUpdatedEvent { - id: u64, - content: String, -} - -#[derive(Clone, Debug, PartialEq)] -struct MessageDeletedEvent { - id: u64, -} - -#[derive(Clone, Debug, PartialEq)] -struct UdpMetricEvent { - value: f32, -} - -// ── Sync Callbacks ────────────────────────────────────────────────────────── - -#[tokio::test] -async fn test_on_callback_sync() { - let bus = EventBus::new(); - let received = Arc::new(AtomicBool::new(false)); - let flag = Arc::clone(&received); - - bus.on::(move |event| { - if event.content == "Hello" { - flag.store(true, Ordering::SeqCst); - } - }); - - 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)); -} - -// ── Async Callbacks (Exact User Requirement) ──────────────────────────────── - -#[tokio::test] -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); - - // Exact syntax specified by the user: - // event_bus.on_async::(|event| async move { ... }); - bus.on_async::(move |event| { - let rc = Arc::clone(&rc); - async move { - 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(MessageUpdatedEvent { - id: 99, - content: "Inferred".into(), - }); - - tokio::time::sleep(std::time::Duration::from_millis(20)).await; - assert!(flag.load(Ordering::SeqCst)); -} - -// ── Targeted Wake-Up & Type Isolation ─────────────────────────────────────── - -#[tokio::test] -async fn test_on_targeted_wakeup() { - let bus = EventBus::new(); - let metric_called = Arc::new(AtomicBool::new(false)); - let flag = Arc::clone(&metric_called); - - bus.on::(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_multiple_subscribers_same_type() { - let bus = EventBus::new(); - let count = Arc::new(AtomicU32::new(0)); - - for _ in 0..3 { - let c = Arc::clone(&count); - bus.on::(move |_| { - c.fetch_add(1, Ordering::SeqCst); - }); - } - - bus.emit(MessageCreatedEvent { - channel_id: Uuid::new_v4(), - content: "Broadcast".into(), - }); - - tokio::time::sleep(std::time::Duration::from_millis(20)).await; - assert_eq!(count.load(Ordering::SeqCst), 3); -} - -#[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::(move |event| { - if event.content == "Test" { - m_flag.store(true, Ordering::SeqCst); - } - }); - - bus.on::(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::(); - - 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::(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::(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::(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::(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::(), 0); - assert!(!bus.has_subscribers::()); - - let _sub = bus.subscribe::(); - assert_eq!(bus.subscriber_count::(), 1); - assert!(bus.has_subscribers::()); - assert_eq!(bus.channel_count(), 1); - - bus.clear_type::(); - assert_eq!(bus.subscriber_count::(), 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::(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::().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::(|e| e.id == 2) - .await - .unwrap(); - assert_eq!(event.id, 2); - assert_eq!(event.content, "Target"); -} diff --git a/src/domain/events/category.rs b/src/domain/events/category.rs new file mode 100644 index 0000000..56ca3c6 --- /dev/null +++ b/src/domain/events/category.rs @@ -0,0 +1,16 @@ +use crate::models::category; + +#[derive(Debug, Clone)] +pub struct CategoryCreatedEvent { + pub category: category::Model, +} + +#[derive(Debug, Clone)] +pub struct CategoryUpdatedEvent { + pub category: category::Model, +} + +#[derive(Debug, Clone)] +pub struct CategoryDeletedEvent { + pub category: category::Model, +} \ No newline at end of file diff --git a/src/domain/events/channel_permission.rs b/src/domain/events/channel_permission.rs new file mode 100644 index 0000000..caa19de --- /dev/null +++ b/src/domain/events/channel_permission.rs @@ -0,0 +1,27 @@ +use crate::models::{channel_role_permission, channel_user_permission}; +use uuid::Uuid; + +#[derive(Debug, Clone)] +pub struct ChannelUserPermissionCreatedEvent { + pub permission: channel_user_permission::Model, +} + +#[derive(Debug, Clone)] +pub struct ChannelUserPermissionUpdatedEvent { + pub channel_id: Uuid, + pub user_id: Uuid, + pub permission: Option, +} + +#[derive(Debug, Clone)] +pub struct ChannelUserPermissionDeletedEvent { + pub permission: channel_user_permission::Model, +} + +#[derive(Debug, Clone)] +pub struct ChannelRolePermissionUpdatedEvent { + pub channel_id: Uuid, + pub server_id: Uuid, + pub role_id: Uuid, + pub permission: Option, +} \ No newline at end of file diff --git a/src/domain/events/mod.rs b/src/domain/events/mod.rs index 96150c7..cce755d 100644 --- a/src/domain/events/mod.rs +++ b/src/domain/events/mod.rs @@ -1,5 +1,10 @@ +pub mod category; pub mod channel; +pub mod channel_permission; pub mod emoji; pub mod message; +pub mod role; pub mod server; +pub mod server_permission; pub mod server_tree; +pub mod user; diff --git a/src/domain/events/role.rs b/src/domain/events/role.rs new file mode 100644 index 0000000..0ca40c5 --- /dev/null +++ b/src/domain/events/role.rs @@ -0,0 +1,29 @@ +use crate::models::{role, role_user}; +use uuid::Uuid; + +#[derive(Debug, Clone)] +pub struct RoleCreatedEvent { + pub role: role::Model, +} + +#[derive(Debug, Clone)] +pub struct RoleUpdatedEvent { + pub role: role::Model, +} + +#[derive(Debug, Clone)] +pub struct RoleDeletedEvent { + pub role: role::Model, +} + +#[derive(Debug, Clone)] +pub struct RoleUserCreatedEvent { + pub role_user: role_user::Model, + pub server_id: Uuid, +} + +#[derive(Debug, Clone)] +pub struct RoleUserDeletedEvent { + pub role_user: role_user::Model, + pub server_id: Uuid, +} \ No newline at end of file diff --git a/src/domain/events/server.rs b/src/domain/events/server.rs index 65c4931..8d83450 100644 --- a/src/domain/events/server.rs +++ b/src/domain/events/server.rs @@ -1,4 +1,6 @@ use crate::models::server; +use crate::models::server_user; +use uuid::Uuid; #[derive(Debug, Clone)] pub struct ServerCreatedEvent { @@ -13,4 +15,15 @@ pub struct ServerUpdatedEvent { #[derive(Debug, Clone)] pub struct ServerDeletedEvent { pub server: server::Model, + pub audience: Vec, +} + +#[derive(Debug, Clone)] +pub struct ServerUserCreatedEvent { + pub server_user: server_user::Model, +} + +#[derive(Debug, Clone)] +pub struct ServerUserDeletedEvent { + pub server_user: server_user::Model, } diff --git a/src/domain/events/server_permission.rs b/src/domain/events/server_permission.rs new file mode 100644 index 0000000..ed5c31a --- /dev/null +++ b/src/domain/events/server_permission.rs @@ -0,0 +1,16 @@ +use crate::models::{server_role_permission, server_user_permission}; +use uuid::Uuid; + +#[derive(Debug, Clone)] +pub struct ServerUserPermissionUpdatedEvent { + pub server_id: Uuid, + pub user_id: Uuid, + pub permission: Option, +} + +#[derive(Debug, Clone)] +pub struct ServerRolePermissionUpdatedEvent { + pub server_id: Uuid, + pub role_id: Uuid, + pub permission: Option, +} \ No newline at end of file diff --git a/src/domain/events/user.rs b/src/domain/events/user.rs new file mode 100644 index 0000000..5fb3713 --- /dev/null +++ b/src/domain/events/user.rs @@ -0,0 +1,21 @@ +use crate::models::user; + +#[derive(Debug, Clone)] +pub struct UserCreatedEvent { + pub user: user::Model, +} + +#[derive(Debug, Clone)] +pub struct UserUpdatedEvent { + pub user: user::Model, +} + +#[derive(Debug, Clone)] +pub struct UserChangedEvent { + pub user: user::Model, +} + +#[derive(Debug, Clone)] +pub struct UserDeletedEvent { + pub user: user::Model, +} \ No newline at end of file diff --git a/src/routes/conversation/handlers.rs b/src/routes/conversation/handlers.rs index ab71dcd..bf18c75 100644 --- a/src/routes/conversation/handlers.rs +++ b/src/routes/conversation/handlers.rs @@ -108,12 +108,9 @@ async fn create_channel(state: &AppState, ids: &[Uuid]) -> Result, event_bus: Arc) { let router = Arc::clone(self); - event_bus.on_async::("message_created", move |event| { + event_bus.on_async::(move |event| { let router = Arc::clone(&router); async move { let attachments = router @@ -142,7 +146,7 @@ impl RealtimeRouter { }); let router = Arc::clone(self); - event_bus.on_async::("message_updated", move |event| { + event_bus.on_async::(move |event| { let router = Arc::clone(&router); async move { let reactions = router @@ -171,7 +175,7 @@ impl RealtimeRouter { }); let router = Arc::clone(self); - event_bus.on_async::("message_deleted", move |event| { + event_bus.on_async::(move |event| { let router = Arc::clone(&router); async move { let users = router @@ -185,47 +189,41 @@ impl RealtimeRouter { }); let router = Arc::clone(self); - event_bus.on_async::( - "message_reaction_added", - move |event| { - let router = Arc::clone(&router); - async move { - let users = router - .services - .realtime_registry - .users_for_channel(event.channel_id); - router.gateway.send_to_users( - users, - "Reaction", - "add", - reaction_model_to_response(event.reaction), - ); - } - }, - ); + event_bus.on_async::(move |event| { + let router = Arc::clone(&router); + async move { + let users = router + .services + .realtime_registry + .users_for_channel(event.channel_id); + router.gateway.send_to_users( + users, + "Reaction", + "add", + reaction_model_to_response(event.reaction), + ); + } + }); let router = Arc::clone(self); - event_bus.on_async::( - "message_reaction_removed", - move |event| { - let router = Arc::clone(&router); - async move { - let users = router - .services - .realtime_registry - .users_for_channel(event.channel_id); - router.gateway.send_to_users( - users, - "Reaction", - "remove", - reaction_model_to_response(event.reaction), - ); - } - }, - ); + event_bus.on_async::(move |event| { + let router = Arc::clone(&router); + async move { + let users = router + .services + .realtime_registry + .users_for_channel(event.channel_id); + router.gateway.send_to_users( + users, + "Reaction", + "remove", + reaction_model_to_response(event.reaction), + ); + } + }); let router = Arc::clone(self); - event_bus.on_async::("channel_created", move |event| { + event_bus.on_async::(move |event| { let router = Arc::clone(&router); async move { if let Err(error) = router @@ -251,7 +249,7 @@ impl RealtimeRouter { }); let router = Arc::clone(self); - event_bus.on_async::("channel_updated", move |event| { + event_bus.on_async::(move |event| { let router = Arc::clone(&router); async move { let mut users = router @@ -274,7 +272,7 @@ impl RealtimeRouter { }); let router = Arc::clone(self); - event_bus.on_async::("channel_deleted", move |event| { + event_bus.on_async::(move |event| { let router = Arc::clone(&router); async move { let users = router @@ -292,9 +290,10 @@ impl RealtimeRouter { }); let router = Arc::clone(self); - event_bus.on_async::("category_created", move |category| { + event_bus.on_async::(move |event| { let router = Arc::clone(&router); async move { + let category = event.category; let users = router.server_users(category.server_id).await; router.gateway.send_to_users( users, @@ -306,9 +305,10 @@ impl RealtimeRouter { }); let router = Arc::clone(self); - event_bus.on_async::("category_updated", move |category| { + event_bus.on_async::(move |event| { let router = Arc::clone(&router); async move { + let category = event.category; let users = router.server_users(category.server_id).await; router.gateway.send_to_users( users, @@ -320,9 +320,10 @@ impl RealtimeRouter { }); let router = Arc::clone(self); - event_bus.on_async::("category_deleted", move |category| { + event_bus.on_async::(move |event| { let router = Arc::clone(&router); async move { + let category = event.category; let users = router.server_users(category.server_id).await; router .gateway @@ -331,9 +332,10 @@ impl RealtimeRouter { }); let router = Arc::clone(self); - event_bus.on_async::("server_created", move |server| { + event_bus.on_async::(move |event| { let router = Arc::clone(&router); async move { + let server = event.server; let users = router.server_users(server.id).await; router.gateway.send_to_users( users, @@ -345,9 +347,10 @@ impl RealtimeRouter { }); let router = Arc::clone(self); - event_bus.on_async::("server_updated", move |server| { + event_bus.on_async::(move |event| { let router = Arc::clone(&router); async move { + let server = event.server; let users = router.server_users(server.id).await; router.gateway.send_to_users( users, @@ -359,37 +362,31 @@ impl RealtimeRouter { }); let router = Arc::clone(self); - event_bus.on_async::<(server::Model, Vec), _, _>( - "server_deleted", - move |(server, users)| { - let router = Arc::clone(&router); - async move { - router - .gateway - .send_to_users(users, "Server", "remove", server.id); - } - }, - ); + event_bus.on_async::(move |event| { + let router = Arc::clone(&router); + async move { + router + .gateway + .send_to_users(event.audience, "Server", "remove", event.server.id); + } + }); let router = Arc::clone(self); - event_bus.on_async::( - "server_tree_invalidated", - move |event| { - let router = Arc::clone(&router); - async move { - let users = match event.user_ids { - Some(users) => users, - None => router.server_users(event.server_id).await, - }; - router - .gateway - .send_to_users(users, "ServerTree", "refresh", event.server_id); - } - }, - ); + event_bus.on_async::(move |event| { + let router = Arc::clone(&router); + async move { + let users = match event.user_ids { + Some(users) => users, + None => router.server_users(event.server_id).await, + }; + router + .gateway + .send_to_users(users, "ServerTree", "refresh", event.server_id); + } + }); let router = Arc::clone(self); - event_bus.on_async::("emoji_created", move |event| { + event_bus.on_async::(move |event| { let router = Arc::clone(&router); async move { let users = router.emoji_users(event.emoji.server_id).await; @@ -403,7 +400,7 @@ impl RealtimeRouter { }); let router = Arc::clone(self); - event_bus.on_async::("emoji_updated", move |event| { + event_bus.on_async::(move |event| { let router = Arc::clone(&router); async move { let mut users = router.emoji_users(event.emoji.server_id).await; @@ -418,7 +415,7 @@ impl RealtimeRouter { }); let router = Arc::clone(self); - event_bus.on_async::("emoji_deleted", move |event| { + event_bus.on_async::(move |event| { let router = Arc::clone(&router); async move { let users = router.emoji_users(event.emoji.server_id).await; diff --git a/src/routes/server/handlers.rs b/src/routes/server/handlers.rs index 831287e..b3e980f 100644 --- a/src/routes/server/handlers.rs +++ b/src/routes/server/handlers.rs @@ -1,8 +1,8 @@ use crate::core::state::AppState; use crate::domain::dto::server::{ CreateServerRequest, JoinServerRequest, ServerResponse, ServerRolePermissionResponse, - ServerTreeResponse, - ServerUserPermissionResponse, SetServerPermissionRequest, UpdateServerRequest, + ServerTreeResponse, ServerUserPermissionResponse, SetServerPermissionRequest, + UpdateServerRequest, }; use crate::http::context::{CurrentUser, Superuser}; use crate::http::error::HTTPError; @@ -336,10 +336,6 @@ pub async fn set_user_permission( .server .set_user_permission(server_id, user_id, payload.permissions) .await?; - state - .event_bus - .emit("server_user_permission_updated", (server_id, user_id)); - let permission = state .repositories .server @@ -347,6 +343,13 @@ pub async fn set_user_permission( .await? .ok_or(HTTPError::NotFound)?; + state.event_bus.emit( + crate::domain::events::server_permission::ServerUserPermissionUpdatedEvent { + server_id, + user_id, + permission: Some(permission.clone()), + }, + ); Ok(Json(mapper::server_user_permission_to_response(permission))) } @@ -386,9 +389,13 @@ pub async fn remove_user_permission( .server .remove_user_permission(server_id, user_id) .await?; - state - .event_bus - .emit("server_user_permission_updated", (server_id, user_id)); + state.event_bus.emit( + crate::domain::events::server_permission::ServerUserPermissionUpdatedEvent { + server_id, + user_id, + permission: None, + }, + ); Ok(StatusCode::NO_CONTENT) } @@ -449,10 +456,6 @@ pub async fn set_role_permission( .server .set_role_permission(server_id, role_id, payload.permissions) .await?; - state - .event_bus - .emit("server_role_permission_updated", (role_id, server_id)); - let permission = state .repositories .server @@ -460,6 +463,13 @@ pub async fn set_role_permission( .await? .ok_or(HTTPError::NotFound)?; + state.event_bus.emit( + crate::domain::events::server_permission::ServerRolePermissionUpdatedEvent { + server_id, + role_id, + permission: Some(permission.clone()), + }, + ); Ok(Json(mapper::server_role_permission_to_response(permission))) } @@ -499,9 +509,13 @@ pub async fn remove_role_permission( .server .remove_role_permission(server_id, role_id) .await?; - state - .event_bus - .emit("server_role_permission_updated", (role_id, server_id)); + state.event_bus.emit( + crate::domain::events::server_permission::ServerRolePermissionUpdatedEvent { + server_id, + role_id, + permission: None, + }, + ); Ok(StatusCode::NO_CONTENT) } diff --git a/src/routes/server_item_order/handlers.rs b/src/routes/server_item_order/handlers.rs index 3392fa2..47849ec 100644 --- a/src/routes/server_item_order/handlers.rs +++ b/src/routes/server_item_order/handlers.rs @@ -28,13 +28,10 @@ pub async fn reorder( .ok_or(HTTPError::NotFound)?; state.services.server_order.reorder(payload).await?; - state.event_bus.emit( - "server_tree_invalidated", - ServerTreeInvalidatedEvent { - server_id, - user_ids: None, - }, - ); + state.event_bus.emit(ServerTreeInvalidatedEvent { + server_id, + user_ids: None, + }); Ok(StatusCode::NO_CONTENT) } diff --git a/src/services/category.rs b/src/services/category.rs index d2f081b..ffe2b3f 100644 --- a/src/services/category.rs +++ b/src/services/category.rs @@ -1,3 +1,6 @@ +use crate::domain::events::category::{ + CategoryCreatedEvent, CategoryDeletedEvent, CategoryUpdatedEvent, +}; use crate::models::category; use crate::models::server_item_order::OrderedResourceType; use crate::services::ServicesContext; @@ -49,7 +52,9 @@ impl CategoryService { }) .await?; - event_bus.emit("category_created", cat.clone()); + event_bus.emit(CategoryCreatedEvent { + category: cat.clone(), + }); Ok(cat) } @@ -76,7 +81,9 @@ impl CategoryService { txn.commit().await?; - event_bus.emit("category_updated", cat.clone()); + event_bus.emit(CategoryUpdatedEvent { + category: cat.clone(), + }); Ok(cat) } @@ -107,7 +114,7 @@ impl CategoryService { txn.commit().await?; if deleted { - event_bus.emit("category_deleted", existing); + event_bus.emit(CategoryDeletedEvent { category: existing }); } Ok(deleted) diff --git a/src/services/channel.rs b/src/services/channel.rs index b4d1cca..f60035d 100644 --- a/src/services/channel.rs +++ b/src/services/channel.rs @@ -2,6 +2,10 @@ use crate::domain::dto::channel::{CreateChannelRequest, UpdateChannelRequest}; use crate::domain::events::channel::{ ChannelCreatedEvent, ChannelDeletedEvent, ChannelUpdatedEvent, }; +use crate::domain::events::channel_permission::{ + ChannelRolePermissionUpdatedEvent, ChannelUserPermissionCreatedEvent, + ChannelUserPermissionDeletedEvent, +}; use crate::models::server_item_order::OrderedResourceType; use crate::models::{channel, role}; use crate::permissions::PermissionSet; @@ -91,12 +95,9 @@ impl ChannelService { .await?; // Post-commit event emission - event_bus.emit( - "channel_created", - ChannelCreatedEvent { - channel: channel.clone(), - }, - ); + event_bus.emit(ChannelCreatedEvent { + channel: channel.clone(), + }); Ok(channel) } @@ -141,13 +142,10 @@ impl ChannelService { txn.commit().await?; - event_bus.emit( - "channel_updated", - ChannelUpdatedEvent { - previous, - channel: channel.clone(), - }, - ); + event_bus.emit(ChannelUpdatedEvent { + previous, + channel: channel.clone(), + }); Ok(channel) } @@ -178,7 +176,7 @@ impl ChannelService { txn.commit().await?; if deleted { - event_bus.emit("channel_deleted", ChannelDeletedEvent { channel: existing }); + event_bus.emit(ChannelDeletedEvent { channel: existing }); } Ok(deleted) @@ -214,12 +212,15 @@ impl ChannelService { .exec(&txn) .await?; + let permission = crate::models::channel_user_permission::Entity::find() + .filter(crate::models::channel_user_permission::Column::ChannelId.eq(channel_id)) + .filter(crate::models::channel_user_permission::Column::UserId.eq(user_id)) + .one(&txn) + .await? + .ok_or_else(|| anyhow::anyhow!("Permission not found"))?; txn.commit().await?; - event_bus.emit( - "channel_user_permission_created", - (channel_id, user_id, permissions), - ); + event_bus.emit(ChannelUserPermissionCreatedEvent { permission }); Ok(()) } @@ -234,6 +235,11 @@ impl ChannelService { let txn = db.begin().await?; + let existing = crate::models::channel_user_permission::Entity::find() + .filter(crate::models::channel_user_permission::Column::ChannelId.eq(channel_id)) + .filter(crate::models::channel_user_permission::Column::UserId.eq(user_id)) + .one(&txn) + .await?; crate::models::channel_user_permission::Entity::delete_many() .filter(crate::models::channel_user_permission::Column::ChannelId.eq(channel_id)) .filter(crate::models::channel_user_permission::Column::UserId.eq(user_id)) @@ -242,7 +248,9 @@ impl ChannelService { txn.commit().await?; - event_bus.emit("channel_user_permission_deleted", (channel_id, user_id)); + if let Some(permission) = existing { + event_bus.emit(ChannelUserPermissionDeletedEvent { permission }); + } Ok(()) } @@ -285,9 +293,19 @@ impl ChannelService { .exec(&txn) .await?; + let permission = crate::models::channel_role_permission::Entity::find() + .filter(crate::models::channel_role_permission::Column::ChannelId.eq(channel_id)) + .filter(crate::models::channel_role_permission::Column::RoleId.eq(role_id)) + .one(&txn) + .await?; txn.commit().await?; - event_bus.emit("channel_role_permission_updated", (role_id, server_id)); + event_bus.emit(ChannelRolePermissionUpdatedEvent { + channel_id, + server_id, + role_id, + permission, + }); Ok(()) } @@ -318,7 +336,12 @@ impl ChannelService { txn.commit().await?; - event_bus.emit("channel_role_permission_updated", (role_id, server_id)); + event_bus.emit(ChannelRolePermissionUpdatedEvent { + channel_id, + server_id, + role_id, + permission: None, + }); Ok(()) } diff --git a/src/services/message.rs b/src/services/message.rs index d7c5caf..2489529 100644 --- a/src/services/message.rs +++ b/src/services/message.rs @@ -3,7 +3,6 @@ use crate::domain::events::message::{ }; use crate::models::{attachment, channel, message}; use crate::services::ServicesContext; -use event_bus::Scope; use sea_orm::{ ActiveModelTrait, ColumnTrait, EntityTrait, QueryFilter, QuerySelect, Set, TransactionTrait, }; @@ -41,9 +40,6 @@ impl MessageService { txn.commit().await?; - let mut scopes: Vec = Vec::new(); - scopes.push(Scope::uuid("channel", msg.channel_id)); - let server_id: Option = channel::Entity::find_by_id(msg.channel_id) .select_only() .column(channel::Column::ServerId) @@ -52,19 +48,11 @@ impl MessageService { .await? .flatten(); - if let Some(server_id) = server_id { - scopes.push(Scope::uuid("server", server_id)); - } - - event_bus.emit_scoped( - "message_created", - scopes, - MessageCreatedEvent { - server_id, - channel_id: msg.channel_id, - message: msg.clone(), - }, - ); + event_bus.emit(MessageCreatedEvent { + server_id, + channel_id: msg.channel_id, + message: msg.clone(), + }); Ok(msg) } @@ -119,19 +107,11 @@ impl MessageService { .one(db) .await? .flatten(); - let mut scopes = vec![Scope::uuid("channel", msg.channel_id)]; - if let Some(server_id) = server_id { - scopes.push(Scope::uuid("server", server_id)); - } - event_bus.emit_scoped( - "message_created", - scopes, - MessageCreatedEvent { - server_id, - channel_id: msg.channel_id, - message: msg.clone(), - }, - ); + event_bus.emit(MessageCreatedEvent { + server_id, + channel_id: msg.channel_id, + message: msg.clone(), + }); Ok(msg) } @@ -165,19 +145,11 @@ impl MessageService { .await? .flatten(); - let mut scopes = vec![Scope::uuid("channel", msg.channel_id)]; - if let Some(server_id) = server_id { - scopes.push(Scope::uuid("server", server_id)); - } - event_bus.emit_scoped( - "message_updated", - scopes, - MessageUpdatedEvent { - server_id, - channel_id: msg.channel_id, - message: msg.clone(), - }, - ); + event_bus.emit(MessageUpdatedEvent { + server_id, + channel_id: msg.channel_id, + message: msg.clone(), + }); Ok(msg) } @@ -208,19 +180,11 @@ impl MessageService { txn.commit().await?; if deleted { - let mut scopes = vec![Scope::uuid("channel", existing.channel_id)]; - if let Some(server_id) = server_id { - scopes.push(Scope::uuid("server", server_id)); - } - event_bus.emit_scoped( - "message_deleted", - scopes, - MessageDeletedEvent { - server_id, - channel_id: existing.channel_id, - message: existing, - }, - ); + event_bus.emit(MessageDeletedEvent { + server_id, + channel_id: existing.channel_id, + message: existing, + }); } Ok(deleted) diff --git a/src/services/message_reaction.rs b/src/services/message_reaction.rs index 2d64f43..ed98609 100644 --- a/src/services/message_reaction.rs +++ b/src/services/message_reaction.rs @@ -3,7 +3,6 @@ use crate::domain::events::message::{MessageReactionAddedEvent, MessageReactionR use crate::http::error::HTTPError; use crate::models::{emoji, message_reaction}; use crate::services::ServicesContext; -use event_bus::Scope; use sea_orm::{ColumnTrait, EntityTrait, QueryFilter, Set}; use std::collections::HashMap; use std::sync::Arc; @@ -117,19 +116,11 @@ impl MessageReactionService { }) .await?; - let mut scopes = vec![Scope::uuid("channel", message.channel_id)]; - if let Some(server_id) = server_id { - scopes.push(Scope::uuid("server", server_id)); - } - self.context.event_bus.emit_scoped( - "message_reaction_added", - scopes, - MessageReactionAddedEvent { - server_id, - channel_id: message.channel_id, - reaction: reaction.clone(), - }, - ); + self.context.event_bus.emit(MessageReactionAddedEvent { + server_id, + channel_id: message.channel_id, + reaction: reaction.clone(), + }); Ok((reaction, true)) } @@ -151,19 +142,11 @@ impl MessageReactionService { .await? .ok_or(HTTPError::NotFound)?; - let mut scopes = vec![Scope::uuid("channel", message.channel_id)]; - if let Some(server_id) = server_id { - scopes.push(Scope::uuid("server", server_id)); - } - self.context.event_bus.emit_scoped( - "message_reaction_removed", - scopes, - MessageReactionRemovedEvent { - server_id, - channel_id: message.channel_id, - reaction: reaction.clone(), - }, - ); + self.context.event_bus.emit(MessageReactionRemovedEvent { + server_id, + channel_id: message.channel_id, + reaction: reaction.clone(), + }); Ok(reaction) } diff --git a/src/services/permission_sync.rs b/src/services/permission_sync.rs index dca3e13..989a9cf 100644 --- a/src/services/permission_sync.rs +++ b/src/services/permission_sync.rs @@ -1,6 +1,16 @@ use crate::domain::events::channel::{ChannelCreatedEvent, ChannelDeletedEvent}; +use crate::domain::events::channel_permission::{ + ChannelRolePermissionUpdatedEvent, ChannelUserPermissionCreatedEvent, + ChannelUserPermissionDeletedEvent, ChannelUserPermissionUpdatedEvent, +}; +use crate::domain::events::role::{RoleUserCreatedEvent, RoleUserDeletedEvent}; +use crate::domain::events::server::{ + ServerCreatedEvent, ServerUserCreatedEvent, ServerUserDeletedEvent, +}; +use crate::domain::events::server_permission::{ + ServerRolePermissionUpdatedEvent, ServerUserPermissionUpdatedEvent, +}; use crate::domain::events::server_tree::ServerTreeInvalidatedEvent; -use crate::models::server; use crate::repositories::Repositories; use crate::services::ServicesContext; use event_bus::EventBus; @@ -60,13 +70,10 @@ pub struct PermissionSyncService { impl PermissionSyncService { fn invalidate_tree(event_bus: &Arc, server_id: Uuid, user_ids: Option>) { - event_bus.emit( - "server_tree_invalidated", - ServerTreeInvalidatedEvent { - server_id, - user_ids, - }, - ); + event_bus.emit(ServerTreeInvalidatedEvent { + server_id, + user_ids, + }); } pub fn new(service_context: Arc) -> Self { @@ -83,18 +90,18 @@ impl PermissionSyncService { // Événements Serveur & Membres Serveur // --------------------------------------------------------------------- let notify = event_bus.clone(); - event_bus.on_async_with( - "server_created", + event_bus.on_async_with::( repositories.clone(), - move |repositories, server: server::Model| async move { - Self::sync_server(repositories, server.id).await; + move |repositories, event| async move { + Self::sync_server(repositories, event.server.id).await; }, ); - event_bus.on_async_with( - "server_user_created", + event_bus.on_async_with::( repositories.clone(), - move |repositories, (server_id, user_id): (Uuid, Uuid)| { + move |repositories, event| { + let server_id = event.server_user.server_id; + let user_id = event.server_user.user_id; let notify = notify.clone(); async move { Self::sync_user(repositories, user_id, server_id).await; @@ -104,10 +111,11 @@ impl PermissionSyncService { ); let notify = event_bus.clone(); - event_bus.on_async_with( - "server_user_deleted", + event_bus.on_async_with::( repositories.clone(), - move |repositories, (server_id, user_id): (Uuid, Uuid)| { + move |repositories, event| { + let server_id = event.server_user.server_id; + let user_id = event.server_user.user_id; let notify = notify.clone(); async move { Self::sync_user(repositories, user_id, server_id).await; @@ -121,10 +129,11 @@ impl PermissionSyncService { // --------------------------------------------------------------------- let notify = event_bus.clone(); - event_bus.on_async_with( - "role_user_created", + event_bus.on_async_with::( repositories.clone(), - move |repositories, (_role_id, user_id, server_id): (Uuid, Uuid, Uuid)| { + move |repositories, event| { + let server_id = event.server_id; + let user_id = event.role_user.user_id; let notify = notify.clone(); async move { Self::sync_user(repositories, user_id, server_id).await; @@ -134,10 +143,11 @@ impl PermissionSyncService { ); let notify = event_bus.clone(); - event_bus.on_async_with( - "role_user_deleted", + event_bus.on_async_with::( repositories.clone(), - move |repositories, (_role_id, user_id, server_id): (Uuid, Uuid, Uuid)| { + move |repositories, event| { + let server_id = event.server_id; + let user_id = event.role_user.user_id; let notify = notify.clone(); async move { Self::sync_user(repositories, user_id, server_id).await; @@ -151,10 +161,11 @@ impl PermissionSyncService { // --------------------------------------------------------------------- let notify = event_bus.clone(); - event_bus.on_async_with( - "server_role_permission_updated", + event_bus.on_async_with::( repositories.clone(), - move |repositories, (role_id, server_id): (Uuid, Uuid)| { + move |repositories, event| { + let role_id = event.role_id; + let server_id = event.server_id; let notify = notify.clone(); async move { Self::sync_role_members(repositories, role_id, server_id).await; @@ -164,10 +175,11 @@ impl PermissionSyncService { ); let notify = event_bus.clone(); - event_bus.on_async_with( - "server_user_permission_updated", + event_bus.on_async_with::( repositories.clone(), - move |repositories, (server_id, user_id): (Uuid, Uuid)| { + move |repositories, event| { + let server_id = event.server_id; + let user_id = event.user_id; let notify = notify.clone(); async move { Self::sync_user(repositories, user_id, server_id).await; @@ -181,10 +193,9 @@ impl PermissionSyncService { // --------------------------------------------------------------------- let notify = event_bus.clone(); - event_bus.on_async_with( - "channel_created", + event_bus.on_async_with::( repositories.clone(), - move |repositories, event: ChannelCreatedEvent| { + move |repositories, event| { let notify = notify.clone(); async move { if let Some(server_id) = event.channel.server_id { @@ -196,10 +207,9 @@ impl PermissionSyncService { ); let notify = event_bus.clone(); - event_bus.on_async_with( - "channel_deleted", + event_bus.on_async_with::( repositories.clone(), - move |repositories, event: ChannelDeletedEvent| { + move |repositories, event| { let notify = notify.clone(); async move { if let Some(server_id) = event.channel.server_id { @@ -211,10 +221,11 @@ impl PermissionSyncService { ); let notify = event_bus.clone(); - event_bus.on_async_with( - "channel_role_permission_updated", + event_bus.on_async_with::( repositories.clone(), - move |repositories, (role_id, server_id): (Uuid, Uuid)| { + move |repositories, event| { + let role_id = event.role_id; + let server_id = event.server_id; let notify = notify.clone(); async move { Self::sync_role_members(repositories, role_id, server_id).await; @@ -224,23 +235,34 @@ impl PermissionSyncService { ); let notify = event_bus.clone(); - event_bus.on_async_with( - "channel_user_permission_updated", + event_bus.on_async_with::( repositories.clone(), - move |repositories, (server_id, user_id): (Uuid, Uuid)| { + move |repositories, event| { + let user_id = event.user_id; let notify = notify.clone(); async move { - Self::sync_user(repositories, user_id, server_id).await; - Self::invalidate_tree(¬ify, server_id, Some(vec![user_id])); + if let Some(channel) = repositories + .channel + .get_by_id(event.channel_id) + .await + .ok() + .flatten() + { + if let Some(server_id) = channel.server_id { + Self::sync_user(repositories, user_id, server_id).await; + Self::invalidate_tree(¬ify, server_id, Some(vec![user_id])); + } + } } }, ); let notify = event_bus.clone(); - event_bus.on_async_with( - "channel_user_permission_created", + event_bus.on_async_with::( repositories.clone(), - move |repositories, (channel_id, user_id, _permissions): (Uuid, Uuid, u64)| { + move |repositories, event| { + let channel_id = event.permission.channel_id; + let user_id = event.permission.user_id; let notify = notify.clone(); async move { if let Some(channel) = repositories @@ -260,10 +282,11 @@ impl PermissionSyncService { ); let notify = event_bus.clone(); - event_bus.on_async_with( - "channel_user_permission_deleted", + event_bus.on_async_with::( repositories, - move |repositories, (channel_id, user_id): (Uuid, Uuid)| { + move |repositories, event| { + let channel_id = event.permission.channel_id; + let user_id = event.permission.user_id; let notify = notify.clone(); async move { if let Some(channel) = repositories diff --git a/src/services/realtime_registry.rs b/src/services/realtime_registry.rs index 020c30c..71662d9 100644 --- a/src/services/realtime_registry.rs +++ b/src/services/realtime_registry.rs @@ -1,3 +1,8 @@ +use crate::domain::events::channel_permission::{ + ChannelUserPermissionCreatedEvent, ChannelUserPermissionDeletedEvent, + ChannelUserPermissionUpdatedEvent, +}; +use crate::domain::events::server_permission::ServerUserPermissionUpdatedEvent; use crate::models::{channel, channel_user, computed_permission::PermissionScopeType}; use crate::permissions::ChannelPermission; use crate::repositories::Repositories; @@ -204,10 +209,10 @@ impl RealtimeRegistry { event_bus: Arc, ) { let registry = Arc::clone(self); - event_bus.on_async_with( - "channel_user_permission_updated", + event_bus.on_async_with::( repositories.clone(), - move |repositories, (_channel_id, user_id, _permissions): (Uuid, Uuid, u64)| { + move |repositories, event| { + let user_id = event.user_id; let registry = Arc::clone(®istry); async move { if let Err(error) = registry.refresh_user(&repositories, user_id).await { @@ -218,10 +223,10 @@ impl RealtimeRegistry { ); let registry = Arc::clone(self); - event_bus.on_async_with( - "channel_user_permission_created", + event_bus.on_async_with::( repositories.clone(), - move |repositories, (_channel_id, user_id, _permissions): (Uuid, Uuid, u64)| { + move |repositories, event| { + let user_id = event.permission.user_id; let registry = Arc::clone(®istry); async move { if let Err(error) = registry.refresh_user(&repositories, user_id).await { @@ -232,10 +237,10 @@ impl RealtimeRegistry { ); let registry = Arc::clone(self); - event_bus.on_async_with( - "channel_user_permission_deleted", + event_bus.on_async_with::( repositories.clone(), - move |repositories, (_channel_id, user_id): (Uuid, Uuid)| { + move |repositories, event| { + let user_id = event.permission.user_id; let registry = Arc::clone(®istry); async move { if let Err(error) = registry.refresh_user(&repositories, user_id).await { @@ -247,10 +252,10 @@ impl RealtimeRegistry { let registry = Arc::clone(self); let repositories = repositories.clone(); - event_bus.on_async_with( - "server_user_permission_updated", + event_bus.on_async_with::( repositories, - move |repositories, (_server_id, user_id): (Uuid, Uuid)| { + move |repositories, event| { + let user_id = event.user_id; let registry = Arc::clone(®istry); async move { if let Err(error) = registry.refresh_user(&repositories, user_id).await { diff --git a/src/services/role.rs b/src/services/role.rs index 2b82f12..e070068 100644 --- a/src/services/role.rs +++ b/src/services/role.rs @@ -1,4 +1,8 @@ -use crate::models::role; +use crate::domain::events::role::{ + RoleCreatedEvent, RoleDeletedEvent, RoleUpdatedEvent, RoleUserCreatedEvent, + RoleUserDeletedEvent, +}; +use crate::models::{role, role_user}; use crate::services::ServicesContext; use sea_orm::{ActiveModelTrait, EntityTrait, TransactionTrait}; use std::sync::Arc; @@ -27,7 +31,7 @@ impl RoleService { txn.commit().await?; - event_bus.emit("role_created", role.clone()); + event_bus.emit(RoleCreatedEvent { role: role.clone() }); Ok(role) } @@ -45,7 +49,7 @@ impl RoleService { txn.commit().await?; - event_bus.emit("role_updated", role.clone()); + event_bus.emit(RoleUpdatedEvent { role: role.clone() }); Ok(role) } @@ -56,14 +60,15 @@ impl RoleService { let txn = db.begin().await?; + let existing = role::Entity::find_by_id(id).one(&txn).await?; let res = role::Entity::delete_by_id(id).exec(&txn).await?; let deleted = res.rows_affected > 0; txn.commit().await?; - if deleted { - event_bus.emit("role_deleted", id); + if let Some(role) = existing.filter(|_| deleted) { + event_bus.emit(RoleDeletedEvent { role }); } Ok(deleted) @@ -82,9 +87,14 @@ impl RoleService { .add_member(role_id, user_id) .await?; if added { - self.service_context - .event_bus - .emit("role_user_created", (role_id, user_id, server_id)); + let role_user = role_user::Entity::find_by_id((role_id, user_id)) + .one(&self.service_context.repositories.server.context.db) + .await? + .ok_or_else(|| anyhow::anyhow!("Role membership not found"))?; + self.service_context.event_bus.emit(RoleUserCreatedEvent { + role_user, + server_id, + }); } Ok(added) } @@ -95,16 +105,20 @@ impl RoleService { user_id: Uuid, server_id: Uuid, ) -> Result { + let existing = role_user::Entity::find_by_id((role_id, user_id)) + .one(&self.service_context.repositories.server.context.db) + .await?; let removed = self .service_context .repositories .role .remove_member(role_id, user_id) .await?; - if removed { - self.service_context - .event_bus - .emit("role_user_deleted", (role_id, user_id, server_id)); + if let Some(role_user) = existing.filter(|_| removed) { + self.service_context.event_bus.emit(RoleUserDeletedEvent { + role_user, + server_id, + }); } Ok(removed) } diff --git a/src/services/server.rs b/src/services/server.rs index b219c3f..115d28b 100644 --- a/src/services/server.rs +++ b/src/services/server.rs @@ -1,3 +1,7 @@ +use crate::domain::events::server::{ + ServerCreatedEvent, ServerDeletedEvent, ServerUpdatedEvent, ServerUserCreatedEvent, + ServerUserDeletedEvent, +}; use crate::models::{role, role_user, server, server_user}; use crate::services::ServicesContext; use sea_orm::{ @@ -63,7 +67,9 @@ impl ServerService { txn.commit().await?; - event_bus.emit("server_created", srv.clone()); + event_bus.emit(ServerCreatedEvent { + server: srv.clone(), + }); Ok(srv) } @@ -97,7 +103,7 @@ impl ServerService { return Ok(srv); } - server_user::ActiveModel { + let membership = server_user::ActiveModel { server_id: Set(server_id), user_id: Set(user_id), ..Default::default() @@ -119,7 +125,9 @@ impl ServerService { .await?; txn.commit().await?; - event_bus.emit("server_user_created", (server_id, user_id)); + event_bus.emit(ServerUserCreatedEvent { + server_user: membership, + }); self.service_context .repositories .read_state @@ -152,7 +160,9 @@ impl ServerService { txn.commit().await?; - event_bus.emit("server_updated", srv.clone()); + event_bus.emit(ServerUpdatedEvent { + server: srv.clone(), + }); Ok(srv) } @@ -182,7 +192,10 @@ impl ServerService { txn.commit().await?; if deleted { - event_bus.emit("server_deleted", (existing, audience)); + event_bus.emit(ServerDeletedEvent { + server: existing, + audience, + }); } Ok(deleted) @@ -194,7 +207,7 @@ impl ServerService { let txn = db.begin().await?; - crate::models::server_user::ActiveModel { + let membership = crate::models::server_user::ActiveModel { server_id: Set(server_id), user_id: Set(user_id), ..Default::default() @@ -214,7 +227,9 @@ impl ServerService { txn.commit().await?; - event_bus.emit("server_user_created", (server_id, user_id)); + event_bus.emit(ServerUserCreatedEvent { + server_user: membership, + }); Ok(true) } @@ -225,6 +240,11 @@ impl ServerService { let txn = db.begin().await?; + let membership = server_user::Entity::find() + .filter(server_user::Column::ServerId.eq(server_id)) + .filter(server_user::Column::UserId.eq(user_id)) + .one(&txn) + .await?; let res = crate::models::server_user::Entity::delete_many() .filter(crate::models::server_user::Column::ServerId.eq(server_id)) .filter(crate::models::server_user::Column::UserId.eq(user_id)) @@ -235,8 +255,8 @@ impl ServerService { txn.commit().await?; - if deleted { - event_bus.emit("server_user_deleted", (server_id, user_id)); + if let Some(server_user) = membership.filter(|_| deleted) { + event_bus.emit(ServerUserDeletedEvent { server_user }); } Ok(deleted) diff --git a/src/services/user.rs b/src/services/user.rs index 373a6d5..5901134 100644 --- a/src/services/user.rs +++ b/src/services/user.rs @@ -1,4 +1,7 @@ use crate::auth::password; +use crate::domain::events::user::{ + UserChangedEvent, UserCreatedEvent, UserDeletedEvent, UserUpdatedEvent, +}; use crate::models::{role, user}; use crate::services::ServicesContext; use sea_orm::{ @@ -30,7 +33,7 @@ impl UserService { txn.commit().await?; - event_bus.emit("user_created", usr.clone()); + event_bus.emit(UserCreatedEvent { user: usr.clone() }); Ok(usr) } @@ -48,7 +51,7 @@ impl UserService { txn.commit().await?; - event_bus.emit("user_updated", usr.clone()); + event_bus.emit(UserUpdatedEvent { user: usr.clone() }); Ok(usr) } @@ -79,7 +82,7 @@ impl UserService { txn.commit().await?; - event_bus.emit("user_changed", usr); + event_bus.emit(UserChangedEvent { user: usr }); Ok(()) } @@ -90,14 +93,15 @@ impl UserService { let txn = db.begin().await?; + let existing = user::Entity::find_by_id(id).one(&txn).await?; let res = user::Entity::delete_by_id(id).exec(&txn).await?; let deleted = res.rows_affected > 0; txn.commit().await?; - if deleted { - event_bus.emit("user_deleted", id); + if let Some(user) = existing.filter(|_| deleted) { + event_bus.emit(UserDeletedEvent { user }); } Ok(deleted)