add event_bus_typed

This commit is contained in:
2026-09-23 10:36:14 +02:00
parent 3780092fa6
commit ab97dcc8d9
35 changed files with 1376 additions and 3228 deletions
+1 -1
View File
@@ -16,8 +16,8 @@ harness = false
tokio = { version = "1.53.1", default-features = false, features = ["rt", "sync"] } tokio = { version = "1.53.1", default-features = false, features = ["rt", "sync"] }
parking_lot = "0.12.5" parking_lot = "0.12.5"
tracing = "0.1" tracing = "0.1"
uuid = { version = "1.26.1", features = ["v4"] }
[dev-dependencies] [dev-dependencies]
tokio = { version = "1.53.1", default-features = false, features = ["rt", "rt-multi-thread", "macros", "time", "sync"] } tokio = { version = "1.53.1", default-features = false, features = ["rt", "rt-multi-thread", "macros", "time", "sync"] }
criterion = { version = "0.8.2", features = ["async_tokio"] } criterion = { version = "0.8.2", features = ["async_tokio"] }
uuid = { version = "1.26.1", features = ["v4"] }
@@ -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`). 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)`). - 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::<Event, _, _>("topic", ...)`), puis d'effectuer un déréférencement / downcast dynamique (`downcast_ref::<T>()` ou macro `match_event!`) sur chaque message reçu. - La réception nécessitait de spécifier à la fois le type et le topic string (`bus.on_async::<Event, _, _>("topic", ...)`), puis d'effectuer un déréférencement / downcast dynamique (`downcast_ref::<T>()` 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. - **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. - **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::<MessageUpdatedEvent>(|event| async move { ... })` ainsi que de l'inférence automatique `bus.on_async(|event: MessageUpdatedEvent| async move { ... })`. - **Syntaxe ergonomique** : support complet de la syntaxe turbofish demandée `bus.on_async::<MessageUpdatedEvent>(|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 ### 2. Émission d'événements
```rust ```rust
use event_bus_typed::EventBus; use event_bus::EventBus;
let bus = EventBus::new(); let bus = EventBus::new();
@@ -139,10 +139,10 @@ tokio::spawn(async move {
Exécuter les tests du crate : Exécuter les tests du crate :
```bash ```bash
cargo test --manifest-path event_bus_typed/Cargo.toml cargo test --manifest-path event_bus/Cargo.toml
``` ```
Exécuter les benchmarks Criterion : Exécuter les benchmarks Criterion :
```bash ```bash
cargo bench --manifest-path event_bus_typed/Cargo.toml cargo bench --manifest-path event_bus/Cargo.toml
``` ```
+68 -111
View File
@@ -1,3 +1,5 @@
#![allow(dead_code)]
use std::sync::{ use std::sync::{
Arc, Arc,
atomic::{AtomicU64, Ordering}, atomic::{AtomicU64, Ordering},
@@ -8,8 +10,6 @@ use criterion::{Criterion, Throughput, criterion_group, criterion_main};
use event_bus::EventBus; use event_bus::EventBus;
use tokio::runtime::Runtime; use tokio::runtime::Runtime;
const TOPIC: &str = "bench-topic";
#[derive(Clone)] #[derive(Clone)]
struct SmallEvent { struct SmallEvent {
value: u64, value: u64,
@@ -38,57 +38,46 @@ fn runtime() -> Runtime {
Runtime::new().expect("failed to create tokio runtime") Runtime::new().expect("failed to create tokio runtime")
} }
fn wait_until_received( async fn wait_until_received(received: &AtomicU64, expected: u64) {
received: &AtomicU64,
expected: u64,
) -> impl std::future::Future<Output = ()> + '_ {
async move {
while received.load(Ordering::Relaxed) < expected { while received.load(Ordering::Relaxed) < expected {
tokio::task::yield_now().await; tokio::task::yield_now().await;
} }
}
} }
fn bench_emit_no_subscriber(c: &mut Criterion) { 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)); group.throughput(Throughput::Elements(1));
let bus = EventBus::with_capacity(1024); let bus = EventBus::with_capacity(1024);
group.bench_function("u64", |b| { group.bench_function("u64", |b| {
b.iter(|| { b.iter(|| {
bus.emit(TOPIC, 42_u64); bus.emit(42_u64);
}); });
}); });
group.bench_function("small_struct", |b| { group.bench_function("small_struct", |b| {
b.iter(|| { b.iter(|| {
bus.emit(TOPIC, SmallEvent { value: 42 }); bus.emit(SmallEvent { value: 42 });
}); });
}); });
group.bench_function("string_struct", |b| { group.bench_function("string_struct", |b| {
b.iter(|| { b.iter(|| {
bus.emit( bus.emit(StringEvent {
TOPIC,
StringEvent {
id: 42, id: 42,
name: "Alice".to_string(), name: "Alice".to_string(),
message: "hello from benchmark".to_string(), message: "hello from benchmark".to_string(),
}, });
);
}); });
}); });
group.bench_function("vec_payload_1kb", |b| { group.bench_function("vec_payload_1kb", |b| {
b.iter(|| { b.iter(|| {
bus.emit( bus.emit(VecEvent {
TOPIC,
VecEvent {
id: 42, id: 42,
payload: vec![7_u8; 1024], payload: vec![7_u8; 1024],
}, });
);
}); });
}); });
@@ -96,13 +85,10 @@ fn bench_emit_no_subscriber(c: &mut Criterion) {
group.bench_function("arc_payload_1kb", |b| { group.bench_function("arc_payload_1kb", |b| {
b.iter(|| { b.iter(|| {
bus.emit( bus.emit(ArcPayloadEvent {
TOPIC,
ArcPayloadEvent {
id: 42, id: 42,
payload: Arc::clone(&shared_payload), payload: Arc::clone(&shared_payload),
}, });
);
}); });
}); });
@@ -112,13 +98,13 @@ fn bench_emit_no_subscriber(c: &mut Criterion) {
fn bench_raw_subscriber(c: &mut Criterion) { fn bench_raw_subscriber(c: &mut Criterion) {
let rt = runtime(); 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.throughput(Throughput::Elements(1));
group.bench_function("u64", |b| { group.bench_function("u64", |b| {
b.to_async(&rt).iter_custom(|iters| async move { 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 mut rx = bus.on_raw(TOPIC); let mut rx = bus.subscribe::<u64>();
let received = Arc::new(AtomicU64::new(0)); let received = Arc::new(AtomicU64::new(0));
let receiver_count = Arc::clone(&received); let receiver_count = Arc::clone(&received);
@@ -134,23 +120,21 @@ fn bench_raw_subscriber(c: &mut Criterion) {
let start = Instant::now(); let start = Instant::now();
for i in 0..iters { for i in 0..iters {
bus.emit(TOPIC, i); bus.emit(i);
} }
wait_until_received(&received, iters).await; wait_until_received(&received, iters).await;
let elapsed = start.elapsed(); let elapsed = start.elapsed();
receiver.abort(); receiver.abort();
elapsed elapsed
}); });
}); });
group.bench_function("small_struct", |b| { group.bench_function("small_struct", |b| {
b.to_async(&rt).iter_custom(|iters| async move { 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 mut rx = bus.on_raw(TOPIC); let mut rx = bus.subscribe::<SmallEvent>();
let received = Arc::new(AtomicU64::new(0)); let received = Arc::new(AtomicU64::new(0));
let receiver_count = Arc::clone(&received); let receiver_count = Arc::clone(&received);
@@ -166,23 +150,21 @@ fn bench_raw_subscriber(c: &mut Criterion) {
let start = Instant::now(); let start = Instant::now();
for i in 0..iters { for i in 0..iters {
bus.emit(TOPIC, SmallEvent { value: i }); bus.emit(SmallEvent { value: i });
} }
wait_until_received(&received, iters).await; wait_until_received(&received, iters).await;
let elapsed = start.elapsed(); let elapsed = start.elapsed();
receiver.abort(); receiver.abort();
elapsed elapsed
}); });
}); });
group.bench_function("string_struct", |b| { group.bench_function("string_struct", |b| {
b.to_async(&rt).iter_custom(|iters| async move { 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 mut rx = bus.on_raw(TOPIC); let mut rx = bus.subscribe::<StringEvent>();
let received = Arc::new(AtomicU64::new(0)); let received = Arc::new(AtomicU64::new(0));
let receiver_count = Arc::clone(&received); let receiver_count = Arc::clone(&received);
@@ -198,30 +180,25 @@ fn bench_raw_subscriber(c: &mut Criterion) {
let start = Instant::now(); let start = Instant::now();
for i in 0..iters { for i in 0..iters {
bus.emit( bus.emit(StringEvent {
TOPIC,
StringEvent {
id: i, id: i,
name: "Alice".to_string(), name: "Alice".to_string(),
message: "hello from benchmark".to_string(), message: "hello from benchmark".to_string(),
}, });
);
} }
wait_until_received(&received, iters).await; wait_until_received(&received, iters).await;
let elapsed = start.elapsed(); let elapsed = start.elapsed();
receiver.abort(); receiver.abort();
elapsed elapsed
}); });
}); });
group.bench_function("vec_payload_1kb", |b| { group.bench_function("vec_payload_1kb", |b| {
b.to_async(&rt).iter_custom(|iters| async move { 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 mut rx = bus.on_raw(TOPIC); let mut rx = bus.subscribe::<VecEvent>();
let received = Arc::new(AtomicU64::new(0)); let received = Arc::new(AtomicU64::new(0));
let receiver_count = Arc::clone(&received); let receiver_count = Arc::clone(&received);
@@ -237,31 +214,29 @@ fn bench_raw_subscriber(c: &mut Criterion) {
let start = Instant::now(); let start = Instant::now();
for i in 0..iters { for i in 0..iters {
bus.emit( bus.emit(VecEvent {
TOPIC,
VecEvent {
id: i, id: i,
payload: vec![7_u8; 1024], payload: vec![7_u8; 1024],
}, });
);
} }
wait_until_received(&received, iters).await; wait_until_received(&received, iters).await;
let elapsed = start.elapsed(); let elapsed = start.elapsed();
receiver.abort(); receiver.abort();
elapsed elapsed
}); });
}); });
group.bench_function("arc_payload_1kb", |b| { let shared_payload: Arc<[u8]> = Arc::from(vec![7_u8; 1024].into_boxed_slice());
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 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::<ArcPayloadEvent>();
let received = Arc::new(AtomicU64::new(0)); let received = Arc::new(AtomicU64::new(0));
let receiver_count = Arc::clone(&received); let receiver_count = Arc::clone(&received);
@@ -277,22 +252,18 @@ fn bench_raw_subscriber(c: &mut Criterion) {
let start = Instant::now(); let start = Instant::now();
for i in 0..iters { for i in 0..iters {
bus.emit( bus.emit(ArcPayloadEvent {
TOPIC,
ArcPayloadEvent {
id: i, id: i,
payload: Arc::clone(&payload), payload: Arc::clone(&payload),
}, });
);
} }
wait_until_received(&received, iters).await; wait_until_received(&received, iters).await;
let elapsed = start.elapsed(); let elapsed = start.elapsed();
receiver.abort(); receiver.abort();
elapsed elapsed
}
}); });
}); });
@@ -302,17 +273,17 @@ fn bench_raw_subscriber(c: &mut Criterion) {
fn bench_typed_callback(c: &mut Criterion) { fn bench_typed_callback(c: &mut Criterion) {
let rt = runtime(); 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.throughput(Throughput::Elements(1));
group.bench_function("u64", |b| { group.bench_function("u64", |b| {
b.to_async(&rt).iter_custom(|iters| async move { 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 received = Arc::new(AtomicU64::new(0));
let handler_count = Arc::clone(&received); let handler_count = Arc::clone(&received);
let subscription = bus.on::<u64, _>(TOPIC, move |event| { let subscription = bus.on::<u64>(move |event| {
let _ = event; let _ = event;
handler_count.fetch_add(1, Ordering::Relaxed); handler_count.fetch_add(1, Ordering::Relaxed);
}); });
@@ -320,27 +291,25 @@ fn bench_typed_callback(c: &mut Criterion) {
let start = Instant::now(); let start = Instant::now();
for i in 0..iters { for i in 0..iters {
bus.emit(TOPIC, i); bus.emit(i);
} }
wait_until_received(&received, iters).await; wait_until_received(&received, iters).await;
let elapsed = start.elapsed(); let elapsed = start.elapsed();
subscription.abort(); subscription.abort();
elapsed elapsed
}); });
}); });
group.bench_function("small_struct", |b| { group.bench_function("small_struct", |b| {
b.to_async(&rt).iter_custom(|iters| async move { 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 received = Arc::new(AtomicU64::new(0));
let handler_count = Arc::clone(&received); let handler_count = Arc::clone(&received);
let subscription = bus.on::<SmallEvent, _>(TOPIC, move |event| { let subscription = bus.on::<SmallEvent>(move |event| {
let _ = event.value; let _ = event.value;
handler_count.fetch_add(1, Ordering::Relaxed); handler_count.fetch_add(1, Ordering::Relaxed);
}); });
@@ -348,27 +317,25 @@ fn bench_typed_callback(c: &mut Criterion) {
let start = Instant::now(); let start = Instant::now();
for i in 0..iters { for i in 0..iters {
bus.emit(TOPIC, SmallEvent { value: i }); bus.emit(SmallEvent { value: i });
} }
wait_until_received(&received, iters).await; wait_until_received(&received, iters).await;
let elapsed = start.elapsed(); let elapsed = start.elapsed();
subscription.abort(); subscription.abort();
elapsed elapsed
}); });
}); });
group.bench_function("string_struct", |b| { group.bench_function("string_struct", |b| {
b.to_async(&rt).iter_custom(|iters| async move { 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 received = Arc::new(AtomicU64::new(0));
let handler_count = Arc::clone(&received); let handler_count = Arc::clone(&received);
let subscription = bus.on::<StringEvent, _>(TOPIC, move |event| { let subscription = bus.on::<StringEvent>(move |event| {
let _ = event.id; let _ = event.id;
let _ = event.name.len(); let _ = event.name.len();
let _ = event.message.len(); let _ = event.message.len();
@@ -378,34 +345,29 @@ fn bench_typed_callback(c: &mut Criterion) {
let start = Instant::now(); let start = Instant::now();
for i in 0..iters { for i in 0..iters {
bus.emit( bus.emit(StringEvent {
TOPIC,
StringEvent {
id: i, id: i,
name: "Alice".to_string(), name: "Alice".to_string(),
message: "hello from benchmark".to_string(), message: "hello from benchmark".to_string(),
}, });
);
} }
wait_until_received(&received, iters).await; wait_until_received(&received, iters).await;
let elapsed = start.elapsed(); let elapsed = start.elapsed();
subscription.abort(); subscription.abort();
elapsed elapsed
}); });
}); });
group.bench_function("vec_payload_1kb", |b| { group.bench_function("vec_payload_1kb", |b| {
b.to_async(&rt).iter_custom(|iters| async move { 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 received = Arc::new(AtomicU64::new(0));
let handler_count = Arc::clone(&received); let handler_count = Arc::clone(&received);
let subscription = bus.on::<VecEvent, _>(TOPIC, move |event| { let subscription = bus.on::<VecEvent>(move |event| {
let _ = event.id; let _ = event.id;
let _ = event.payload.len(); let _ = event.payload.len();
handler_count.fetch_add(1, Ordering::Relaxed); handler_count.fetch_add(1, Ordering::Relaxed);
@@ -414,34 +376,33 @@ fn bench_typed_callback(c: &mut Criterion) {
let start = Instant::now(); let start = Instant::now();
for i in 0..iters { for i in 0..iters {
bus.emit( bus.emit(VecEvent {
TOPIC,
VecEvent {
id: i, id: i,
payload: vec![7_u8; 1024], payload: vec![7_u8; 1024],
}, });
);
} }
wait_until_received(&received, iters).await; wait_until_received(&received, iters).await;
let elapsed = start.elapsed(); let elapsed = start.elapsed();
subscription.abort(); subscription.abort();
elapsed elapsed
}); });
}); });
let shared_payload: Arc<[u8]> = Arc::from(vec![7_u8; 1024].into_boxed_slice());
group.bench_function("arc_payload_1kb", |b| { group.bench_function("arc_payload_1kb", |b| {
b.to_async(&rt).iter_custom(|iters| async move { let shared = Arc::clone(&shared_payload);
let bus = Arc::new(EventBus::with_capacity(iters as usize + 1024)); b.to_async(&rt).iter_custom(|iters| {
let payload: Arc<[u8]> = Arc::from(vec![7_u8; 1024].into_boxed_slice()); let payload = Arc::clone(&shared);
async move {
let bus = EventBus::with_capacity(iters as usize + 1024);
let received = Arc::new(AtomicU64::new(0)); let received = Arc::new(AtomicU64::new(0));
let handler_count = Arc::clone(&received); let handler_count = Arc::clone(&received);
let subscription = bus.on::<ArcPayloadEvent, _>(TOPIC, move |event| { let subscription = bus.on::<ArcPayloadEvent>(move |event| {
let _ = event.id; let _ = event.id;
let _ = event.payload.len(); let _ = event.payload.len();
handler_count.fetch_add(1, Ordering::Relaxed); handler_count.fetch_add(1, Ordering::Relaxed);
@@ -450,22 +411,18 @@ fn bench_typed_callback(c: &mut Criterion) {
let start = Instant::now(); let start = Instant::now();
for i in 0..iters { for i in 0..iters {
bus.emit( bus.emit(ArcPayloadEvent {
TOPIC,
ArcPayloadEvent {
id: i, id: i,
payload: Arc::clone(&payload), payload: Arc::clone(&payload),
}, });
);
} }
wait_until_received(&received, iters).await; wait_until_received(&received, iters).await;
let elapsed = start.elapsed(); let elapsed = start.elapsed();
subscription.abort(); subscription.abort();
elapsed elapsed
}
}); });
}); });
@@ -475,13 +432,13 @@ fn bench_typed_callback(c: &mut Criterion) {
fn bench_multiple_subscribers(c: &mut Criterion) { fn bench_multiple_subscribers(c: &mut Criterion) {
let rt = runtime(); 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)); group.throughput(Throughput::Elements(1));
for subscriber_count in [1_u64, 2, 4, 8, 16, 32] { for subscriber_count in [1_u64, 2, 4, 8, 16, 32] {
group.bench_function(format!("{subscriber_count}_subscribers"), |b| { group.bench_function(format!("{subscriber_count}_subscribers"), |b| {
b.to_async(&rt).iter_custom(|iters| async move { 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 expected = iters * subscriber_count;
let received = Arc::new(AtomicU64::new(0)); let received = Arc::new(AtomicU64::new(0));
@@ -490,7 +447,7 @@ fn bench_multiple_subscribers(c: &mut Criterion) {
for _ in 0..subscriber_count { for _ in 0..subscriber_count {
let handler_count = Arc::clone(&received); let handler_count = Arc::clone(&received);
let subscription = bus.on::<SmallEvent, _>(TOPIC, move |event| { let subscription = bus.on::<SmallEvent>(move |event| {
let _ = event.value; let _ = event.value;
handler_count.fetch_add(1, Ordering::Relaxed); handler_count.fetch_add(1, Ordering::Relaxed);
}); });
@@ -501,7 +458,7 @@ fn bench_multiple_subscribers(c: &mut Criterion) {
let start = Instant::now(); let start = Instant::now();
for i in 0..iters { for i in 0..iters {
bus.emit(TOPIC, SmallEvent { value: i }); bus.emit(SmallEvent { value: i });
} }
wait_until_received(&received, expected).await; wait_until_received(&received, expected).await;
+410 -401
View File
@@ -1,420 +1,90 @@
use std::any::Any; use std::any::{Any, TypeId};
use std::future::Future; use std::collections::HashMap;
use std::fmt::Debug;
use std::sync::Arc; use std::sync::Arc;
use parking_lot::RwLock; use parking_lot::RwLock;
use std::collections::HashMap;
use std::iter;
use tokio::sync::broadcast; use tokio::sync::broadcast;
use tokio::task::JoinHandle; use tokio::task::JoinHandle;
// use tracing::log::kv::{Key, Value};
use tracing::{debug, trace, warn}; use tracing::{debug, trace, warn};
use uuid::Uuid;
/// Raw event type: an atomic reference-counted pointer to any value. use crate::handler::{AsyncHandler, AsyncHandlerWith};
pub type AnyEvent = Arc<dyn Any + Send + Sync>;
/// Default buffer capacity for each broadcast channel. /// Default buffer capacity for each broadcast channel.
const DEFAULT_CAPACITY: usize = 64; pub const DEFAULT_CAPACITY: usize = 1024;
#[derive(Debug, Clone, PartialEq, Eq)] /// Marker trait for events dispatched through [`EventBus`].
pub enum ScopeValue { ///
String(String), /// Any type implementing `Clone + Send + Sync + 'static` automatically
Uuid(Uuid), /// implements `Event`.
} pub trait Event: Clone + Send + Sync + 'static {}
impl ScopeValue {
fn into_string(self) -> String {
match self {
Self::String(value) => value,
Self::Uuid(value) => value.to_string(),
}
}
}
#[derive(Debug, Clone, PartialEq, Eq)] impl<T: Clone + Send + Sync + 'static> Event for T {}
pub struct Scope {
pub key: String,
pub value: ScopeValue,
}
impl Scope {
pub fn new(key: impl Into<String>, value: ScopeValue) -> Self {
Self {
key: key.into(),
value,
}
}
pub fn uuid(key: impl Into<String>, value: Uuid) -> Self {
Self::new(key, ScopeValue::Uuid(value))
}
pub fn string(key: impl Into<String>, value: impl Into<String>) -> Self {
Self::new(key, ScopeValue::String(value.into()))
}
}
impl IntoIterator for Scope {
type Item = Scope;
type IntoIter = iter::Once<Scope>;
fn into_iter(self) -> Self::IntoIter {
iter::once(self)
}
}
/// The central event bus.
///
/// Share it via `Arc<EventBus>` across modules.
/// Each topic has its own broadcast channel: only subscribers of the matching
/// topic are woken up on `emit` (targeted wake-up).
///
/// # Minimal example — sync callback
/// ```rust,no_run
/// use std::sync::Arc;
/// use oxspeak_server_lib::event_bus::EventBus;
///
/// #[derive(Clone, Debug)]
/// struct User { name: String }
///
/// # tokio_test::block_on(async {
/// let bus = Arc::new(EventBus::new());
///
/// bus.on::<User>("user-connected", |user| {
/// println!("Connected: {:?}", user);
/// });
///
/// bus.emit("user-connected", User { name: "Alice".into() });
/// # tokio::time::sleep(std::time::Duration::from_millis(10)).await;
/// # });
/// ```
///
/// # Example — async callback
/// ```rust,no_run
/// use std::sync::Arc;
/// use oxspeak_server_lib::event_bus::EventBus;
///
/// #[derive(Clone, Debug)]
/// struct User { name: String }
///
/// # tokio_test::block_on(async {
/// let bus = Arc::new(EventBus::new());
///
/// bus.on_async::<User, _, _>("user-connected", |user| async move {
/// println!("(async) Connected: {:?}", user);
/// });
///
/// bus.emit("user-connected", User { name: "Bob".into() });
/// # tokio::time::sleep(std::time::Duration::from_millis(10)).await;
/// # });
/// ```
#[derive(Debug)] #[derive(Debug)]
pub struct EventBus { struct EventBusInner {
/// Channels indexed by exact topic. channels: RwLock<HashMap<TypeId, Box<dyn Any + Send + Sync>>>,
channels: RwLock<HashMap<String, broadcast::Sender<AnyEvent>>>,
capacity: usize, capacity: usize,
} }
impl EventBus { /// A central, strongly-typed broadcast event bus.
/// Creates a bus with the default capacity (64 messages per channel). ///
pub fn new() -> Self { /// Unlike string/topic-based event buses, [`EventBus`] dispatches events directly
debug!( /// based on the concrete Rust type of the event (using [`TypeId`]).
"EventBus created with default capacity ({})", ///
DEFAULT_CAPACITY /// 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.
Self { ///
channels: RwLock::new(HashMap::new()), /// # Sync callback example
capacity: DEFAULT_CAPACITY, /// ```rust,no_run
} /// use event_bus::EventBus;
} ///
/// #[derive(Clone, Debug)]
/// Creates a bus with a custom buffer capacity. /// struct MessageCreatedEvent {
pub fn with_capacity(capacity: usize) -> Self { /// content: String,
debug!("EventBus created with capacity {}", capacity); /// }
Self { ///
channels: RwLock::new(HashMap::new()), /// #[tokio::main]
capacity, /// async fn main() {
} /// let bus = EventBus::new();
} ///
/// bus.on::<MessageCreatedEvent>(|event| {
// ───────────────────────────────────────────────────────────────────────── /// println!("Created message: {:?}", event);
// Internal /// });
// ───────────────────────────────────────────────────────────────────────── ///
/// bus.emit(MessageCreatedEvent {
fn get_or_create_sender(&self, topic: &str) -> broadcast::Sender<AnyEvent> { /// content: "Hello!".into(),
{ /// });
let channels = self.channels.read(); /// }
if let Some(tx) = channels.get(topic) { /// ```
return tx.clone(); ///
} /// # Async callback example
} /// ```rust,no_run
let mut channels = self.channels.write(); /// use event_bus::EventBus;
let created = !channels.contains_key(topic); ///
let tx = channels /// #[derive(Clone, Debug)]
.entry(topic.to_string()) /// struct MessageUpdatedEvent {
.or_insert_with(|| { /// id: u64,
let (tx, _) = broadcast::channel(self.capacity); /// content: String,
tx /// }
}) ///
.clone(); /// #[tokio::main]
if created { /// async fn main() {
debug!(topic, "New broadcast channel created"); /// let bus = EventBus::new();
} ///
tx /// bus.on_async::<MessageUpdatedEvent>(|event| async move {
} /// println!("(async) Updated message: {:?}", event);
/// });
// ───────────────────────────────────────────────────────────────────────── ///
// Emission /// bus.emit(MessageUpdatedEvent {
// ───────────────────────────────────────────────────────────────────────── /// id: 42,
/// content: "World!".into(),
/// 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. #[derive(Debug, Clone)]
/// pub struct EventBus {
/// # Example inner: Arc<EventBusInner>,
/// ```rust,no_run
/// # use std::sync::Arc;
/// # use oxspeak_server_lib::event_bus::EventBus;
/// # #[derive(Clone)] struct User;
/// # let bus = Arc::new(EventBus::new());
/// bus.emit("user-connected", User);
/// bus.emit("user-deleted", uuid::Uuid::new_v4());
/// ```
pub fn emit<T: Any + Send + Sync + 'static>(&self, topic: &str, event: T) {
trace!(topic, "Emitting event");
let event: AnyEvent = Arc::new(event);
self.emit_arc(topic, event);
}
// todo : undocumented...
pub fn emit_scoped<T>(&self, topic: &str, scopes: impl IntoIterator<Item = Scope>, event: T)
where
T: Any + Send + Sync + 'static,
{
let event: AnyEvent = Arc::new(event);
// Émission sur le topic général.
self.emit_arc(topic, Arc::clone(&event));
// Émission sur chaque topic scoped.
for scope in scopes {
let scoped_topic = format!("{}:{}:{}", scope.key, scope.value.into_string(), topic);
self.emit_arc(&scoped_topic, Arc::clone(&event));
}
}
// todo : undocumented...
fn emit_arc(&self, topic: &str, event: AnyEvent) {
trace!(topic, "Emitting event");
if let Some(tx) = self.channels.read().get(topic) {
let receiver_count = tx.receiver_count();
let _ = tx.send(event);
trace!(topic, receiver_count, "Event delivered to channel");
}
}
// ─────────────────────────────────────────────────────────────────────────
// Subscription — callbacks (main API)
// ─────────────────────────────────────────────────────────────────────────
/// Subscribes to a topic and calls `handler` on each event of type `T`.
///
/// The handler runs in a dedicated Tokio task (fire-and-forget).
/// Events of a different type are silently ignored.
/// Returns a [`JoinHandle`] to cancel the subscription if needed.
///
/// # Example
/// ```rust,no_run
/// # use std::sync::Arc;
/// # use oxspeak_server_lib::event_bus::EventBus;
/// # #[derive(Clone, Debug)] struct User { name: String }
/// # let bus = Arc::new(EventBus::new());
/// bus.on::<User>("user-connected", |user| {
/// println!("Connected: {:?}", user);
/// });
/// ```
pub fn on<T, F>(&self, topic: &str, handler: F) -> JoinHandle<()>
where
T: Any + Send + Sync + Clone + 'static,
F: Fn(T) + Send + Sync + 'static,
{
let mut rx = self.get_or_create_sender(topic).subscribe();
let topic_owned = topic.to_string();
debug!(topic, "Sync subscriber registered");
tokio::spawn(async move {
loop {
match rx.recv().await {
Ok(evt) => {
if let Some(typed) = evt.downcast_ref::<T>() {
trace!(topic = topic_owned, "Sync handler invoked");
handler(typed.clone());
}
}
Err(broadcast::error::RecvError::Lagged(n)) => {
warn!(
topic = topic_owned,
skipped = n,
"Subscriber lagged, messages dropped"
);
}
Err(broadcast::error::RecvError::Closed) => {
debug!(
topic = topic_owned,
"Channel closed, sync subscriber exiting"
);
break;
}
}
}
})
}
/// Subscribes to a topic and calls an **async** handler on each event of type `T`.
///
/// Ideal for performing async operations in the handler
/// (DB query, HTTP call, WebSocket broadcast, …).
/// Returns a [`JoinHandle`] to cancel the subscription if needed.
///
/// # Example
/// ```rust,no_run
/// # use std::sync::Arc;
/// # use oxspeak_server_lib::event_bus::EventBus;
/// # #[derive(Clone, Debug)] struct User { name: String }
/// # let bus = Arc::new(EventBus::new());
/// bus.on_async::<User, _, _>("user-connected", |user| async move {
/// println!("(async) Connected: {:?}", user);
/// // async work here: DB query, HTTP, etc.
/// });
/// ```
pub fn on_async<T, F, Fut>(&self, topic: &str, handler: F) -> JoinHandle<()>
where
T: Any + Send + Sync + Clone + 'static,
F: Fn(T) -> Fut + Send + Sync + 'static,
Fut: Future<Output = ()> + Send + 'static,
{
let mut rx = self.get_or_create_sender(topic).subscribe();
let topic_owned = topic.to_string();
debug!(topic, "Async subscriber registered");
tokio::spawn(async move {
loop {
match rx.recv().await {
Ok(evt) => {
if let Some(typed) = evt.downcast_ref::<T>() {
trace!(topic = topic_owned, "Async handler invoked");
handler(typed.clone()).await;
}
}
Err(broadcast::error::RecvError::Lagged(n)) => {
warn!(
topic = topic_owned,
skipped = n,
"Subscriber lagged, messages dropped"
);
}
Err(broadcast::error::RecvError::Closed) => {
debug!(
topic = topic_owned,
"Channel closed, async subscriber exiting"
);
break;
}
}
}
})
}
// todo : Undocumented
pub fn on_async_with<T, C, F, Fut>(&self, topic: &str, context: C, handler: F) -> JoinHandle<()>
where
T: Any + Send + Sync + Clone + 'static,
C: Clone + Send + Sync + 'static,
F: Fn(C, T) -> Fut + Send + Sync + 'static,
Fut: Future<Output = ()> + Send + 'static,
{
let mut rx = self.get_or_create_sender(topic).subscribe();
let topic_owned = topic.to_string();
debug!(topic, "Async subscriber registered");
tokio::spawn(async move {
loop {
match rx.recv().await {
Ok(evt) => {
if let Some(typed) = evt.downcast_ref::<T>() {
trace!(topic = topic_owned, "Async handler invoked");
handler(context.clone(), typed.clone()).await;
}
}
Err(broadcast::error::RecvError::Lagged(n)) => {
warn!(
topic = topic_owned,
skipped = n,
"Subscriber lagged, messages dropped"
);
}
Err(broadcast::error::RecvError::Closed) => {
debug!(
topic = topic_owned,
"Channel closed, async subscriber exiting"
);
break;
}
}
}
})
}
// ─────────────────────────────────────────────────────────────────────────
// Subscription — low-level access (advanced use cases)
// ─────────────────────────────────────────────────────────────────────────
/// Returns a raw [`AnyEvent`] receiver to manage the loop yourself.
///
/// Useful with the [`match_event!`][crate::match_event] macro to handle
/// multiple different types on the same topic.
///
/// # Example
/// ```rust,no_run
/// # use std::sync::Arc;
/// # use oxspeak_server_lib::event_bus::EventBus;
/// # use oxspeak_server_lib::match_event;
/// # #[derive(Clone, Debug)] struct User { name: String }
/// # #[derive(Clone, Debug)] struct UdpMetric { value: f32 }
/// # let bus = Arc::new(EventBus::new());
/// # tokio_test::block_on(async {
/// let mut rx = bus.on_raw("user-connected");
/// bus.emit("user-connected", User { name: "Alice".into() });
///
/// if let Ok(evt) = rx.recv().await {
/// match_event!(evt,
/// User => |u| println!("User: {:?}", u),
/// UdpMetric => |m| println!("Metric: {:?}", m),
/// );
/// }
/// # });
/// ```
pub fn on_raw(&self, topic: &str) -> broadcast::Receiver<AnyEvent> {
debug!(topic, "Raw subscriber registered");
self.get_or_create_sender(topic).subscribe()
}
// ─────────────────────────────────────────────────────────────────────────
// Utilities
// ─────────────────────────────────────────────────────────────────────────
/// Returns the list of currently registered topics.
pub fn topics(&self) -> Vec<String> {
self.channels.read().keys().cloned().collect()
}
} }
impl Default for EventBus { impl Default for EventBus {
@@ -422,3 +92,342 @@ impl Default for EventBus {
Self::new() Self::new()
} }
} }
impl EventBus {
/// Creates a new `EventBus` with default capacity (1024 messages per channel).
pub fn new() -> Self {
Self::with_capacity(DEFAULT_CAPACITY)
}
/// Creates a new `EventBus` with custom buffer capacity per channel.
pub fn with_capacity(capacity: usize) -> Self {
debug!(capacity, "EventBus created");
Self {
inner: Arc::new(EventBusInner {
channels: RwLock::new(HashMap::new()),
capacity,
}),
}
}
// ─────────────────────────────────────────────────────────────────────────
// Internal Channel Management
// ─────────────────────────────────────────────────────────────────────────
fn get_or_create_sender<E: Event>(&self) -> broadcast::Sender<E> {
let type_id = TypeId::of::<E>();
if let Some(tx) = self
.inner
.channels
.read()
.get(&type_id)
.and_then(|entry| entry.downcast_ref::<broadcast::Sender<E>>())
{
return tx.clone();
}
let mut channels = self.inner.channels.write();
if let Some(tx) = channels
.get(&type_id)
.and_then(|entry| entry.downcast_ref::<broadcast::Sender<E>>())
{
return tx.clone();
}
let (tx, _) = broadcast::channel::<E>(self.inner.capacity);
debug!(
event_type = std::any::type_name::<E>(),
capacity = self.inner.capacity,
"New broadcast channel created"
);
channels.insert(type_id, Box::new(tx.clone()));
tx
}
// ─────────────────────────────────────────────────────────────────────────
// Emission
// ─────────────────────────────────────────────────────────────────────────
/// Emits a strongly-typed event to all subscribers of `E` without blocking ($O(1)$).
///
/// If no subscribers exist for this event type, the event is dropped immediately
/// without cloning or allocating.
///
/// # Example
/// ```rust,no_run
/// # use event_bus::EventBus;
/// # #[derive(Clone)] struct UserConnected { id: u64 }
/// # let bus = EventBus::new();
/// bus.emit(UserConnected { id: 1 });
/// ```
pub fn emit<E: Event>(&self, event: E) {
trace!(
event_type = std::any::type_name::<E>(),
"Emitting event"
);
let type_id = TypeId::of::<E>();
if let Some(tx) = self
.inner
.channels
.read()
.get(&type_id)
.and_then(|entry| entry.downcast_ref::<broadcast::Sender<E>>())
.filter(|tx| tx.receiver_count() > 0)
{
let _ = tx.send(event);
}
}
// ─────────────────────────────────────────────────────────────────────────
// Subscriptions — Callbacks
// ─────────────────────────────────────────────────────────────────────────
/// Registers a synchronous callback for events of type `E`.
///
/// The handler runs in a dedicated background Tokio task.
/// Returns a [`JoinHandle`] allowing to cancel the subscription via [`.abort()`](JoinHandle::abort).
pub fn on<E: Event>(&self, handler: impl Fn(E) + Send + Sync + 'static) -> JoinHandle<()> {
let mut rx = self.subscribe::<E>();
let type_name = std::any::type_name::<E>();
debug!(event_type = type_name, "Sync subscriber registered");
tokio::spawn(async move {
loop {
match rx.recv().await {
Ok(event) => {
trace!(event_type = type_name, "Sync handler invoked");
handler(event);
}
Err(broadcast::error::RecvError::Lagged(skipped)) => {
warn!(
event_type = type_name,
skipped,
"Subscriber lagged behind and skipped messages"
);
}
Err(broadcast::error::RecvError::Closed) => {
debug!(event_type = type_name, "Channel closed, subscriber exiting");
break;
}
}
}
})
}
/// Registers an asynchronous callback for events of type `E`.
///
/// The handler runs in a dedicated background Tokio task.
/// Returns a [`JoinHandle`] allowing to cancel the subscription via [`.abort()`](JoinHandle::abort).
///
/// # Example
/// ```rust,no_run
/// # use event_bus::EventBus;
/// # #[derive(Clone)] struct MessageUpdatedEvent { id: u64 }
/// # let bus = EventBus::new();
/// bus.on_async::<MessageUpdatedEvent>(|event| async move {
/// println!("Updated: {:?}", event.id);
/// });
/// ```
pub fn on_async<E: Event>(&self, handler: impl AsyncHandler<E>) -> JoinHandle<()> {
let mut rx = self.subscribe::<E>();
let type_name = std::any::type_name::<E>();
debug!(event_type = type_name, "Async subscriber registered");
tokio::spawn(async move {
loop {
match rx.recv().await {
Ok(event) => {
trace!(event_type = type_name, "Async handler invoked");
handler(event).await;
}
Err(broadcast::error::RecvError::Lagged(skipped)) => {
warn!(
event_type = type_name,
skipped,
"Subscriber lagged behind and skipped messages"
);
}
Err(broadcast::error::RecvError::Closed) => {
debug!(event_type = type_name, "Channel closed, subscriber exiting");
break;
}
}
}
})
}
/// Registers an asynchronous callback for events of type `E` with an injected context `C`.
pub fn on_async_with<E: Event, C: Clone + Send + Sync + 'static>(
&self,
context: C,
handler: impl AsyncHandlerWith<E, C>,
) -> JoinHandle<()> {
let mut rx = self.subscribe::<E>();
let type_name = std::any::type_name::<E>();
debug!(event_type = type_name, "Async subscriber with context registered");
tokio::spawn(async move {
loop {
match rx.recv().await {
Ok(event) => {
trace!(event_type = type_name, "Async handler with context invoked");
handler(context.clone(), event).await;
}
Err(broadcast::error::RecvError::Lagged(skipped)) => {
warn!(
event_type = type_name,
skipped,
"Subscriber lagged behind and skipped messages"
);
}
Err(broadcast::error::RecvError::Closed) => {
debug!(event_type = type_name, "Channel closed, subscriber exiting");
break;
}
}
}
})
}
// ─────────────────────────────────────────────────────────────────────────
// Low-Level Subscription (Direct Stream / Receiver)
// ─────────────────────────────────────────────────────────────────────────
/// Returns a direct [`broadcast::Receiver<E>`] for events of type `E`.
///
/// Allows writing custom event processing loops without callback wrappers.
///
/// # Example
/// ```rust,no_run
/// use event_bus::EventBus;
///
/// #[derive(Clone)]
/// struct MyEvent;
///
/// #[tokio::main]
/// async fn main() {
/// let bus = EventBus::new();
/// let mut rx = bus.subscribe::<MyEvent>();
/// bus.emit(MyEvent);
/// if let Ok(event) = rx.recv().await {
/// // direct typed `event`
/// }
/// }
/// ```
pub fn subscribe<E: Event>(&self) -> broadcast::Receiver<E> {
self.get_or_create_sender::<E>().subscribe()
}
/// Waits for the next event of type `E` to be emitted.
///
/// Creates a temporary one-shot subscription and resolves as soon as an event of type `E`
/// is emitted. The subscription is automatically dropped after receiving the event.
///
/// # Example
/// ```rust,no_run
/// # use event_bus::EventBus;
/// # #[derive(Clone)] struct MyEvent { id: u64 }
/// #[tokio::main]
/// async fn main() {
/// let bus = EventBus::new();
/// let event = bus.wait_next::<MyEvent>().await.unwrap();
/// println!("Received next event: {}", event.id);
/// }
/// ```
pub async fn wait_next<E: Event>(&self) -> Result<E, broadcast::error::RecvError> {
let mut rx = self.subscribe::<E>();
rx.recv().await
}
/// Waits for an event of type `E` satisfying the given predicate to be emitted.
///
/// Creates a temporary subscription, receives events of type `E`, and resolves
/// when the predicate returns `true`. The subscription is automatically dropped afterwards.
///
/// # Example
/// ```rust,no_run
/// # use event_bus::EventBus;
/// # #[derive(Clone)] struct MessageSaved { id: u64 }
/// #[tokio::main]
/// async fn main() {
/// let bus = EventBus::new();
/// let target_id = 42;
/// let event = bus.wait_for::<MessageSaved>(|e| e.id == target_id).await.unwrap();
/// println!("Saved message confirmed: {}", event.id);
/// }
/// ```
pub async fn wait_for<E: Event>(
&self,
mut predicate: impl FnMut(&E) -> bool,
) -> Result<E, broadcast::error::RecvError> {
let mut rx = self.subscribe::<E>();
loop {
match rx.recv().await {
Ok(event) => {
if predicate(&event) {
return Ok(event);
}
}
Err(broadcast::error::RecvError::Lagged(skipped)) => {
warn!(
event_type = std::any::type_name::<E>(),
skipped,
"wait_for subscriber lagged behind and skipped messages"
);
}
Err(broadcast::error::RecvError::Closed) => {
return Err(broadcast::error::RecvError::Closed);
}
}
}
}
// ─────────────────────────────────────────────────────────────────────────
// Metrics & Utilities
// ─────────────────────────────────────────────────────────────────────────
/// Returns the total number of active subscribers for event type `E`.
pub fn subscriber_count<E: Event>(&self) -> usize {
let type_id = TypeId::of::<E>();
self.inner
.channels
.read()
.get(&type_id)
.and_then(|e| e.downcast_ref::<broadcast::Sender<E>>())
.map(|tx| tx.receiver_count())
.unwrap_or(0)
}
/// Returns `true` if there are any active subscribers for event type `E`.
pub fn has_subscribers<E: Event>(&self) -> bool {
self.subscriber_count::<E>() > 0
}
/// Returns the total number of broadcast channels currently instantiated in the bus.
pub fn channel_count(&self) -> usize {
self.inner.channels.read().len()
}
/// Returns `true` if no channels are currently registered in the bus.
pub fn is_empty(&self) -> bool {
self.inner.channels.read().is_empty()
}
/// Removes and drops all channels in the event bus.
pub fn clear(&self) {
self.inner.channels.write().clear();
}
/// Removes and drops the channel associated with event type `E`.
pub fn clear_type<E: Event>(&self) {
let type_id = TypeId::of::<E>();
self.inner.channels.write().remove(&type_id);
}
}
@@ -10,7 +10,7 @@ use std::future::Future;
/// enabling seamless turbofish syntax with clean field access: /// enabling seamless turbofish syntax with clean field access:
/// ///
/// ```rust,no_run /// ```rust,no_run
/// # use event_bus_typed::EventBus; /// # use event_bus::EventBus;
/// # #[derive(Clone)] struct MessageUpdatedEvent { id: u64 } /// # #[derive(Clone)] struct MessageUpdatedEvent { id: u64 }
/// # let bus = EventBus::new(); /// # let bus = EventBus::new();
/// bus.on_async::<MessageUpdatedEvent>(|event| async move { /// bus.on_async::<MessageUpdatedEvent>(|event| async move {
+44 -44
View File
@@ -1,49 +1,49 @@
/// Downcasts an [`AnyEvent`] to one or more concrete types and executes //! # event_bus
/// the matching closure if the type matches. //!
/// //! A strongly-typed, high-performance in-memory event bus for Tokio.
/// Non-matching branches are silently ignored. //!
/// //! ## Overview
/// # Syntax //!
/// ```text //! Unlike string/topic-based event buses, `event_bus` routes events using
/// match_event!(evt, Type1 => |val| { ... }, Type2 => |val| { ... }) //! their concrete Rust types ([`std::any::TypeId`]).
/// ``` //!
/// //! - **Strong typing**: No string keys required for event types, no manual `match_event!`
/// # Example //! macros, and no runtime downcasting (`downcast_ref`) inside the subscriber loops.
/// ```rust,no_run //! - **Ergonomic async subscribers**: Handlers can be registered with clean turbofish syntax:
/// # use std::sync::Arc; //! `bus.on_async::<MessageUpdatedEvent>(|event| async move { ... })`.
/// # use oxspeak_server_lib::event_bus::EventBus; //! - **Targeted wake-up**: Tokio broadcast channels are isolated per event type.
/// # use oxspeak_server_lib::match_event; //!
/// # #[derive(Clone, Debug)] struct User { name: String } //! ## Example
/// # #[derive(Clone, Debug)] struct UdpMetric { value: f32 } //!
/// # let bus = Arc::new(EventBus::new()); //! ```rust,no_run
/// # tokio_test::block_on(async { //! use event_bus::EventBus;
/// let mut rx = bus.on_raw("user-connected"); //!
/// bus.emit("user-connected", User { name: "Alice".into() }); //! #[derive(Clone, Debug, PartialEq)]
/// //! struct MessageCreatedEvent {
/// if let Ok(evt) = rx.recv().await { //! content: String,
/// match_event!(evt, //! }
/// User => |u| println!("User: {:?}", u), //!
/// UdpMetric => |m| println!("Metric: {:?}", m), //! #[tokio::main]
/// ); //! async fn main() {
/// } //! let bus = EventBus::new();
/// # }); //!
/// ``` //! // Async subscriber
#[macro_export] //! bus.on_async::<MessageCreatedEvent>(|event| async move {
macro_rules! match_event { //! println!("Received message: {}", event.content);
($evt:expr, $($type:ty => $handler:expr),+ $(,)?) => { //! });
$( //!
if let Some(val) = ($evt).downcast_ref::<$type>() { //! // Emit event
($handler)(val.clone()); //! bus.emit(MessageCreatedEvent {
} else //! content: "Hello from typed event bus!".into(),
)+ //! });
{ //! }
// No matching type → silently ignored //! ```
}
};
}
mod bus; 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)] #[cfg(test)]
mod tests; mod tests;
+312 -140
View File
@@ -1,192 +1,364 @@
use crate::{EventBus, match_event};
use std::sync::Arc; use std::sync::Arc;
use std::sync::atomic::{AtomicBool, AtomicU32, Ordering}; use std::sync::atomic::{AtomicBool, AtomicU32, Ordering};
use uuid::Uuid;
use crate::EventBus;
#[derive(Clone, Debug, PartialEq)] #[derive(Clone, Debug, PartialEq)]
struct User { struct MessageCreatedEvent {
name: String, channel_id: Uuid,
content: String,
} }
#[derive(Clone, Debug, PartialEq)] #[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, value: f32,
} }
// ── on (callback sync) ──────────────────────────────────────────────────── // ── Sync Callbacks ──────────────────────────────────────────────────────────
#[tokio::test] #[tokio::test]
async fn test_on_callback_sync() { async fn test_on_callback_sync() {
let bus = Arc::new(EventBus::new()); let bus = EventBus::new();
let received = Arc::new(AtomicBool::new(false)); let received = Arc::new(AtomicBool::new(false));
let flag = Arc::clone(&received); let flag = Arc::clone(&received);
bus.on::<User, _>("user-connected", move |user| { bus.on::<MessageCreatedEvent>(move |event| {
if user.name == "Alice" { if event.content == "Hello" {
flag.store(true, Ordering::SeqCst); flag.store(true, Ordering::SeqCst);
} }
}); });
bus.emit( bus.emit(MessageCreatedEvent {
"user-connected", channel_id: Uuid::new_v4(),
User { content: "Hello".into(),
name: "Alice".into(), });
},
);
tokio::time::sleep(std::time::Duration::from_millis(20)).await;
tokio::time::sleep(std::time::Duration::from_millis(20)).await;
assert!(received.load(Ordering::SeqCst)); assert!(received.load(Ordering::SeqCst));
} }
#[tokio::test] // ── Async Callbacks (Exact User Requirement) ────────────────────────────────
async fn test_on_targeted_wakeup() {
// Émettre sur "user-connected" ne doit pas réveiller "udp-metrics-updated"
let bus = Arc::new(EventBus::new());
let metric_called = Arc::new(AtomicBool::new(false));
let flag = Arc::clone(&metric_called);
bus.on::<UdpMetric, _>("udp-metrics-updated", move |_| {
flag.store(true, Ordering::SeqCst);
});
bus.emit(
"user-connected",
User {
name: "Carol".into(),
},
);
tokio::time::sleep(std::time::Duration::from_millis(20)).await;
assert!(!metric_called.load(Ordering::SeqCst));
}
#[tokio::test] #[tokio::test]
async fn test_on_type_mismatch_ignored() { async fn test_on_async_callback_turbofish() {
// Émettre un UdpMetric sur un topic écouté en User → handler pas appelé let bus = EventBus::new();
let bus = Arc::new(EventBus::new()); let received_content = Arc::new(tokio::sync::Mutex::new(String::new()));
let called = Arc::new(AtomicBool::new(false)); let rc = Arc::clone(&received_content);
let flag = Arc::clone(&called);
bus.on::<User, _>("mixed-topic", move |_| { // Exact syntax specified by the user:
flag.store(true, Ordering::SeqCst); // event_bus.on_async::<MessageUpdatedEvent>(|event| async move { ... });
}); bus.on_async::<MessageUpdatedEvent>(move |event| {
let rc = Arc::clone(&rc);
bus.emit("mixed-topic", UdpMetric { value: 1.0 });
tokio::time::sleep(std::time::Duration::from_millis(20)).await;
assert!(!called.load(Ordering::SeqCst));
}
#[tokio::test]
async fn test_on_multiple_subscribers_same_topic() {
let bus = Arc::new(EventBus::new());
let count = Arc::new(AtomicU32::new(0));
for _ in 0..3 {
let c = Arc::clone(&count);
bus.on::<User, _>("user-connected", move |_| {
c.fetch_add(1, Ordering::SeqCst);
});
}
bus.emit(
"user-connected",
User {
name: "Grace".into(),
},
);
tokio::time::sleep(std::time::Duration::from_millis(20)).await;
assert_eq!(count.load(Ordering::SeqCst), 3);
}
// ── on_async ──────────────────────────────────────────────────────────────
#[tokio::test]
async fn test_on_async_callback() {
let bus = Arc::new(EventBus::new());
let received = Arc::new(AtomicBool::new(false));
let flag = Arc::clone(&received);
bus.on_async::<User, _, _>("user-connected", move |user| {
let f = Arc::clone(&flag);
async move { 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); f.store(true, Ordering::SeqCst);
} }
} }
}); });
bus.emit( bus.emit(MessageUpdatedEvent {
"user-connected", id: 99,
User { content: "Inferred".into(),
name: "Async".into(), });
},
);
tokio::time::sleep(std::time::Duration::from_millis(20)).await;
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] #[tokio::test]
async fn test_on_raw_and_match_event_macro() { async fn test_on_targeted_wakeup() {
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() {
let bus = EventBus::new(); let bus = EventBus::new();
// on_raw enregistre le canal (get_or_create) let metric_called = Arc::new(AtomicBool::new(false));
let _rx1 = bus.on_raw("user-connected"); let flag = Arc::clone(&metric_called);
let _rx2 = bus.on_raw("udp-metrics-updated");
let mut topics = bus.topics(); bus.on::<UdpMetricEvent>(move |_| {
topics.sort(); flag.store(true, Ordering::SeqCst);
assert_eq!(topics, vec!["udp-metrics-updated", "user-connected"]); });
// 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] #[tokio::test]
async fn test_emit_multiple_types_same_bus() { async fn test_multiple_subscribers_same_type() {
let bus = Arc::new(EventBus::new()); let bus = EventBus::new();
let user_ok = Arc::new(AtomicBool::new(false)); let count = Arc::new(AtomicU32::new(0));
let metric_ok = Arc::new(AtomicBool::new(false));
let u = Arc::clone(&user_ok);
let m = Arc::clone(&metric_ok);
bus.on::<User, _>("user-connected", move |user| { for _ in 0..3 {
if user.name == "Bob" { let c = Arc::clone(&count);
u.store(true, Ordering::SeqCst); bus.on::<MessageCreatedEvent>(move |_| {
} c.fetch_add(1, Ordering::SeqCst);
}); });
bus.on::<UdpMetric, _>("udp-metrics-updated", move |metric| {
if (metric.value - 3.14).abs() < 0.001 {
m.store(true, 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; tokio::time::sleep(std::time::Duration::from_millis(20)).await;
assert_eq!(count.load(Ordering::SeqCst), 3);
}
assert!(user_ok.load(Ordering::SeqCst)); #[tokio::test]
async fn test_multiple_different_types_same_bus() {
let bus = EventBus::new();
let msg_ok = Arc::new(AtomicBool::new(false));
let metric_ok = Arc::new(AtomicBool::new(false));
let m_flag = Arc::clone(&msg_ok);
let u_flag = Arc::clone(&metric_ok);
bus.on::<MessageCreatedEvent>(move |event| {
if event.content == "Test" {
m_flag.store(true, Ordering::SeqCst);
}
});
bus.on::<UdpMetricEvent>(move |metric| {
if (metric.value - 42.5).abs() < 0.001 {
u_flag.store(true, Ordering::SeqCst);
}
});
bus.emit(MessageCreatedEvent {
channel_id: Uuid::new_v4(),
content: "Test".into(),
});
bus.emit(UdpMetricEvent { value: 42.5 });
tokio::time::sleep(std::time::Duration::from_millis(20)).await;
assert!(msg_ok.load(Ordering::SeqCst));
assert!(metric_ok.load(Ordering::SeqCst)); assert!(metric_ok.load(Ordering::SeqCst));
} }
// ── Direct Stream / Receiver (No match_event! needed) ───────────────────────
#[tokio::test]
async fn test_subscribe_direct_typed_receiver() {
let bus = EventBus::new();
let mut rx = bus.subscribe::<MessageUpdatedEvent>();
bus.emit(MessageUpdatedEvent {
id: 123,
content: "Direct typed".into(),
});
let event = rx.recv().await.expect("failed to receive event");
// event is directly of type MessageUpdatedEvent, no downcast needed!
assert_eq!(event.id, 123);
assert_eq!(event.content, "Direct typed");
}
// ── In-Handler Filtering (Direct Field Access) ──────────────────────────────
#[tokio::test]
async fn test_filter_by_field_in_subscriber() {
let bus = EventBus::new();
let channel_a = Uuid::new_v4();
let channel_b = Uuid::new_v4();
let count_a = Arc::new(AtomicU32::new(0));
let count_b = Arc::new(AtomicU32::new(0));
let count_global = Arc::new(AtomicU32::new(0));
let ca = Arc::clone(&count_a);
bus.on_async::<MessageCreatedEvent>(move |event| {
let ca = Arc::clone(&ca);
async move {
if event.channel_id == channel_a {
ca.fetch_add(1, Ordering::SeqCst);
}
}
});
let cb = Arc::clone(&count_b);
bus.on_async::<MessageCreatedEvent>(move |event| {
let cb = Arc::clone(&cb);
async move {
if event.channel_id == channel_b {
cb.fetch_add(1, Ordering::SeqCst);
}
}
});
let cg = Arc::clone(&count_global);
bus.on::<MessageCreatedEvent>(move |_| {
cg.fetch_add(1, Ordering::SeqCst);
});
// Emit event with channel_a
bus.emit(MessageCreatedEvent {
channel_id: channel_a,
content: "For A".into(),
});
tokio::time::sleep(std::time::Duration::from_millis(20)).await;
// Both channel_a handler and global handler processed it, but not channel_b
assert_eq!(count_a.load(Ordering::SeqCst), 1);
assert_eq!(count_b.load(Ordering::SeqCst), 0);
assert_eq!(count_global.load(Ordering::SeqCst), 1);
}
// ── Async with Context ──────────────────────────────────────────────────────
#[tokio::test]
async fn test_on_async_with_context() {
let bus = EventBus::new();
let prefix = Arc::new("Prefix: ".to_string());
let result = Arc::new(tokio::sync::Mutex::new(String::new()));
let r = Arc::clone(&result);
bus.on_async_with::<MessageCreatedEvent, _>(prefix, move |ctx, event| {
let r = Arc::clone(&r);
async move {
let mut lock = r.lock().await;
*lock = format!("{}{}", ctx, event.content);
}
});
bus.emit(MessageCreatedEvent {
channel_id: Uuid::new_v4(),
content: "Hello Context".into(),
});
tokio::time::sleep(std::time::Duration::from_millis(20)).await;
let final_str = result.lock().await.clone();
assert_eq!(final_str, "Prefix: Hello Context");
}
// ── Metrics, Utilities & Cleanup ────────────────────────────────────────────
#[test]
fn test_subscriber_count_and_clear() {
let bus = EventBus::new();
assert_eq!(bus.subscriber_count::<MessageCreatedEvent>(), 0);
assert!(!bus.has_subscribers::<MessageCreatedEvent>());
let _sub = bus.subscribe::<MessageCreatedEvent>();
assert_eq!(bus.subscriber_count::<MessageCreatedEvent>(), 1);
assert!(bus.has_subscribers::<MessageCreatedEvent>());
assert_eq!(bus.channel_count(), 1);
bus.clear_type::<MessageCreatedEvent>();
assert_eq!(bus.subscriber_count::<MessageCreatedEvent>(), 0);
assert_eq!(bus.channel_count(), 0);
}
#[tokio::test]
async fn test_subscription_abort() {
let bus = EventBus::new();
let count = Arc::new(AtomicU32::new(0));
let c = Arc::clone(&count);
let handle = bus.on::<MessageDeletedEvent>(move |_| {
c.fetch_add(1, Ordering::SeqCst);
});
bus.emit(MessageDeletedEvent { id: 1 });
tokio::time::sleep(std::time::Duration::from_millis(20)).await;
assert_eq!(count.load(Ordering::SeqCst), 1);
// Cancel the subscription
handle.abort();
tokio::time::sleep(std::time::Duration::from_millis(10)).await;
bus.emit(MessageDeletedEvent { id: 2 });
tokio::time::sleep(std::time::Duration::from_millis(20)).await;
// Count should not increase after abort
assert_eq!(count.load(Ordering::SeqCst), 1);
}
// ── One-Shot Listeners (wait_next & wait_for) ────────────────────────────────
#[tokio::test]
async fn test_wait_next() {
let bus = EventBus::new();
let b = bus.clone();
tokio::spawn(async move {
tokio::time::sleep(std::time::Duration::from_millis(10)).await;
b.emit(MessageUpdatedEvent {
id: 777,
content: "Next event".into(),
});
});
let event = bus.wait_next::<MessageUpdatedEvent>().await.unwrap();
assert_eq!(event.id, 777);
assert_eq!(event.content, "Next event");
}
#[tokio::test]
async fn test_wait_for() {
let bus = EventBus::new();
let b = bus.clone();
tokio::spawn(async move {
tokio::time::sleep(std::time::Duration::from_millis(10)).await;
b.emit(MessageUpdatedEvent {
id: 1,
content: "Ignore".into(),
});
tokio::time::sleep(std::time::Duration::from_millis(10)).await;
b.emit(MessageUpdatedEvent {
id: 2,
content: "Target".into(),
});
});
let event = bus
.wait_for::<MessageUpdatedEvent>(|e| e.id == 2)
.await
.unwrap();
assert_eq!(event.id, 2);
assert_eq!(event.content, "Target");
}
-798
View File
@@ -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"
-25
View File
@@ -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"] }
@@ -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<u8>,
}
#[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::<u64>();
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::<SmallEvent>();
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::<StringEvent>();
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::<VecEvent>();
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::<ArcPayloadEvent>();
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::<u64>(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::<SmallEvent>(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::<StringEvent>(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::<VecEvent>(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::<ArcPayloadEvent>(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::<SmallEvent>(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);
-433
View File
@@ -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<T: Clone + Send + Sync + 'static> Event for T {}
#[derive(Debug)]
struct EventBusInner {
channels: RwLock<HashMap<TypeId, Box<dyn Any + Send + Sync>>>,
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::<MessageCreatedEvent>(|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::<MessageUpdatedEvent>(|event| async move {
/// println!("(async) Updated message: {:?}", event);
/// });
///
/// bus.emit(MessageUpdatedEvent {
/// id: 42,
/// content: "World!".into(),
/// });
/// }
/// ```
#[derive(Debug, Clone)]
pub struct EventBus {
inner: Arc<EventBusInner>,
}
impl Default for EventBus {
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<E: Event>(&self) -> broadcast::Sender<E> {
let type_id = TypeId::of::<E>();
if let Some(tx) = self
.inner
.channels
.read()
.get(&type_id)
.and_then(|entry| entry.downcast_ref::<broadcast::Sender<E>>())
{
return tx.clone();
}
let mut channels = self.inner.channels.write();
if let Some(tx) = channels
.get(&type_id)
.and_then(|entry| entry.downcast_ref::<broadcast::Sender<E>>())
{
return tx.clone();
}
let (tx, _) = broadcast::channel::<E>(self.inner.capacity);
debug!(
event_type = std::any::type_name::<E>(),
capacity = self.inner.capacity,
"New broadcast channel created"
);
channels.insert(type_id, Box::new(tx.clone()));
tx
}
// ─────────────────────────────────────────────────────────────────────────
// Emission
// ─────────────────────────────────────────────────────────────────────────
/// Emits a strongly-typed event to all subscribers of `E` without blocking ($O(1)$).
///
/// If no subscribers exist for this event type, the event is dropped immediately
/// without cloning or allocating.
///
/// # Example
/// ```rust,no_run
/// # use event_bus_typed::EventBus;
/// # #[derive(Clone)] struct UserConnected { id: u64 }
/// # let bus = EventBus::new();
/// bus.emit(UserConnected { id: 1 });
/// ```
pub fn emit<E: Event>(&self, event: E) {
trace!(
event_type = std::any::type_name::<E>(),
"Emitting event"
);
let type_id = TypeId::of::<E>();
if let Some(tx) = self
.inner
.channels
.read()
.get(&type_id)
.and_then(|entry| entry.downcast_ref::<broadcast::Sender<E>>())
.filter(|tx| tx.receiver_count() > 0)
{
let _ = tx.send(event);
}
}
// ─────────────────────────────────────────────────────────────────────────
// Subscriptions — Callbacks
// ─────────────────────────────────────────────────────────────────────────
/// Registers a synchronous callback for events of type `E`.
///
/// The handler runs in a dedicated background Tokio task.
/// Returns a [`JoinHandle`] allowing to cancel the subscription via [`.abort()`](JoinHandle::abort).
pub fn on<E: Event>(&self, handler: impl Fn(E) + Send + Sync + 'static) -> JoinHandle<()> {
let mut rx = self.subscribe::<E>();
let type_name = std::any::type_name::<E>();
debug!(event_type = type_name, "Sync subscriber registered");
tokio::spawn(async move {
loop {
match rx.recv().await {
Ok(event) => {
trace!(event_type = type_name, "Sync handler invoked");
handler(event);
}
Err(broadcast::error::RecvError::Lagged(skipped)) => {
warn!(
event_type = type_name,
skipped,
"Subscriber lagged behind and skipped messages"
);
}
Err(broadcast::error::RecvError::Closed) => {
debug!(event_type = type_name, "Channel closed, subscriber exiting");
break;
}
}
}
})
}
/// Registers an asynchronous callback for events of type `E`.
///
/// The handler runs in a dedicated background Tokio task.
/// Returns a [`JoinHandle`] allowing to cancel the subscription via [`.abort()`](JoinHandle::abort).
///
/// # Example
/// ```rust,no_run
/// # use event_bus_typed::EventBus;
/// # #[derive(Clone)] struct MessageUpdatedEvent { id: u64 }
/// # let bus = EventBus::new();
/// bus.on_async::<MessageUpdatedEvent>(|event| async move {
/// println!("Updated: {:?}", event.id);
/// });
/// ```
pub fn on_async<E: Event>(&self, handler: impl AsyncHandler<E>) -> JoinHandle<()> {
let mut rx = self.subscribe::<E>();
let type_name = std::any::type_name::<E>();
debug!(event_type = type_name, "Async subscriber registered");
tokio::spawn(async move {
loop {
match rx.recv().await {
Ok(event) => {
trace!(event_type = type_name, "Async handler invoked");
handler(event).await;
}
Err(broadcast::error::RecvError::Lagged(skipped)) => {
warn!(
event_type = type_name,
skipped,
"Subscriber lagged behind and skipped messages"
);
}
Err(broadcast::error::RecvError::Closed) => {
debug!(event_type = type_name, "Channel closed, subscriber exiting");
break;
}
}
}
})
}
/// Registers an asynchronous callback for events of type `E` with an injected context `C`.
pub fn on_async_with<E: Event, C: Clone + Send + Sync + 'static>(
&self,
context: C,
handler: impl AsyncHandlerWith<E, C>,
) -> JoinHandle<()> {
let mut rx = self.subscribe::<E>();
let type_name = std::any::type_name::<E>();
debug!(event_type = type_name, "Async subscriber with context registered");
tokio::spawn(async move {
loop {
match rx.recv().await {
Ok(event) => {
trace!(event_type = type_name, "Async handler with context invoked");
handler(context.clone(), event).await;
}
Err(broadcast::error::RecvError::Lagged(skipped)) => {
warn!(
event_type = type_name,
skipped,
"Subscriber lagged behind and skipped messages"
);
}
Err(broadcast::error::RecvError::Closed) => {
debug!(event_type = type_name, "Channel closed, subscriber exiting");
break;
}
}
}
})
}
// ─────────────────────────────────────────────────────────────────────────
// Low-Level Subscription (Direct Stream / Receiver)
// ─────────────────────────────────────────────────────────────────────────
/// Returns a direct [`broadcast::Receiver<E>`] for events of type `E`.
///
/// Allows writing custom event processing loops without callback wrappers.
///
/// # Example
/// ```rust,no_run
/// use event_bus_typed::EventBus;
///
/// #[derive(Clone)]
/// struct MyEvent;
///
/// #[tokio::main]
/// async fn main() {
/// let bus = EventBus::new();
/// let mut rx = bus.subscribe::<MyEvent>();
/// bus.emit(MyEvent);
/// if let Ok(event) = rx.recv().await {
/// // direct typed `event`
/// }
/// }
/// ```
pub fn subscribe<E: Event>(&self) -> broadcast::Receiver<E> {
self.get_or_create_sender::<E>().subscribe()
}
/// Waits for the next event of type `E` to be emitted.
///
/// Creates a temporary one-shot subscription and resolves as soon as an event of type `E`
/// is emitted. The subscription is automatically dropped after receiving the event.
///
/// # Example
/// ```rust,no_run
/// # use event_bus_typed::EventBus;
/// # #[derive(Clone)] struct MyEvent { id: u64 }
/// #[tokio::main]
/// async fn main() {
/// let bus = EventBus::new();
/// let event = bus.wait_next::<MyEvent>().await.unwrap();
/// println!("Received next event: {}", event.id);
/// }
/// ```
pub async fn wait_next<E: Event>(&self) -> Result<E, broadcast::error::RecvError> {
let mut rx = self.subscribe::<E>();
rx.recv().await
}
/// Waits for an event of type `E` satisfying the given predicate to be emitted.
///
/// Creates a temporary subscription, receives events of type `E`, and resolves
/// when the predicate returns `true`. The subscription is automatically dropped afterwards.
///
/// # Example
/// ```rust,no_run
/// # use event_bus_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::<MessageSaved>(|e| e.id == target_id).await.unwrap();
/// println!("Saved message confirmed: {}", event.id);
/// }
/// ```
pub async fn wait_for<E: Event>(
&self,
mut predicate: impl FnMut(&E) -> bool,
) -> Result<E, broadcast::error::RecvError> {
let mut rx = self.subscribe::<E>();
loop {
match rx.recv().await {
Ok(event) => {
if predicate(&event) {
return Ok(event);
}
}
Err(broadcast::error::RecvError::Lagged(skipped)) => {
warn!(
event_type = std::any::type_name::<E>(),
skipped,
"wait_for subscriber lagged behind and skipped messages"
);
}
Err(broadcast::error::RecvError::Closed) => {
return Err(broadcast::error::RecvError::Closed);
}
}
}
}
// ─────────────────────────────────────────────────────────────────────────
// Metrics & Utilities
// ─────────────────────────────────────────────────────────────────────────
/// Returns the total number of active subscribers for event type `E`.
pub fn subscriber_count<E: Event>(&self) -> usize {
let type_id = TypeId::of::<E>();
self.inner
.channels
.read()
.get(&type_id)
.and_then(|e| e.downcast_ref::<broadcast::Sender<E>>())
.map(|tx| tx.receiver_count())
.unwrap_or(0)
}
/// Returns `true` if there are any active subscribers for event type `E`.
pub fn has_subscribers<E: Event>(&self) -> bool {
self.subscriber_count::<E>() > 0
}
/// Returns the total number of broadcast channels currently instantiated in the bus.
pub fn channel_count(&self) -> usize {
self.inner.channels.read().len()
}
/// Returns `true` if no channels are currently registered in the bus.
pub fn is_empty(&self) -> bool {
self.inner.channels.read().is_empty()
}
/// Removes and drops all channels in the event bus.
pub fn clear(&self) {
self.inner.channels.write().clear();
}
/// Removes and drops the channel associated with event type `E`.
pub fn clear_type<E: Event>(&self) {
let type_id = TypeId::of::<E>();
self.inner.channels.write().remove(&type_id);
}
}
-49
View File
@@ -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::<MessageUpdatedEvent>(|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::<MessageCreatedEvent>(|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;
-364
View File
@@ -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::<MessageCreatedEvent>(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::<MessageUpdatedEvent>(|event| async move { ... });
bus.on_async::<MessageUpdatedEvent>(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::<UdpMetricEvent>(move |_| {
flag.store(true, Ordering::SeqCst);
});
// Emitting MessageCreatedEvent must never wake up UdpMetricEvent subscribers
bus.emit(MessageCreatedEvent {
channel_id: Uuid::new_v4(),
content: "Ignore me".into(),
});
tokio::time::sleep(std::time::Duration::from_millis(20)).await;
assert!(!metric_called.load(Ordering::SeqCst));
}
#[tokio::test]
async fn test_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::<MessageCreatedEvent>(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::<MessageCreatedEvent>(move |event| {
if event.content == "Test" {
m_flag.store(true, Ordering::SeqCst);
}
});
bus.on::<UdpMetricEvent>(move |metric| {
if (metric.value - 42.5).abs() < 0.001 {
u_flag.store(true, Ordering::SeqCst);
}
});
bus.emit(MessageCreatedEvent {
channel_id: Uuid::new_v4(),
content: "Test".into(),
});
bus.emit(UdpMetricEvent { value: 42.5 });
tokio::time::sleep(std::time::Duration::from_millis(20)).await;
assert!(msg_ok.load(Ordering::SeqCst));
assert!(metric_ok.load(Ordering::SeqCst));
}
// ── Direct Stream / Receiver (No match_event! needed) ───────────────────────
#[tokio::test]
async fn test_subscribe_direct_typed_receiver() {
let bus = EventBus::new();
let mut rx = bus.subscribe::<MessageUpdatedEvent>();
bus.emit(MessageUpdatedEvent {
id: 123,
content: "Direct typed".into(),
});
let event = rx.recv().await.expect("failed to receive event");
// event is directly of type MessageUpdatedEvent, no downcast needed!
assert_eq!(event.id, 123);
assert_eq!(event.content, "Direct typed");
}
// ── In-Handler Filtering (Direct Field Access) ──────────────────────────────
#[tokio::test]
async fn test_filter_by_field_in_subscriber() {
let bus = EventBus::new();
let channel_a = Uuid::new_v4();
let channel_b = Uuid::new_v4();
let count_a = Arc::new(AtomicU32::new(0));
let count_b = Arc::new(AtomicU32::new(0));
let count_global = Arc::new(AtomicU32::new(0));
let ca = Arc::clone(&count_a);
bus.on_async::<MessageCreatedEvent>(move |event| {
let ca = Arc::clone(&ca);
async move {
if event.channel_id == channel_a {
ca.fetch_add(1, Ordering::SeqCst);
}
}
});
let cb = Arc::clone(&count_b);
bus.on_async::<MessageCreatedEvent>(move |event| {
let cb = Arc::clone(&cb);
async move {
if event.channel_id == channel_b {
cb.fetch_add(1, Ordering::SeqCst);
}
}
});
let cg = Arc::clone(&count_global);
bus.on::<MessageCreatedEvent>(move |_| {
cg.fetch_add(1, Ordering::SeqCst);
});
// Emit event with channel_a
bus.emit(MessageCreatedEvent {
channel_id: channel_a,
content: "For A".into(),
});
tokio::time::sleep(std::time::Duration::from_millis(20)).await;
// Both channel_a handler and global handler processed it, but not channel_b
assert_eq!(count_a.load(Ordering::SeqCst), 1);
assert_eq!(count_b.load(Ordering::SeqCst), 0);
assert_eq!(count_global.load(Ordering::SeqCst), 1);
}
// ── Async with Context ──────────────────────────────────────────────────────
#[tokio::test]
async fn test_on_async_with_context() {
let bus = EventBus::new();
let prefix = Arc::new("Prefix: ".to_string());
let result = Arc::new(tokio::sync::Mutex::new(String::new()));
let r = Arc::clone(&result);
bus.on_async_with::<MessageCreatedEvent, _>(prefix, move |ctx, event| {
let r = Arc::clone(&r);
async move {
let mut lock = r.lock().await;
*lock = format!("{}{}", ctx, event.content);
}
});
bus.emit(MessageCreatedEvent {
channel_id: Uuid::new_v4(),
content: "Hello Context".into(),
});
tokio::time::sleep(std::time::Duration::from_millis(20)).await;
let final_str = result.lock().await.clone();
assert_eq!(final_str, "Prefix: Hello Context");
}
// ── Metrics, Utilities & Cleanup ────────────────────────────────────────────
#[test]
fn test_subscriber_count_and_clear() {
let bus = EventBus::new();
assert_eq!(bus.subscriber_count::<MessageCreatedEvent>(), 0);
assert!(!bus.has_subscribers::<MessageCreatedEvent>());
let _sub = bus.subscribe::<MessageCreatedEvent>();
assert_eq!(bus.subscriber_count::<MessageCreatedEvent>(), 1);
assert!(bus.has_subscribers::<MessageCreatedEvent>());
assert_eq!(bus.channel_count(), 1);
bus.clear_type::<MessageCreatedEvent>();
assert_eq!(bus.subscriber_count::<MessageCreatedEvent>(), 0);
assert_eq!(bus.channel_count(), 0);
}
#[tokio::test]
async fn test_subscription_abort() {
let bus = EventBus::new();
let count = Arc::new(AtomicU32::new(0));
let c = Arc::clone(&count);
let handle = bus.on::<MessageDeletedEvent>(move |_| {
c.fetch_add(1, Ordering::SeqCst);
});
bus.emit(MessageDeletedEvent { id: 1 });
tokio::time::sleep(std::time::Duration::from_millis(20)).await;
assert_eq!(count.load(Ordering::SeqCst), 1);
// Cancel the subscription
handle.abort();
tokio::time::sleep(std::time::Duration::from_millis(10)).await;
bus.emit(MessageDeletedEvent { id: 2 });
tokio::time::sleep(std::time::Duration::from_millis(20)).await;
// Count should not increase after abort
assert_eq!(count.load(Ordering::SeqCst), 1);
}
// ── One-Shot Listeners (wait_next & wait_for) ────────────────────────────────
#[tokio::test]
async fn test_wait_next() {
let bus = EventBus::new();
let b = bus.clone();
tokio::spawn(async move {
tokio::time::sleep(std::time::Duration::from_millis(10)).await;
b.emit(MessageUpdatedEvent {
id: 777,
content: "Next event".into(),
});
});
let event = bus.wait_next::<MessageUpdatedEvent>().await.unwrap();
assert_eq!(event.id, 777);
assert_eq!(event.content, "Next event");
}
#[tokio::test]
async fn test_wait_for() {
let bus = EventBus::new();
let b = bus.clone();
tokio::spawn(async move {
tokio::time::sleep(std::time::Duration::from_millis(10)).await;
b.emit(MessageUpdatedEvent {
id: 1,
content: "Ignore".into(),
});
tokio::time::sleep(std::time::Duration::from_millis(10)).await;
b.emit(MessageUpdatedEvent {
id: 2,
content: "Target".into(),
});
});
let event = bus
.wait_for::<MessageUpdatedEvent>(|e| e.id == 2)
.await
.unwrap();
assert_eq!(event.id, 2);
assert_eq!(event.content, "Target");
}
+16
View File
@@ -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,
}
+27
View File
@@ -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<channel_user_permission::Model>,
}
#[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<channel_role_permission::Model>,
}
+5
View File
@@ -1,5 +1,10 @@
pub mod category;
pub mod channel; pub mod channel;
pub mod channel_permission;
pub mod emoji; pub mod emoji;
pub mod message; pub mod message;
pub mod role;
pub mod server; pub mod server;
pub mod server_permission;
pub mod server_tree; pub mod server_tree;
pub mod user;
+29
View File
@@ -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,
}
+13
View File
@@ -1,4 +1,6 @@
use crate::models::server; use crate::models::server;
use crate::models::server_user;
use uuid::Uuid;
#[derive(Debug, Clone)] #[derive(Debug, Clone)]
pub struct ServerCreatedEvent { pub struct ServerCreatedEvent {
@@ -13,4 +15,15 @@ pub struct ServerUpdatedEvent {
#[derive(Debug, Clone)] #[derive(Debug, Clone)]
pub struct ServerDeletedEvent { pub struct ServerDeletedEvent {
pub server: server::Model, pub server: server::Model,
pub audience: Vec<Uuid>,
}
#[derive(Debug, Clone)]
pub struct ServerUserCreatedEvent {
pub server_user: server_user::Model,
}
#[derive(Debug, Clone)]
pub struct ServerUserDeletedEvent {
pub server_user: server_user::Model,
} }
+16
View File
@@ -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<server_user_permission::Model>,
}
#[derive(Debug, Clone)]
pub struct ServerRolePermissionUpdatedEvent {
pub server_id: Uuid,
pub role_id: Uuid,
pub permission: Option<server_role_permission::Model>,
}
+21
View File
@@ -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,
}
+2 -5
View File
@@ -108,12 +108,9 @@ async fn create_channel(state: &AppState, ids: &[Uuid]) -> Result<channel::Model
.services .services
.realtime_registry .realtime_registry
.set_channel_users(channel.id, ids.iter().copied()); .set_channel_users(channel.id, ids.iter().copied());
state.event_bus.emit( state.event_bus.emit(ChannelCreatedEvent {
"channel_created",
ChannelCreatedEvent {
channel: channel.clone(), channel: channel.clone(),
}, });
);
Ok(channel) Ok(channel)
} }
+5 -13
View File
@@ -155,12 +155,9 @@ pub async fn create(
..Default::default() ..Default::default()
}; };
let created = state.services.emoji.create(model, name).await?; let created = state.services.emoji.create(model, name).await?;
state.event_bus.emit( state.event_bus.emit(EmojiCreatedEvent {
"emoji_created",
EmojiCreatedEvent {
emoji: created.clone(), emoji: created.clone(),
}, });
);
Ok((StatusCode::CREATED, Json(mapper::response(created)))) Ok((StatusCode::CREATED, Json(mapper::response(created))))
} }
@@ -201,13 +198,10 @@ pub async fn update(
active.name = Set(EmojiService::normalize_name(&name)?); active.name = Set(EmojiService::normalize_name(&name)?);
} }
let updated = state.repositories.emoji.update(active).await?; let updated = state.repositories.emoji.update(active).await?;
state.event_bus.emit( state.event_bus.emit(EmojiUpdatedEvent {
"emoji_updated",
EmojiUpdatedEvent {
previous, previous,
emoji: updated.clone(), emoji: updated.clone(),
}, });
);
Ok(Json(mapper::response(updated))) Ok(Json(mapper::response(updated)))
} }
@@ -241,9 +235,7 @@ pub async fn delete(
model.file_path.as_deref(), model.file_path.as_deref(),
) )
.await; .await;
state state.event_bus.emit(EmojiDeletedEvent { emoji: model });
.event_bus
.emit("emoji_deleted", EmojiDeletedEvent { emoji: model });
Ok(StatusCode::NO_CONTENT) Ok(StatusCode::NO_CONTENT)
} else { } else {
Err(HTTPError::NotFound) Err(HTTPError::NotFound)
+33 -36
View File
@@ -1,4 +1,7 @@
use crate::core::AppState; use crate::core::AppState;
use crate::domain::events::category::{
CategoryCreatedEvent, CategoryDeletedEvent, CategoryUpdatedEvent,
};
use crate::domain::events::channel::{ use crate::domain::events::channel::{
ChannelCreatedEvent, ChannelDeletedEvent, ChannelUpdatedEvent, ChannelCreatedEvent, ChannelDeletedEvent, ChannelUpdatedEvent,
}; };
@@ -7,8 +10,9 @@ use crate::domain::events::message::{
MessageCreatedEvent, MessageDeletedEvent, MessageReactionAddedEvent, MessageCreatedEvent, MessageDeletedEvent, MessageReactionAddedEvent,
MessageReactionRemovedEvent, MessageUpdatedEvent, MessageReactionRemovedEvent, MessageUpdatedEvent,
}; };
use crate::domain::events::server::{ServerCreatedEvent, ServerDeletedEvent, ServerUpdatedEvent};
use crate::domain::events::server_tree::ServerTreeInvalidatedEvent; use crate::domain::events::server_tree::ServerTreeInvalidatedEvent;
use crate::models::{category, server, server_user, user}; use crate::models::{server_user, user};
use crate::repositories::Repositories; use crate::repositories::Repositories;
use crate::routes::category::mapper::category_model_to_category_response; use crate::routes::category::mapper::category_model_to_category_response;
use crate::routes::channel::mapper::channel_model_to_channel_response; use crate::routes::channel::mapper::channel_model_to_channel_response;
@@ -112,7 +116,7 @@ impl RealtimeRouter {
pub fn start(self: &Arc<Self>, event_bus: Arc<EventBus>) { pub fn start(self: &Arc<Self>, event_bus: Arc<EventBus>) {
let router = Arc::clone(self); let router = Arc::clone(self);
event_bus.on_async::<MessageCreatedEvent, _, _>("message_created", move |event| { event_bus.on_async::<MessageCreatedEvent>(move |event| {
let router = Arc::clone(&router); let router = Arc::clone(&router);
async move { async move {
let attachments = router let attachments = router
@@ -142,7 +146,7 @@ impl RealtimeRouter {
}); });
let router = Arc::clone(self); let router = Arc::clone(self);
event_bus.on_async::<MessageUpdatedEvent, _, _>("message_updated", move |event| { event_bus.on_async::<MessageUpdatedEvent>(move |event| {
let router = Arc::clone(&router); let router = Arc::clone(&router);
async move { async move {
let reactions = router let reactions = router
@@ -171,7 +175,7 @@ impl RealtimeRouter {
}); });
let router = Arc::clone(self); let router = Arc::clone(self);
event_bus.on_async::<MessageDeletedEvent, _, _>("message_deleted", move |event| { event_bus.on_async::<MessageDeletedEvent>(move |event| {
let router = Arc::clone(&router); let router = Arc::clone(&router);
async move { async move {
let users = router let users = router
@@ -185,9 +189,7 @@ impl RealtimeRouter {
}); });
let router = Arc::clone(self); let router = Arc::clone(self);
event_bus.on_async::<MessageReactionAddedEvent, _, _>( event_bus.on_async::<MessageReactionAddedEvent>(move |event| {
"message_reaction_added",
move |event| {
let router = Arc::clone(&router); let router = Arc::clone(&router);
async move { async move {
let users = router let users = router
@@ -201,13 +203,10 @@ impl RealtimeRouter {
reaction_model_to_response(event.reaction), reaction_model_to_response(event.reaction),
); );
} }
}, });
);
let router = Arc::clone(self); let router = Arc::clone(self);
event_bus.on_async::<MessageReactionRemovedEvent, _, _>( event_bus.on_async::<MessageReactionRemovedEvent>(move |event| {
"message_reaction_removed",
move |event| {
let router = Arc::clone(&router); let router = Arc::clone(&router);
async move { async move {
let users = router let users = router
@@ -221,11 +220,10 @@ impl RealtimeRouter {
reaction_model_to_response(event.reaction), reaction_model_to_response(event.reaction),
); );
} }
}, });
);
let router = Arc::clone(self); let router = Arc::clone(self);
event_bus.on_async::<ChannelCreatedEvent, _, _>("channel_created", move |event| { event_bus.on_async::<ChannelCreatedEvent>(move |event| {
let router = Arc::clone(&router); let router = Arc::clone(&router);
async move { async move {
if let Err(error) = router if let Err(error) = router
@@ -251,7 +249,7 @@ impl RealtimeRouter {
}); });
let router = Arc::clone(self); let router = Arc::clone(self);
event_bus.on_async::<ChannelUpdatedEvent, _, _>("channel_updated", move |event| { event_bus.on_async::<ChannelUpdatedEvent>(move |event| {
let router = Arc::clone(&router); let router = Arc::clone(&router);
async move { async move {
let mut users = router let mut users = router
@@ -274,7 +272,7 @@ impl RealtimeRouter {
}); });
let router = Arc::clone(self); let router = Arc::clone(self);
event_bus.on_async::<ChannelDeletedEvent, _, _>("channel_deleted", move |event| { event_bus.on_async::<ChannelDeletedEvent>(move |event| {
let router = Arc::clone(&router); let router = Arc::clone(&router);
async move { async move {
let users = router let users = router
@@ -292,9 +290,10 @@ impl RealtimeRouter {
}); });
let router = Arc::clone(self); let router = Arc::clone(self);
event_bus.on_async::<category::Model, _, _>("category_created", move |category| { event_bus.on_async::<CategoryCreatedEvent>(move |event| {
let router = Arc::clone(&router); let router = Arc::clone(&router);
async move { async move {
let category = event.category;
let users = router.server_users(category.server_id).await; let users = router.server_users(category.server_id).await;
router.gateway.send_to_users( router.gateway.send_to_users(
users, users,
@@ -306,9 +305,10 @@ impl RealtimeRouter {
}); });
let router = Arc::clone(self); let router = Arc::clone(self);
event_bus.on_async::<category::Model, _, _>("category_updated", move |category| { event_bus.on_async::<CategoryUpdatedEvent>(move |event| {
let router = Arc::clone(&router); let router = Arc::clone(&router);
async move { async move {
let category = event.category;
let users = router.server_users(category.server_id).await; let users = router.server_users(category.server_id).await;
router.gateway.send_to_users( router.gateway.send_to_users(
users, users,
@@ -320,9 +320,10 @@ impl RealtimeRouter {
}); });
let router = Arc::clone(self); let router = Arc::clone(self);
event_bus.on_async::<category::Model, _, _>("category_deleted", move |category| { event_bus.on_async::<CategoryDeletedEvent>(move |event| {
let router = Arc::clone(&router); let router = Arc::clone(&router);
async move { async move {
let category = event.category;
let users = router.server_users(category.server_id).await; let users = router.server_users(category.server_id).await;
router router
.gateway .gateway
@@ -331,9 +332,10 @@ impl RealtimeRouter {
}); });
let router = Arc::clone(self); let router = Arc::clone(self);
event_bus.on_async::<server::Model, _, _>("server_created", move |server| { event_bus.on_async::<ServerCreatedEvent>(move |event| {
let router = Arc::clone(&router); let router = Arc::clone(&router);
async move { async move {
let server = event.server;
let users = router.server_users(server.id).await; let users = router.server_users(server.id).await;
router.gateway.send_to_users( router.gateway.send_to_users(
users, users,
@@ -345,9 +347,10 @@ impl RealtimeRouter {
}); });
let router = Arc::clone(self); let router = Arc::clone(self);
event_bus.on_async::<server::Model, _, _>("server_updated", move |server| { event_bus.on_async::<ServerUpdatedEvent>(move |event| {
let router = Arc::clone(&router); let router = Arc::clone(&router);
async move { async move {
let server = event.server;
let users = router.server_users(server.id).await; let users = router.server_users(server.id).await;
router.gateway.send_to_users( router.gateway.send_to_users(
users, users,
@@ -359,22 +362,17 @@ impl RealtimeRouter {
}); });
let router = Arc::clone(self); let router = Arc::clone(self);
event_bus.on_async::<(server::Model, Vec<Uuid>), _, _>( event_bus.on_async::<ServerDeletedEvent>(move |event| {
"server_deleted",
move |(server, users)| {
let router = Arc::clone(&router); let router = Arc::clone(&router);
async move { async move {
router router
.gateway .gateway
.send_to_users(users, "Server", "remove", server.id); .send_to_users(event.audience, "Server", "remove", event.server.id);
} }
}, });
);
let router = Arc::clone(self); let router = Arc::clone(self);
event_bus.on_async::<ServerTreeInvalidatedEvent, _, _>( event_bus.on_async::<ServerTreeInvalidatedEvent>(move |event| {
"server_tree_invalidated",
move |event| {
let router = Arc::clone(&router); let router = Arc::clone(&router);
async move { async move {
let users = match event.user_ids { let users = match event.user_ids {
@@ -385,11 +383,10 @@ impl RealtimeRouter {
.gateway .gateway
.send_to_users(users, "ServerTree", "refresh", event.server_id); .send_to_users(users, "ServerTree", "refresh", event.server_id);
} }
}, });
);
let router = Arc::clone(self); let router = Arc::clone(self);
event_bus.on_async::<EmojiCreatedEvent, _, _>("emoji_created", move |event| { event_bus.on_async::<EmojiCreatedEvent>(move |event| {
let router = Arc::clone(&router); let router = Arc::clone(&router);
async move { async move {
let users = router.emoji_users(event.emoji.server_id).await; let users = router.emoji_users(event.emoji.server_id).await;
@@ -403,7 +400,7 @@ impl RealtimeRouter {
}); });
let router = Arc::clone(self); let router = Arc::clone(self);
event_bus.on_async::<EmojiUpdatedEvent, _, _>("emoji_updated", move |event| { event_bus.on_async::<EmojiUpdatedEvent>(move |event| {
let router = Arc::clone(&router); let router = Arc::clone(&router);
async move { async move {
let mut users = router.emoji_users(event.emoji.server_id).await; let mut users = router.emoji_users(event.emoji.server_id).await;
@@ -418,7 +415,7 @@ impl RealtimeRouter {
}); });
let router = Arc::clone(self); let router = Arc::clone(self);
event_bus.on_async::<EmojiDeletedEvent, _, _>("emoji_deleted", move |event| { event_bus.on_async::<EmojiDeletedEvent>(move |event| {
let router = Arc::clone(&router); let router = Arc::clone(&router);
async move { async move {
let users = router.emoji_users(event.emoji.server_id).await; let users = router.emoji_users(event.emoji.server_id).await;
+30 -16
View File
@@ -1,8 +1,8 @@
use crate::core::state::AppState; use crate::core::state::AppState;
use crate::domain::dto::server::{ use crate::domain::dto::server::{
CreateServerRequest, JoinServerRequest, ServerResponse, ServerRolePermissionResponse, CreateServerRequest, JoinServerRequest, ServerResponse, ServerRolePermissionResponse,
ServerTreeResponse, ServerTreeResponse, ServerUserPermissionResponse, SetServerPermissionRequest,
ServerUserPermissionResponse, SetServerPermissionRequest, UpdateServerRequest, UpdateServerRequest,
}; };
use crate::http::context::{CurrentUser, Superuser}; use crate::http::context::{CurrentUser, Superuser};
use crate::http::error::HTTPError; use crate::http::error::HTTPError;
@@ -336,10 +336,6 @@ pub async fn set_user_permission(
.server .server
.set_user_permission(server_id, user_id, payload.permissions) .set_user_permission(server_id, user_id, payload.permissions)
.await?; .await?;
state
.event_bus
.emit("server_user_permission_updated", (server_id, user_id));
let permission = state let permission = state
.repositories .repositories
.server .server
@@ -347,6 +343,13 @@ pub async fn set_user_permission(
.await? .await?
.ok_or(HTTPError::NotFound)?; .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))) Ok(Json(mapper::server_user_permission_to_response(permission)))
} }
@@ -386,9 +389,13 @@ pub async fn remove_user_permission(
.server .server
.remove_user_permission(server_id, user_id) .remove_user_permission(server_id, user_id)
.await?; .await?;
state state.event_bus.emit(
.event_bus crate::domain::events::server_permission::ServerUserPermissionUpdatedEvent {
.emit("server_user_permission_updated", (server_id, user_id)); server_id,
user_id,
permission: None,
},
);
Ok(StatusCode::NO_CONTENT) Ok(StatusCode::NO_CONTENT)
} }
@@ -449,10 +456,6 @@ pub async fn set_role_permission(
.server .server
.set_role_permission(server_id, role_id, payload.permissions) .set_role_permission(server_id, role_id, payload.permissions)
.await?; .await?;
state
.event_bus
.emit("server_role_permission_updated", (role_id, server_id));
let permission = state let permission = state
.repositories .repositories
.server .server
@@ -460,6 +463,13 @@ pub async fn set_role_permission(
.await? .await?
.ok_or(HTTPError::NotFound)?; .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))) Ok(Json(mapper::server_role_permission_to_response(permission)))
} }
@@ -499,9 +509,13 @@ pub async fn remove_role_permission(
.server .server
.remove_role_permission(server_id, role_id) .remove_role_permission(server_id, role_id)
.await?; .await?;
state state.event_bus.emit(
.event_bus crate::domain::events::server_permission::ServerRolePermissionUpdatedEvent {
.emit("server_role_permission_updated", (role_id, server_id)); server_id,
role_id,
permission: None,
},
);
Ok(StatusCode::NO_CONTENT) Ok(StatusCode::NO_CONTENT)
} }
+2 -5
View File
@@ -28,13 +28,10 @@ pub async fn reorder(
.ok_or(HTTPError::NotFound)?; .ok_or(HTTPError::NotFound)?;
state.services.server_order.reorder(payload).await?; state.services.server_order.reorder(payload).await?;
state.event_bus.emit( state.event_bus.emit(ServerTreeInvalidatedEvent {
"server_tree_invalidated",
ServerTreeInvalidatedEvent {
server_id, server_id,
user_ids: None, user_ids: None,
}, });
);
Ok(StatusCode::NO_CONTENT) Ok(StatusCode::NO_CONTENT)
} }
+10 -3
View File
@@ -1,3 +1,6 @@
use crate::domain::events::category::{
CategoryCreatedEvent, CategoryDeletedEvent, CategoryUpdatedEvent,
};
use crate::models::category; use crate::models::category;
use crate::models::server_item_order::OrderedResourceType; use crate::models::server_item_order::OrderedResourceType;
use crate::services::ServicesContext; use crate::services::ServicesContext;
@@ -49,7 +52,9 @@ impl CategoryService {
}) })
.await?; .await?;
event_bus.emit("category_created", cat.clone()); event_bus.emit(CategoryCreatedEvent {
category: cat.clone(),
});
Ok(cat) Ok(cat)
} }
@@ -76,7 +81,9 @@ impl CategoryService {
txn.commit().await?; txn.commit().await?;
event_bus.emit("category_updated", cat.clone()); event_bus.emit(CategoryUpdatedEvent {
category: cat.clone(),
});
Ok(cat) Ok(cat)
} }
@@ -107,7 +114,7 @@ impl CategoryService {
txn.commit().await?; txn.commit().await?;
if deleted { if deleted {
event_bus.emit("category_deleted", existing); event_bus.emit(CategoryDeletedEvent { category: existing });
} }
Ok(deleted) Ok(deleted)
+41 -18
View File
@@ -2,6 +2,10 @@ use crate::domain::dto::channel::{CreateChannelRequest, UpdateChannelRequest};
use crate::domain::events::channel::{ use crate::domain::events::channel::{
ChannelCreatedEvent, ChannelDeletedEvent, ChannelUpdatedEvent, ChannelCreatedEvent, ChannelDeletedEvent, ChannelUpdatedEvent,
}; };
use crate::domain::events::channel_permission::{
ChannelRolePermissionUpdatedEvent, ChannelUserPermissionCreatedEvent,
ChannelUserPermissionDeletedEvent,
};
use crate::models::server_item_order::OrderedResourceType; use crate::models::server_item_order::OrderedResourceType;
use crate::models::{channel, role}; use crate::models::{channel, role};
use crate::permissions::PermissionSet; use crate::permissions::PermissionSet;
@@ -91,12 +95,9 @@ impl ChannelService {
.await?; .await?;
// Post-commit event emission // Post-commit event emission
event_bus.emit( event_bus.emit(ChannelCreatedEvent {
"channel_created",
ChannelCreatedEvent {
channel: channel.clone(), channel: channel.clone(),
}, });
);
Ok(channel) Ok(channel)
} }
@@ -141,13 +142,10 @@ impl ChannelService {
txn.commit().await?; txn.commit().await?;
event_bus.emit( event_bus.emit(ChannelUpdatedEvent {
"channel_updated",
ChannelUpdatedEvent {
previous, previous,
channel: channel.clone(), channel: channel.clone(),
}, });
);
Ok(channel) Ok(channel)
} }
@@ -178,7 +176,7 @@ impl ChannelService {
txn.commit().await?; txn.commit().await?;
if deleted { if deleted {
event_bus.emit("channel_deleted", ChannelDeletedEvent { channel: existing }); event_bus.emit(ChannelDeletedEvent { channel: existing });
} }
Ok(deleted) Ok(deleted)
@@ -214,12 +212,15 @@ impl ChannelService {
.exec(&txn) .exec(&txn)
.await?; .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?; txn.commit().await?;
event_bus.emit( event_bus.emit(ChannelUserPermissionCreatedEvent { permission });
"channel_user_permission_created",
(channel_id, user_id, permissions),
);
Ok(()) Ok(())
} }
@@ -234,6 +235,11 @@ impl ChannelService {
let txn = db.begin().await?; 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() 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::ChannelId.eq(channel_id))
.filter(crate::models::channel_user_permission::Column::UserId.eq(user_id)) .filter(crate::models::channel_user_permission::Column::UserId.eq(user_id))
@@ -242,7 +248,9 @@ impl ChannelService {
txn.commit().await?; 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(()) Ok(())
} }
@@ -285,9 +293,19 @@ impl ChannelService {
.exec(&txn) .exec(&txn)
.await?; .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?; 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(()) Ok(())
} }
@@ -318,7 +336,12 @@ impl ChannelService {
txn.commit().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: None,
});
Ok(()) Ok(())
} }
+8 -44
View File
@@ -3,7 +3,6 @@ use crate::domain::events::message::{
}; };
use crate::models::{attachment, channel, message}; use crate::models::{attachment, channel, message};
use crate::services::ServicesContext; use crate::services::ServicesContext;
use event_bus::Scope;
use sea_orm::{ use sea_orm::{
ActiveModelTrait, ColumnTrait, EntityTrait, QueryFilter, QuerySelect, Set, TransactionTrait, ActiveModelTrait, ColumnTrait, EntityTrait, QueryFilter, QuerySelect, Set, TransactionTrait,
}; };
@@ -41,9 +40,6 @@ impl MessageService {
txn.commit().await?; txn.commit().await?;
let mut scopes: Vec<Scope> = Vec::new();
scopes.push(Scope::uuid("channel", msg.channel_id));
let server_id: Option<Uuid> = channel::Entity::find_by_id(msg.channel_id) let server_id: Option<Uuid> = channel::Entity::find_by_id(msg.channel_id)
.select_only() .select_only()
.column(channel::Column::ServerId) .column(channel::Column::ServerId)
@@ -52,19 +48,11 @@ impl MessageService {
.await? .await?
.flatten(); .flatten();
if let Some(server_id) = server_id { event_bus.emit(MessageCreatedEvent {
scopes.push(Scope::uuid("server", server_id));
}
event_bus.emit_scoped(
"message_created",
scopes,
MessageCreatedEvent {
server_id, server_id,
channel_id: msg.channel_id, channel_id: msg.channel_id,
message: msg.clone(), message: msg.clone(),
}, });
);
Ok(msg) Ok(msg)
} }
@@ -119,19 +107,11 @@ impl MessageService {
.one(db) .one(db)
.await? .await?
.flatten(); .flatten();
let mut scopes = vec![Scope::uuid("channel", msg.channel_id)]; event_bus.emit(MessageCreatedEvent {
if let Some(server_id) = server_id {
scopes.push(Scope::uuid("server", server_id));
}
event_bus.emit_scoped(
"message_created",
scopes,
MessageCreatedEvent {
server_id, server_id,
channel_id: msg.channel_id, channel_id: msg.channel_id,
message: msg.clone(), message: msg.clone(),
}, });
);
Ok(msg) Ok(msg)
} }
@@ -165,19 +145,11 @@ impl MessageService {
.await? .await?
.flatten(); .flatten();
let mut scopes = vec![Scope::uuid("channel", msg.channel_id)]; event_bus.emit(MessageUpdatedEvent {
if let Some(server_id) = server_id {
scopes.push(Scope::uuid("server", server_id));
}
event_bus.emit_scoped(
"message_updated",
scopes,
MessageUpdatedEvent {
server_id, server_id,
channel_id: msg.channel_id, channel_id: msg.channel_id,
message: msg.clone(), message: msg.clone(),
}, });
);
Ok(msg) Ok(msg)
} }
@@ -208,19 +180,11 @@ impl MessageService {
txn.commit().await?; txn.commit().await?;
if deleted { if deleted {
let mut scopes = vec![Scope::uuid("channel", existing.channel_id)]; event_bus.emit(MessageDeletedEvent {
if let Some(server_id) = server_id {
scopes.push(Scope::uuid("server", server_id));
}
event_bus.emit_scoped(
"message_deleted",
scopes,
MessageDeletedEvent {
server_id, server_id,
channel_id: existing.channel_id, channel_id: existing.channel_id,
message: existing, message: existing,
}, });
);
} }
Ok(deleted) Ok(deleted)
+4 -21
View File
@@ -3,7 +3,6 @@ use crate::domain::events::message::{MessageReactionAddedEvent, MessageReactionR
use crate::http::error::HTTPError; use crate::http::error::HTTPError;
use crate::models::{emoji, message_reaction}; use crate::models::{emoji, message_reaction};
use crate::services::ServicesContext; use crate::services::ServicesContext;
use event_bus::Scope;
use sea_orm::{ColumnTrait, EntityTrait, QueryFilter, Set}; use sea_orm::{ColumnTrait, EntityTrait, QueryFilter, Set};
use std::collections::HashMap; use std::collections::HashMap;
use std::sync::Arc; use std::sync::Arc;
@@ -117,19 +116,11 @@ impl MessageReactionService {
}) })
.await?; .await?;
let mut scopes = vec![Scope::uuid("channel", message.channel_id)]; self.context.event_bus.emit(MessageReactionAddedEvent {
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, server_id,
channel_id: message.channel_id, channel_id: message.channel_id,
reaction: reaction.clone(), reaction: reaction.clone(),
}, });
);
Ok((reaction, true)) Ok((reaction, true))
} }
@@ -151,19 +142,11 @@ impl MessageReactionService {
.await? .await?
.ok_or(HTTPError::NotFound)?; .ok_or(HTTPError::NotFound)?;
let mut scopes = vec![Scope::uuid("channel", message.channel_id)]; self.context.event_bus.emit(MessageReactionRemovedEvent {
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, server_id,
channel_id: message.channel_id, channel_id: message.channel_id,
reaction: reaction.clone(), reaction: reaction.clone(),
}, });
);
Ok(reaction) Ok(reaction)
} }
+69 -46
View File
@@ -1,6 +1,16 @@
use crate::domain::events::channel::{ChannelCreatedEvent, ChannelDeletedEvent}; 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::domain::events::server_tree::ServerTreeInvalidatedEvent;
use crate::models::server;
use crate::repositories::Repositories; use crate::repositories::Repositories;
use crate::services::ServicesContext; use crate::services::ServicesContext;
use event_bus::EventBus; use event_bus::EventBus;
@@ -60,13 +70,10 @@ pub struct PermissionSyncService {
impl PermissionSyncService { impl PermissionSyncService {
fn invalidate_tree(event_bus: &Arc<EventBus>, server_id: Uuid, user_ids: Option<Vec<Uuid>>) { fn invalidate_tree(event_bus: &Arc<EventBus>, server_id: Uuid, user_ids: Option<Vec<Uuid>>) {
event_bus.emit( event_bus.emit(ServerTreeInvalidatedEvent {
"server_tree_invalidated",
ServerTreeInvalidatedEvent {
server_id, server_id,
user_ids, user_ids,
}, });
);
} }
pub fn new(service_context: Arc<ServicesContext>) -> Self { pub fn new(service_context: Arc<ServicesContext>) -> Self {
@@ -83,18 +90,18 @@ impl PermissionSyncService {
// Événements Serveur & Membres Serveur // Événements Serveur & Membres Serveur
// --------------------------------------------------------------------- // ---------------------------------------------------------------------
let notify = event_bus.clone(); let notify = event_bus.clone();
event_bus.on_async_with( event_bus.on_async_with::<ServerCreatedEvent, _>(
"server_created",
repositories.clone(), repositories.clone(),
move |repositories, server: server::Model| async move { move |repositories, event| async move {
Self::sync_server(repositories, server.id).await; Self::sync_server(repositories, event.server.id).await;
}, },
); );
event_bus.on_async_with( event_bus.on_async_with::<ServerUserCreatedEvent, _>(
"server_user_created",
repositories.clone(), 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(); let notify = notify.clone();
async move { async move {
Self::sync_user(repositories, user_id, server_id).await; Self::sync_user(repositories, user_id, server_id).await;
@@ -104,10 +111,11 @@ impl PermissionSyncService {
); );
let notify = event_bus.clone(); let notify = event_bus.clone();
event_bus.on_async_with( event_bus.on_async_with::<ServerUserDeletedEvent, _>(
"server_user_deleted",
repositories.clone(), 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(); let notify = notify.clone();
async move { async move {
Self::sync_user(repositories, user_id, server_id).await; Self::sync_user(repositories, user_id, server_id).await;
@@ -121,10 +129,11 @@ impl PermissionSyncService {
// --------------------------------------------------------------------- // ---------------------------------------------------------------------
let notify = event_bus.clone(); let notify = event_bus.clone();
event_bus.on_async_with( event_bus.on_async_with::<RoleUserCreatedEvent, _>(
"role_user_created",
repositories.clone(), 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(); let notify = notify.clone();
async move { async move {
Self::sync_user(repositories, user_id, server_id).await; Self::sync_user(repositories, user_id, server_id).await;
@@ -134,10 +143,11 @@ impl PermissionSyncService {
); );
let notify = event_bus.clone(); let notify = event_bus.clone();
event_bus.on_async_with( event_bus.on_async_with::<RoleUserDeletedEvent, _>(
"role_user_deleted",
repositories.clone(), 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(); let notify = notify.clone();
async move { async move {
Self::sync_user(repositories, user_id, server_id).await; Self::sync_user(repositories, user_id, server_id).await;
@@ -151,10 +161,11 @@ impl PermissionSyncService {
// --------------------------------------------------------------------- // ---------------------------------------------------------------------
let notify = event_bus.clone(); let notify = event_bus.clone();
event_bus.on_async_with( event_bus.on_async_with::<ServerRolePermissionUpdatedEvent, _>(
"server_role_permission_updated",
repositories.clone(), 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(); let notify = notify.clone();
async move { async move {
Self::sync_role_members(repositories, role_id, server_id).await; Self::sync_role_members(repositories, role_id, server_id).await;
@@ -164,10 +175,11 @@ impl PermissionSyncService {
); );
let notify = event_bus.clone(); let notify = event_bus.clone();
event_bus.on_async_with( event_bus.on_async_with::<ServerUserPermissionUpdatedEvent, _>(
"server_user_permission_updated",
repositories.clone(), 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(); let notify = notify.clone();
async move { async move {
Self::sync_user(repositories, user_id, server_id).await; Self::sync_user(repositories, user_id, server_id).await;
@@ -181,10 +193,9 @@ impl PermissionSyncService {
// --------------------------------------------------------------------- // ---------------------------------------------------------------------
let notify = event_bus.clone(); let notify = event_bus.clone();
event_bus.on_async_with( event_bus.on_async_with::<ChannelCreatedEvent, _>(
"channel_created",
repositories.clone(), repositories.clone(),
move |repositories, event: ChannelCreatedEvent| { move |repositories, event| {
let notify = notify.clone(); let notify = notify.clone();
async move { async move {
if let Some(server_id) = event.channel.server_id { if let Some(server_id) = event.channel.server_id {
@@ -196,10 +207,9 @@ impl PermissionSyncService {
); );
let notify = event_bus.clone(); let notify = event_bus.clone();
event_bus.on_async_with( event_bus.on_async_with::<ChannelDeletedEvent, _>(
"channel_deleted",
repositories.clone(), repositories.clone(),
move |repositories, event: ChannelDeletedEvent| { move |repositories, event| {
let notify = notify.clone(); let notify = notify.clone();
async move { async move {
if let Some(server_id) = event.channel.server_id { if let Some(server_id) = event.channel.server_id {
@@ -211,10 +221,11 @@ impl PermissionSyncService {
); );
let notify = event_bus.clone(); let notify = event_bus.clone();
event_bus.on_async_with( event_bus.on_async_with::<ChannelRolePermissionUpdatedEvent, _>(
"channel_role_permission_updated",
repositories.clone(), 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(); let notify = notify.clone();
async move { async move {
Self::sync_role_members(repositories, role_id, server_id).await; Self::sync_role_members(repositories, role_id, server_id).await;
@@ -224,23 +235,34 @@ impl PermissionSyncService {
); );
let notify = event_bus.clone(); let notify = event_bus.clone();
event_bus.on_async_with( event_bus.on_async_with::<ChannelUserPermissionUpdatedEvent, _>(
"channel_user_permission_updated",
repositories.clone(), repositories.clone(),
move |repositories, (server_id, user_id): (Uuid, Uuid)| { move |repositories, event| {
let user_id = event.user_id;
let notify = notify.clone(); let notify = notify.clone();
async move { async move {
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::sync_user(repositories, user_id, server_id).await;
Self::invalidate_tree(&notify, server_id, Some(vec![user_id])); Self::invalidate_tree(&notify, server_id, Some(vec![user_id]));
} }
}
}
}, },
); );
let notify = event_bus.clone(); let notify = event_bus.clone();
event_bus.on_async_with( event_bus.on_async_with::<ChannelUserPermissionCreatedEvent, _>(
"channel_user_permission_created",
repositories.clone(), 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(); let notify = notify.clone();
async move { async move {
if let Some(channel) = repositories if let Some(channel) = repositories
@@ -260,10 +282,11 @@ impl PermissionSyncService {
); );
let notify = event_bus.clone(); let notify = event_bus.clone();
event_bus.on_async_with( event_bus.on_async_with::<ChannelUserPermissionDeletedEvent, _>(
"channel_user_permission_deleted",
repositories, 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(); let notify = notify.clone();
async move { async move {
if let Some(channel) = repositories if let Some(channel) = repositories
+17 -12
View File
@@ -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::models::{channel, channel_user, computed_permission::PermissionScopeType};
use crate::permissions::ChannelPermission; use crate::permissions::ChannelPermission;
use crate::repositories::Repositories; use crate::repositories::Repositories;
@@ -204,10 +209,10 @@ impl RealtimeRegistry {
event_bus: Arc<EventBus>, event_bus: Arc<EventBus>,
) { ) {
let registry = Arc::clone(self); let registry = Arc::clone(self);
event_bus.on_async_with( event_bus.on_async_with::<ChannelUserPermissionUpdatedEvent, _>(
"channel_user_permission_updated",
repositories.clone(), 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(&registry); let registry = Arc::clone(&registry);
async move { async move {
if let Err(error) = registry.refresh_user(&repositories, user_id).await { if let Err(error) = registry.refresh_user(&repositories, user_id).await {
@@ -218,10 +223,10 @@ impl RealtimeRegistry {
); );
let registry = Arc::clone(self); let registry = Arc::clone(self);
event_bus.on_async_with( event_bus.on_async_with::<ChannelUserPermissionCreatedEvent, _>(
"channel_user_permission_created",
repositories.clone(), 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(&registry); let registry = Arc::clone(&registry);
async move { async move {
if let Err(error) = registry.refresh_user(&repositories, user_id).await { if let Err(error) = registry.refresh_user(&repositories, user_id).await {
@@ -232,10 +237,10 @@ impl RealtimeRegistry {
); );
let registry = Arc::clone(self); let registry = Arc::clone(self);
event_bus.on_async_with( event_bus.on_async_with::<ChannelUserPermissionDeletedEvent, _>(
"channel_user_permission_deleted",
repositories.clone(), repositories.clone(),
move |repositories, (_channel_id, user_id): (Uuid, Uuid)| { move |repositories, event| {
let user_id = event.permission.user_id;
let registry = Arc::clone(&registry); let registry = Arc::clone(&registry);
async move { async move {
if let Err(error) = registry.refresh_user(&repositories, user_id).await { if let Err(error) = registry.refresh_user(&repositories, user_id).await {
@@ -247,10 +252,10 @@ impl RealtimeRegistry {
let registry = Arc::clone(self); let registry = Arc::clone(self);
let repositories = repositories.clone(); let repositories = repositories.clone();
event_bus.on_async_with( event_bus.on_async_with::<ServerUserPermissionUpdatedEvent, _>(
"server_user_permission_updated",
repositories, repositories,
move |repositories, (_server_id, user_id): (Uuid, Uuid)| { move |repositories, event| {
let user_id = event.user_id;
let registry = Arc::clone(&registry); let registry = Arc::clone(&registry);
async move { async move {
if let Err(error) = registry.refresh_user(&repositories, user_id).await { if let Err(error) = registry.refresh_user(&repositories, user_id).await {
+26 -12
View File
@@ -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 crate::services::ServicesContext;
use sea_orm::{ActiveModelTrait, EntityTrait, TransactionTrait}; use sea_orm::{ActiveModelTrait, EntityTrait, TransactionTrait};
use std::sync::Arc; use std::sync::Arc;
@@ -27,7 +31,7 @@ impl RoleService {
txn.commit().await?; txn.commit().await?;
event_bus.emit("role_created", role.clone()); event_bus.emit(RoleCreatedEvent { role: role.clone() });
Ok(role) Ok(role)
} }
@@ -45,7 +49,7 @@ impl RoleService {
txn.commit().await?; txn.commit().await?;
event_bus.emit("role_updated", role.clone()); event_bus.emit(RoleUpdatedEvent { role: role.clone() });
Ok(role) Ok(role)
} }
@@ -56,14 +60,15 @@ impl RoleService {
let txn = db.begin().await?; 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 res = role::Entity::delete_by_id(id).exec(&txn).await?;
let deleted = res.rows_affected > 0; let deleted = res.rows_affected > 0;
txn.commit().await?; txn.commit().await?;
if deleted { if let Some(role) = existing.filter(|_| deleted) {
event_bus.emit("role_deleted", id); event_bus.emit(RoleDeletedEvent { role });
} }
Ok(deleted) Ok(deleted)
@@ -82,9 +87,14 @@ impl RoleService {
.add_member(role_id, user_id) .add_member(role_id, user_id)
.await?; .await?;
if added { if added {
self.service_context let role_user = role_user::Entity::find_by_id((role_id, user_id))
.event_bus .one(&self.service_context.repositories.server.context.db)
.emit("role_user_created", (role_id, user_id, server_id)); .await?
.ok_or_else(|| anyhow::anyhow!("Role membership not found"))?;
self.service_context.event_bus.emit(RoleUserCreatedEvent {
role_user,
server_id,
});
} }
Ok(added) Ok(added)
} }
@@ -95,16 +105,20 @@ impl RoleService {
user_id: Uuid, user_id: Uuid,
server_id: Uuid, server_id: Uuid,
) -> Result<bool, anyhow::Error> { ) -> Result<bool, anyhow::Error> {
let existing = role_user::Entity::find_by_id((role_id, user_id))
.one(&self.service_context.repositories.server.context.db)
.await?;
let removed = self let removed = self
.service_context .service_context
.repositories .repositories
.role .role
.remove_member(role_id, user_id) .remove_member(role_id, user_id)
.await?; .await?;
if removed { if let Some(role_user) = existing.filter(|_| removed) {
self.service_context self.service_context.event_bus.emit(RoleUserDeletedEvent {
.event_bus role_user,
.emit("role_user_deleted", (role_id, user_id, server_id)); server_id,
});
} }
Ok(removed) Ok(removed)
} }
+29 -9
View File
@@ -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::models::{role, role_user, server, server_user};
use crate::services::ServicesContext; use crate::services::ServicesContext;
use sea_orm::{ use sea_orm::{
@@ -63,7 +67,9 @@ impl ServerService {
txn.commit().await?; txn.commit().await?;
event_bus.emit("server_created", srv.clone()); event_bus.emit(ServerCreatedEvent {
server: srv.clone(),
});
Ok(srv) Ok(srv)
} }
@@ -97,7 +103,7 @@ impl ServerService {
return Ok(srv); return Ok(srv);
} }
server_user::ActiveModel { let membership = server_user::ActiveModel {
server_id: Set(server_id), server_id: Set(server_id),
user_id: Set(user_id), user_id: Set(user_id),
..Default::default() ..Default::default()
@@ -119,7 +125,9 @@ impl ServerService {
.await?; .await?;
txn.commit().await?; txn.commit().await?;
event_bus.emit("server_user_created", (server_id, user_id)); event_bus.emit(ServerUserCreatedEvent {
server_user: membership,
});
self.service_context self.service_context
.repositories .repositories
.read_state .read_state
@@ -152,7 +160,9 @@ impl ServerService {
txn.commit().await?; txn.commit().await?;
event_bus.emit("server_updated", srv.clone()); event_bus.emit(ServerUpdatedEvent {
server: srv.clone(),
});
Ok(srv) Ok(srv)
} }
@@ -182,7 +192,10 @@ impl ServerService {
txn.commit().await?; txn.commit().await?;
if deleted { if deleted {
event_bus.emit("server_deleted", (existing, audience)); event_bus.emit(ServerDeletedEvent {
server: existing,
audience,
});
} }
Ok(deleted) Ok(deleted)
@@ -194,7 +207,7 @@ impl ServerService {
let txn = db.begin().await?; let txn = db.begin().await?;
crate::models::server_user::ActiveModel { let membership = crate::models::server_user::ActiveModel {
server_id: Set(server_id), server_id: Set(server_id),
user_id: Set(user_id), user_id: Set(user_id),
..Default::default() ..Default::default()
@@ -214,7 +227,9 @@ impl ServerService {
txn.commit().await?; txn.commit().await?;
event_bus.emit("server_user_created", (server_id, user_id)); event_bus.emit(ServerUserCreatedEvent {
server_user: membership,
});
Ok(true) Ok(true)
} }
@@ -225,6 +240,11 @@ impl ServerService {
let txn = db.begin().await?; 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() 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::ServerId.eq(server_id))
.filter(crate::models::server_user::Column::UserId.eq(user_id)) .filter(crate::models::server_user::Column::UserId.eq(user_id))
@@ -235,8 +255,8 @@ impl ServerService {
txn.commit().await?; txn.commit().await?;
if deleted { if let Some(server_user) = membership.filter(|_| deleted) {
event_bus.emit("server_user_deleted", (server_id, user_id)); event_bus.emit(ServerUserDeletedEvent { server_user });
} }
Ok(deleted) Ok(deleted)
+9 -5
View File
@@ -1,4 +1,7 @@
use crate::auth::password; use crate::auth::password;
use crate::domain::events::user::{
UserChangedEvent, UserCreatedEvent, UserDeletedEvent, UserUpdatedEvent,
};
use crate::models::{role, user}; use crate::models::{role, user};
use crate::services::ServicesContext; use crate::services::ServicesContext;
use sea_orm::{ use sea_orm::{
@@ -30,7 +33,7 @@ impl UserService {
txn.commit().await?; txn.commit().await?;
event_bus.emit("user_created", usr.clone()); event_bus.emit(UserCreatedEvent { user: usr.clone() });
Ok(usr) Ok(usr)
} }
@@ -48,7 +51,7 @@ impl UserService {
txn.commit().await?; txn.commit().await?;
event_bus.emit("user_updated", usr.clone()); event_bus.emit(UserUpdatedEvent { user: usr.clone() });
Ok(usr) Ok(usr)
} }
@@ -79,7 +82,7 @@ impl UserService {
txn.commit().await?; txn.commit().await?;
event_bus.emit("user_changed", usr); event_bus.emit(UserChangedEvent { user: usr });
Ok(()) Ok(())
} }
@@ -90,14 +93,15 @@ impl UserService {
let txn = db.begin().await?; 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 res = user::Entity::delete_by_id(id).exec(&txn).await?;
let deleted = res.rows_affected > 0; let deleted = res.rows_affected > 0;
txn.commit().await?; txn.commit().await?;
if deleted { if let Some(user) = existing.filter(|_| deleted) {
event_bus.emit("user_deleted", id); event_bus.emit(UserDeletedEvent { user });
} }
Ok(deleted) Ok(deleted)