add event_bus_typed
This commit is contained in:
@@ -0,0 +1 @@
|
||||
/target
|
||||
@@ -16,8 +16,8 @@ harness = false
|
||||
tokio = { version = "1.53.1", default-features = false, features = ["rt", "sync"] }
|
||||
parking_lot = "0.12.5"
|
||||
tracing = "0.1"
|
||||
uuid = { version = "1.26.1", features = ["v4"] }
|
||||
|
||||
[dev-dependencies]
|
||||
tokio = { version = "1.53.1", default-features = false, features = ["rt", "rt-multi-thread", "macros", "time", "sync"] }
|
||||
criterion = { version = "0.8.2", features = ["async_tokio"] }
|
||||
uuid = { version = "1.26.1", features = ["v4"] }
|
||||
|
||||
@@ -0,0 +1,148 @@
|
||||
# event_bus
|
||||
|
||||
Un bus d'événements asynchrone en mémoire pour Tokio, entièrement basé sur le typage fort en Rust (`TypeId`).
|
||||
|
||||
---
|
||||
|
||||
## 🎯 Problématique résolue
|
||||
|
||||
Dans l'implémentation initiale (`event_bus`), le modèle était inspiré de JavaScript (topics basés sur des chaînes de caractères) :
|
||||
- Les événements étaient transportés via un pointeur générique `AnyEvent` (`Arc<dyn Any + Send + Sync>`).
|
||||
- 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.
|
||||
|
||||
**`event_bus` résout entièrement cette complexité :**
|
||||
- **Typage fort natif** : le routage est directement effectué par l'identifiant de type (`std::any::TypeId`), sans nom de topic requis.
|
||||
- **Zéro downcast / déréférencement à la réception** : le callback reçoit directement la structure d'événement typée.
|
||||
- **Syntaxe ergonomique** : support complet de la syntaxe turbofish demandée `bus.on_async::<MessageUpdatedEvent>(|event| async move { ... })` ainsi que de l'inférence automatique `bus.on_async(|event: MessageUpdatedEvent| async move { ... })`.
|
||||
- **Source unique de vérité** : plus besoin de `Scope` externe ; le contexte (ex: `channel_id`, `server_id`, `caller_id`) est directement transporté dans les champs de la structure typée.
|
||||
- **Targeted wake-up** : canaux Tokio `broadcast` isolés par type d'événement, garantissant des performances optimales sans réveil inutile de tâches.
|
||||
|
||||
---
|
||||
|
||||
## 🚀 Utilisation
|
||||
|
||||
### 1. Définir des événements
|
||||
|
||||
N'importe quelle structure Rust implémentant `Clone + Send + Sync + 'static` est automatiquement un `Event` valide (aucun macro derive supplémentaire nécessaire) :
|
||||
|
||||
```rust
|
||||
use uuid::Uuid;
|
||||
|
||||
#[derive(Clone, Debug)]
|
||||
pub struct MessageCreatedEvent {
|
||||
pub server_id: Option<Uuid>,
|
||||
pub channel_id: Uuid,
|
||||
pub content: String,
|
||||
}
|
||||
|
||||
#[derive(Clone, Debug)]
|
||||
pub struct MessageUpdatedEvent {
|
||||
pub id: u64,
|
||||
pub content: String,
|
||||
}
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
### 2. Émission d'événements
|
||||
|
||||
```rust
|
||||
use event_bus::EventBus;
|
||||
|
||||
let bus = EventBus::new();
|
||||
|
||||
// Émission typée directe
|
||||
bus.emit(MessageUpdatedEvent {
|
||||
id: 42,
|
||||
content: "Nouveau message".into(),
|
||||
});
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
### 3. Réception asynchrone (`on_async`)
|
||||
|
||||
Syntaxe turbofish exacte demandée :
|
||||
|
||||
```rust
|
||||
bus.on_async::<MessageUpdatedEvent>(|event| async move {
|
||||
// `event` est directement de type MessageUpdatedEvent
|
||||
println!("Message {} mis à jour : {}", event.id, event.content);
|
||||
});
|
||||
```
|
||||
|
||||
Ou avec inférence sur l'argument de fermeture :
|
||||
|
||||
```rust
|
||||
bus.on_async(|event: MessageUpdatedEvent| async move {
|
||||
println!("Contenu : {}", event.content);
|
||||
});
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
### 4. Réception synchrone (`on`)
|
||||
|
||||
```rust
|
||||
bus.on::<MessageCreatedEvent>(|event| {
|
||||
println!("Nouveau message créé sur le salon : {:?}", event.channel_id);
|
||||
});
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
### 5. Utilisation avec contexte injecté (`on_async_with`)
|
||||
|
||||
Pratique pour passer des services ou repositories sans clones manuels répétés :
|
||||
|
||||
```rust
|
||||
bus.on_async_with::<MessageCreatedEvent, _>(router, |router, event| async move {
|
||||
router.gateway.send(...);
|
||||
});
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
### 6. Écouteurs One-Shot (`wait_next` et `wait_for`)
|
||||
|
||||
Permet d'attendre un événement de manière linéaire avec une `Future` (sans boucle manuelle ni fuite de souscription) :
|
||||
|
||||
```rust
|
||||
// Attend le tout prochain événement de ce type
|
||||
let event = bus.wait_next::<MessageCreatedEvent>().await?;
|
||||
|
||||
// Ou attend un événement répondant à une condition précise
|
||||
let confirmed = bus.wait_for::<MessageSavedEvent>(|e| e.id == target_id).await?;
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
### 7. Flux direct / Récepteur sans callback (`subscribe`)
|
||||
|
||||
Si vous préférez consommer les événements dans votre propre boucle de streaming :
|
||||
|
||||
```rust
|
||||
let mut rx = bus.subscribe::<MessageUpdatedEvent>();
|
||||
|
||||
tokio::spawn(async move {
|
||||
while let Ok(event) = rx.recv().await {
|
||||
// `event` est directement MessageUpdatedEvent, aucun `match_event!` requis !
|
||||
println!("Reçu : {}", event.content);
|
||||
}
|
||||
});
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 🧪 Tests et Benchmarks
|
||||
|
||||
Exécuter les tests du crate :
|
||||
```bash
|
||||
cargo test --manifest-path event_bus/Cargo.toml
|
||||
```
|
||||
|
||||
Exécuter les benchmarks Criterion :
|
||||
```bash
|
||||
cargo bench --manifest-path event_bus/Cargo.toml
|
||||
```
|
||||
@@ -1,3 +1,5 @@
|
||||
#![allow(dead_code)]
|
||||
|
||||
use std::sync::{
|
||||
Arc,
|
||||
atomic::{AtomicU64, Ordering},
|
||||
@@ -8,8 +10,6 @@ use criterion::{Criterion, Throughput, criterion_group, criterion_main};
|
||||
use event_bus::EventBus;
|
||||
use tokio::runtime::Runtime;
|
||||
|
||||
const TOPIC: &str = "bench-topic";
|
||||
|
||||
#[derive(Clone)]
|
||||
struct SmallEvent {
|
||||
value: u64,
|
||||
@@ -38,57 +38,46 @@ fn runtime() -> Runtime {
|
||||
Runtime::new().expect("failed to create tokio runtime")
|
||||
}
|
||||
|
||||
fn wait_until_received(
|
||||
received: &AtomicU64,
|
||||
expected: u64,
|
||||
) -> impl std::future::Future<Output = ()> + '_ {
|
||||
async move {
|
||||
while received.load(Ordering::Relaxed) < expected {
|
||||
tokio::task::yield_now().await;
|
||||
}
|
||||
async fn wait_until_received(received: &AtomicU64, expected: u64) {
|
||||
while received.load(Ordering::Relaxed) < expected {
|
||||
tokio::task::yield_now().await;
|
||||
}
|
||||
}
|
||||
|
||||
fn bench_emit_no_subscriber(c: &mut Criterion) {
|
||||
let mut group = c.benchmark_group("event_bus/no_subscriber");
|
||||
let mut group = c.benchmark_group("event_bus_typed/no_subscriber");
|
||||
group.throughput(Throughput::Elements(1));
|
||||
|
||||
let bus = EventBus::with_capacity(1024);
|
||||
|
||||
group.bench_function("u64", |b| {
|
||||
b.iter(|| {
|
||||
bus.emit(TOPIC, 42_u64);
|
||||
bus.emit(42_u64);
|
||||
});
|
||||
});
|
||||
|
||||
group.bench_function("small_struct", |b| {
|
||||
b.iter(|| {
|
||||
bus.emit(TOPIC, SmallEvent { value: 42 });
|
||||
bus.emit(SmallEvent { value: 42 });
|
||||
});
|
||||
});
|
||||
|
||||
group.bench_function("string_struct", |b| {
|
||||
b.iter(|| {
|
||||
bus.emit(
|
||||
TOPIC,
|
||||
StringEvent {
|
||||
id: 42,
|
||||
name: "Alice".to_string(),
|
||||
message: "hello from benchmark".to_string(),
|
||||
},
|
||||
);
|
||||
bus.emit(StringEvent {
|
||||
id: 42,
|
||||
name: "Alice".to_string(),
|
||||
message: "hello from benchmark".to_string(),
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
group.bench_function("vec_payload_1kb", |b| {
|
||||
b.iter(|| {
|
||||
bus.emit(
|
||||
TOPIC,
|
||||
VecEvent {
|
||||
id: 42,
|
||||
payload: vec![7_u8; 1024],
|
||||
},
|
||||
);
|
||||
bus.emit(VecEvent {
|
||||
id: 42,
|
||||
payload: vec![7_u8; 1024],
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
@@ -96,13 +85,10 @@ fn bench_emit_no_subscriber(c: &mut Criterion) {
|
||||
|
||||
group.bench_function("arc_payload_1kb", |b| {
|
||||
b.iter(|| {
|
||||
bus.emit(
|
||||
TOPIC,
|
||||
ArcPayloadEvent {
|
||||
id: 42,
|
||||
payload: Arc::clone(&shared_payload),
|
||||
},
|
||||
);
|
||||
bus.emit(ArcPayloadEvent {
|
||||
id: 42,
|
||||
payload: Arc::clone(&shared_payload),
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
@@ -112,13 +98,13 @@ fn bench_emit_no_subscriber(c: &mut Criterion) {
|
||||
fn bench_raw_subscriber(c: &mut Criterion) {
|
||||
let rt = runtime();
|
||||
|
||||
let mut group = c.benchmark_group("event_bus/raw_subscriber");
|
||||
let mut group = c.benchmark_group("event_bus_typed/raw_subscriber");
|
||||
group.throughput(Throughput::Elements(1));
|
||||
|
||||
group.bench_function("u64", |b| {
|
||||
b.to_async(&rt).iter_custom(|iters| async move {
|
||||
let bus = Arc::new(EventBus::with_capacity(iters as usize + 1024));
|
||||
let mut rx = bus.on_raw(TOPIC);
|
||||
let bus = EventBus::with_capacity(iters as usize + 1024);
|
||||
let mut rx = bus.subscribe::<u64>();
|
||||
|
||||
let received = Arc::new(AtomicU64::new(0));
|
||||
let receiver_count = Arc::clone(&received);
|
||||
@@ -134,23 +120,21 @@ fn bench_raw_subscriber(c: &mut Criterion) {
|
||||
let start = Instant::now();
|
||||
|
||||
for i in 0..iters {
|
||||
bus.emit(TOPIC, i);
|
||||
bus.emit(i);
|
||||
}
|
||||
|
||||
wait_until_received(&received, iters).await;
|
||||
|
||||
let elapsed = start.elapsed();
|
||||
|
||||
receiver.abort();
|
||||
|
||||
elapsed
|
||||
});
|
||||
});
|
||||
|
||||
group.bench_function("small_struct", |b| {
|
||||
b.to_async(&rt).iter_custom(|iters| async move {
|
||||
let bus = Arc::new(EventBus::with_capacity(iters as usize + 1024));
|
||||
let mut rx = bus.on_raw(TOPIC);
|
||||
let bus = EventBus::with_capacity(iters as usize + 1024);
|
||||
let mut rx = bus.subscribe::<SmallEvent>();
|
||||
|
||||
let received = Arc::new(AtomicU64::new(0));
|
||||
let receiver_count = Arc::clone(&received);
|
||||
@@ -166,23 +150,21 @@ fn bench_raw_subscriber(c: &mut Criterion) {
|
||||
let start = Instant::now();
|
||||
|
||||
for i in 0..iters {
|
||||
bus.emit(TOPIC, SmallEvent { value: i });
|
||||
bus.emit(SmallEvent { value: i });
|
||||
}
|
||||
|
||||
wait_until_received(&received, iters).await;
|
||||
|
||||
let elapsed = start.elapsed();
|
||||
|
||||
receiver.abort();
|
||||
|
||||
elapsed
|
||||
});
|
||||
});
|
||||
|
||||
group.bench_function("string_struct", |b| {
|
||||
b.to_async(&rt).iter_custom(|iters| async move {
|
||||
let bus = Arc::new(EventBus::with_capacity(iters as usize + 1024));
|
||||
let mut rx = bus.on_raw(TOPIC);
|
||||
let bus = EventBus::with_capacity(iters as usize + 1024);
|
||||
let mut rx = bus.subscribe::<StringEvent>();
|
||||
|
||||
let received = Arc::new(AtomicU64::new(0));
|
||||
let receiver_count = Arc::clone(&received);
|
||||
@@ -198,30 +180,25 @@ fn bench_raw_subscriber(c: &mut Criterion) {
|
||||
let start = Instant::now();
|
||||
|
||||
for i in 0..iters {
|
||||
bus.emit(
|
||||
TOPIC,
|
||||
StringEvent {
|
||||
id: i,
|
||||
name: "Alice".to_string(),
|
||||
message: "hello from benchmark".to_string(),
|
||||
},
|
||||
);
|
||||
bus.emit(StringEvent {
|
||||
id: i,
|
||||
name: "Alice".to_string(),
|
||||
message: "hello from benchmark".to_string(),
|
||||
});
|
||||
}
|
||||
|
||||
wait_until_received(&received, iters).await;
|
||||
|
||||
let elapsed = start.elapsed();
|
||||
|
||||
receiver.abort();
|
||||
|
||||
elapsed
|
||||
});
|
||||
});
|
||||
|
||||
group.bench_function("vec_payload_1kb", |b| {
|
||||
b.to_async(&rt).iter_custom(|iters| async move {
|
||||
let bus = Arc::new(EventBus::with_capacity(iters as usize + 1024));
|
||||
let mut rx = bus.on_raw(TOPIC);
|
||||
let bus = EventBus::with_capacity(iters as usize + 1024);
|
||||
let mut rx = bus.subscribe::<VecEvent>();
|
||||
|
||||
let received = Arc::new(AtomicU64::new(0));
|
||||
let receiver_count = Arc::clone(&received);
|
||||
@@ -237,62 +214,56 @@ fn bench_raw_subscriber(c: &mut Criterion) {
|
||||
let start = Instant::now();
|
||||
|
||||
for i in 0..iters {
|
||||
bus.emit(
|
||||
TOPIC,
|
||||
VecEvent {
|
||||
id: i,
|
||||
payload: vec![7_u8; 1024],
|
||||
},
|
||||
);
|
||||
bus.emit(VecEvent {
|
||||
id: i,
|
||||
payload: vec![7_u8; 1024],
|
||||
});
|
||||
}
|
||||
|
||||
wait_until_received(&received, iters).await;
|
||||
|
||||
let elapsed = start.elapsed();
|
||||
|
||||
receiver.abort();
|
||||
|
||||
elapsed
|
||||
});
|
||||
});
|
||||
|
||||
let shared_payload: Arc<[u8]> = Arc::from(vec![7_u8; 1024].into_boxed_slice());
|
||||
|
||||
group.bench_function("arc_payload_1kb", |b| {
|
||||
b.to_async(&rt).iter_custom(|iters| async move {
|
||||
let bus = Arc::new(EventBus::with_capacity(iters as usize + 1024));
|
||||
let mut rx = bus.on_raw(TOPIC);
|
||||
let shared = Arc::clone(&shared_payload);
|
||||
b.to_async(&rt).iter_custom(|iters| {
|
||||
let payload = Arc::clone(&shared);
|
||||
async move {
|
||||
let bus = EventBus::with_capacity(iters as usize + 1024);
|
||||
let mut rx = bus.subscribe::<ArcPayloadEvent>();
|
||||
|
||||
let payload: Arc<[u8]> = Arc::from(vec![7_u8; 1024].into_boxed_slice());
|
||||
let received = Arc::new(AtomicU64::new(0));
|
||||
let receiver_count = Arc::clone(&received);
|
||||
|
||||
let received = Arc::new(AtomicU64::new(0));
|
||||
let receiver_count = Arc::clone(&received);
|
||||
|
||||
let receiver = tokio::spawn(async move {
|
||||
while receiver_count.load(Ordering::Relaxed) < iters {
|
||||
if rx.recv().await.is_ok() {
|
||||
receiver_count.fetch_add(1, Ordering::Relaxed);
|
||||
let receiver = tokio::spawn(async move {
|
||||
while receiver_count.load(Ordering::Relaxed) < iters {
|
||||
if rx.recv().await.is_ok() {
|
||||
receiver_count.fetch_add(1, Ordering::Relaxed);
|
||||
}
|
||||
}
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
let start = Instant::now();
|
||||
let start = Instant::now();
|
||||
|
||||
for i in 0..iters {
|
||||
bus.emit(
|
||||
TOPIC,
|
||||
ArcPayloadEvent {
|
||||
for i in 0..iters {
|
||||
bus.emit(ArcPayloadEvent {
|
||||
id: i,
|
||||
payload: Arc::clone(&payload),
|
||||
},
|
||||
);
|
||||
});
|
||||
}
|
||||
|
||||
wait_until_received(&received, iters).await;
|
||||
|
||||
let elapsed = start.elapsed();
|
||||
receiver.abort();
|
||||
elapsed
|
||||
}
|
||||
|
||||
wait_until_received(&received, iters).await;
|
||||
|
||||
let elapsed = start.elapsed();
|
||||
|
||||
receiver.abort();
|
||||
|
||||
elapsed
|
||||
});
|
||||
});
|
||||
|
||||
@@ -302,17 +273,17 @@ fn bench_raw_subscriber(c: &mut Criterion) {
|
||||
fn bench_typed_callback(c: &mut Criterion) {
|
||||
let rt = runtime();
|
||||
|
||||
let mut group = c.benchmark_group("event_bus/typed_callback");
|
||||
let mut group = c.benchmark_group("event_bus_typed/typed_callback");
|
||||
group.throughput(Throughput::Elements(1));
|
||||
|
||||
group.bench_function("u64", |b| {
|
||||
b.to_async(&rt).iter_custom(|iters| async move {
|
||||
let bus = Arc::new(EventBus::with_capacity(iters as usize + 1024));
|
||||
let bus = EventBus::with_capacity(iters as usize + 1024);
|
||||
|
||||
let received = Arc::new(AtomicU64::new(0));
|
||||
let handler_count = Arc::clone(&received);
|
||||
|
||||
let subscription = bus.on::<u64, _>(TOPIC, move |event| {
|
||||
let subscription = bus.on::<u64>(move |event| {
|
||||
let _ = event;
|
||||
handler_count.fetch_add(1, Ordering::Relaxed);
|
||||
});
|
||||
@@ -320,27 +291,25 @@ fn bench_typed_callback(c: &mut Criterion) {
|
||||
let start = Instant::now();
|
||||
|
||||
for i in 0..iters {
|
||||
bus.emit(TOPIC, i);
|
||||
bus.emit(i);
|
||||
}
|
||||
|
||||
wait_until_received(&received, iters).await;
|
||||
|
||||
let elapsed = start.elapsed();
|
||||
|
||||
subscription.abort();
|
||||
|
||||
elapsed
|
||||
});
|
||||
});
|
||||
|
||||
group.bench_function("small_struct", |b| {
|
||||
b.to_async(&rt).iter_custom(|iters| async move {
|
||||
let bus = Arc::new(EventBus::with_capacity(iters as usize + 1024));
|
||||
let bus = EventBus::with_capacity(iters as usize + 1024);
|
||||
|
||||
let received = Arc::new(AtomicU64::new(0));
|
||||
let handler_count = Arc::clone(&received);
|
||||
|
||||
let subscription = bus.on::<SmallEvent, _>(TOPIC, move |event| {
|
||||
let subscription = bus.on::<SmallEvent>(move |event| {
|
||||
let _ = event.value;
|
||||
handler_count.fetch_add(1, Ordering::Relaxed);
|
||||
});
|
||||
@@ -348,27 +317,25 @@ fn bench_typed_callback(c: &mut Criterion) {
|
||||
let start = Instant::now();
|
||||
|
||||
for i in 0..iters {
|
||||
bus.emit(TOPIC, SmallEvent { value: i });
|
||||
bus.emit(SmallEvent { value: i });
|
||||
}
|
||||
|
||||
wait_until_received(&received, iters).await;
|
||||
|
||||
let elapsed = start.elapsed();
|
||||
|
||||
subscription.abort();
|
||||
|
||||
elapsed
|
||||
});
|
||||
});
|
||||
|
||||
group.bench_function("string_struct", |b| {
|
||||
b.to_async(&rt).iter_custom(|iters| async move {
|
||||
let bus = Arc::new(EventBus::with_capacity(iters as usize + 1024));
|
||||
let bus = EventBus::with_capacity(iters as usize + 1024);
|
||||
|
||||
let received = Arc::new(AtomicU64::new(0));
|
||||
let handler_count = Arc::clone(&received);
|
||||
|
||||
let subscription = bus.on::<StringEvent, _>(TOPIC, move |event| {
|
||||
let subscription = bus.on::<StringEvent>(move |event| {
|
||||
let _ = event.id;
|
||||
let _ = event.name.len();
|
||||
let _ = event.message.len();
|
||||
@@ -378,34 +345,29 @@ fn bench_typed_callback(c: &mut Criterion) {
|
||||
let start = Instant::now();
|
||||
|
||||
for i in 0..iters {
|
||||
bus.emit(
|
||||
TOPIC,
|
||||
StringEvent {
|
||||
id: i,
|
||||
name: "Alice".to_string(),
|
||||
message: "hello from benchmark".to_string(),
|
||||
},
|
||||
);
|
||||
bus.emit(StringEvent {
|
||||
id: i,
|
||||
name: "Alice".to_string(),
|
||||
message: "hello from benchmark".to_string(),
|
||||
});
|
||||
}
|
||||
|
||||
wait_until_received(&received, iters).await;
|
||||
|
||||
let elapsed = start.elapsed();
|
||||
|
||||
subscription.abort();
|
||||
|
||||
elapsed
|
||||
});
|
||||
});
|
||||
|
||||
group.bench_function("vec_payload_1kb", |b| {
|
||||
b.to_async(&rt).iter_custom(|iters| async move {
|
||||
let bus = Arc::new(EventBus::with_capacity(iters as usize + 1024));
|
||||
let bus = EventBus::with_capacity(iters as usize + 1024);
|
||||
|
||||
let received = Arc::new(AtomicU64::new(0));
|
||||
let handler_count = Arc::clone(&received);
|
||||
|
||||
let subscription = bus.on::<VecEvent, _>(TOPIC, move |event| {
|
||||
let subscription = bus.on::<VecEvent>(move |event| {
|
||||
let _ = event.id;
|
||||
let _ = event.payload.len();
|
||||
handler_count.fetch_add(1, Ordering::Relaxed);
|
||||
@@ -414,58 +376,53 @@ fn bench_typed_callback(c: &mut Criterion) {
|
||||
let start = Instant::now();
|
||||
|
||||
for i in 0..iters {
|
||||
bus.emit(
|
||||
TOPIC,
|
||||
VecEvent {
|
||||
id: i,
|
||||
payload: vec![7_u8; 1024],
|
||||
},
|
||||
);
|
||||
bus.emit(VecEvent {
|
||||
id: i,
|
||||
payload: vec![7_u8; 1024],
|
||||
});
|
||||
}
|
||||
|
||||
wait_until_received(&received, iters).await;
|
||||
|
||||
let elapsed = start.elapsed();
|
||||
|
||||
subscription.abort();
|
||||
|
||||
elapsed
|
||||
});
|
||||
});
|
||||
|
||||
let shared_payload: Arc<[u8]> = Arc::from(vec![7_u8; 1024].into_boxed_slice());
|
||||
|
||||
group.bench_function("arc_payload_1kb", |b| {
|
||||
b.to_async(&rt).iter_custom(|iters| async move {
|
||||
let bus = Arc::new(EventBus::with_capacity(iters as usize + 1024));
|
||||
let payload: Arc<[u8]> = Arc::from(vec![7_u8; 1024].into_boxed_slice());
|
||||
let shared = Arc::clone(&shared_payload);
|
||||
b.to_async(&rt).iter_custom(|iters| {
|
||||
let payload = Arc::clone(&shared);
|
||||
async move {
|
||||
let bus = EventBus::with_capacity(iters as usize + 1024);
|
||||
|
||||
let received = Arc::new(AtomicU64::new(0));
|
||||
let handler_count = Arc::clone(&received);
|
||||
let received = Arc::new(AtomicU64::new(0));
|
||||
let handler_count = Arc::clone(&received);
|
||||
|
||||
let subscription = bus.on::<ArcPayloadEvent, _>(TOPIC, move |event| {
|
||||
let _ = event.id;
|
||||
let _ = event.payload.len();
|
||||
handler_count.fetch_add(1, Ordering::Relaxed);
|
||||
});
|
||||
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();
|
||||
let start = Instant::now();
|
||||
|
||||
for i in 0..iters {
|
||||
bus.emit(
|
||||
TOPIC,
|
||||
ArcPayloadEvent {
|
||||
for i in 0..iters {
|
||||
bus.emit(ArcPayloadEvent {
|
||||
id: i,
|
||||
payload: Arc::clone(&payload),
|
||||
},
|
||||
);
|
||||
});
|
||||
}
|
||||
|
||||
wait_until_received(&received, iters).await;
|
||||
|
||||
let elapsed = start.elapsed();
|
||||
subscription.abort();
|
||||
elapsed
|
||||
}
|
||||
|
||||
wait_until_received(&received, iters).await;
|
||||
|
||||
let elapsed = start.elapsed();
|
||||
|
||||
subscription.abort();
|
||||
|
||||
elapsed
|
||||
});
|
||||
});
|
||||
|
||||
@@ -475,13 +432,13 @@ fn bench_typed_callback(c: &mut Criterion) {
|
||||
fn bench_multiple_subscribers(c: &mut Criterion) {
|
||||
let rt = runtime();
|
||||
|
||||
let mut group = c.benchmark_group("event_bus/multiple_subscribers");
|
||||
let mut group = c.benchmark_group("event_bus_typed/multiple_subscribers");
|
||||
group.throughput(Throughput::Elements(1));
|
||||
|
||||
for subscriber_count in [1_u64, 2, 4, 8, 16, 32] {
|
||||
group.bench_function(format!("{subscriber_count}_subscribers"), |b| {
|
||||
b.to_async(&rt).iter_custom(|iters| async move {
|
||||
let bus = Arc::new(EventBus::with_capacity(iters as usize + 1024));
|
||||
let bus = EventBus::with_capacity(iters as usize + 1024);
|
||||
|
||||
let expected = iters * subscriber_count;
|
||||
let received = Arc::new(AtomicU64::new(0));
|
||||
@@ -490,7 +447,7 @@ fn bench_multiple_subscribers(c: &mut Criterion) {
|
||||
for _ in 0..subscriber_count {
|
||||
let handler_count = Arc::clone(&received);
|
||||
|
||||
let subscription = bus.on::<SmallEvent, _>(TOPIC, move |event| {
|
||||
let subscription = bus.on::<SmallEvent>(move |event| {
|
||||
let _ = event.value;
|
||||
handler_count.fetch_add(1, Ordering::Relaxed);
|
||||
});
|
||||
@@ -501,7 +458,7 @@ fn bench_multiple_subscribers(c: &mut Criterion) {
|
||||
let start = Instant::now();
|
||||
|
||||
for i in 0..iters {
|
||||
bus.emit(TOPIC, SmallEvent { value: i });
|
||||
bus.emit(SmallEvent { value: i });
|
||||
}
|
||||
|
||||
wait_until_received(&received, expected).await;
|
||||
|
||||
+410
-401
@@ -1,420 +1,90 @@
|
||||
use std::any::Any;
|
||||
use std::future::Future;
|
||||
use std::any::{Any, TypeId};
|
||||
use std::collections::HashMap;
|
||||
use std::fmt::Debug;
|
||||
use std::sync::Arc;
|
||||
|
||||
use parking_lot::RwLock;
|
||||
use std::collections::HashMap;
|
||||
use std::iter;
|
||||
use tokio::sync::broadcast;
|
||||
use tokio::task::JoinHandle;
|
||||
// use tracing::log::kv::{Key, Value};
|
||||
use tracing::{debug, trace, warn};
|
||||
use uuid::Uuid;
|
||||
|
||||
/// Raw event type: an atomic reference-counted pointer to any value.
|
||||
pub type AnyEvent = Arc<dyn Any + Send + Sync>;
|
||||
use crate::handler::{AsyncHandler, AsyncHandlerWith};
|
||||
|
||||
/// Default buffer capacity for each broadcast channel.
|
||||
const DEFAULT_CAPACITY: usize = 64;
|
||||
pub const DEFAULT_CAPACITY: usize = 1024;
|
||||
|
||||
#[derive(Debug, Clone, PartialEq, Eq)]
|
||||
pub enum ScopeValue {
|
||||
String(String),
|
||||
Uuid(Uuid),
|
||||
}
|
||||
impl ScopeValue {
|
||||
fn into_string(self) -> String {
|
||||
match self {
|
||||
Self::String(value) => value,
|
||||
Self::Uuid(value) => value.to_string(),
|
||||
}
|
||||
}
|
||||
}
|
||||
/// Marker trait for events dispatched through [`EventBus`].
|
||||
///
|
||||
/// Any type implementing `Clone + Send + Sync + 'static` automatically
|
||||
/// implements `Event`.
|
||||
pub trait Event: Clone + Send + Sync + 'static {}
|
||||
|
||||
#[derive(Debug, Clone, PartialEq, Eq)]
|
||||
pub struct Scope {
|
||||
pub key: String,
|
||||
pub value: ScopeValue,
|
||||
}
|
||||
impl<T: Clone + Send + Sync + 'static> Event for T {}
|
||||
|
||||
impl Scope {
|
||||
pub fn new(key: impl Into<String>, value: ScopeValue) -> Self {
|
||||
Self {
|
||||
key: key.into(),
|
||||
value,
|
||||
}
|
||||
}
|
||||
|
||||
pub fn uuid(key: impl Into<String>, value: Uuid) -> Self {
|
||||
Self::new(key, ScopeValue::Uuid(value))
|
||||
}
|
||||
|
||||
pub fn string(key: impl Into<String>, value: impl Into<String>) -> Self {
|
||||
Self::new(key, ScopeValue::String(value.into()))
|
||||
}
|
||||
}
|
||||
impl IntoIterator for Scope {
|
||||
type Item = Scope;
|
||||
type IntoIter = iter::Once<Scope>;
|
||||
|
||||
fn into_iter(self) -> Self::IntoIter {
|
||||
iter::once(self)
|
||||
}
|
||||
}
|
||||
|
||||
/// The central event bus.
|
||||
///
|
||||
/// Share it via `Arc<EventBus>` across modules.
|
||||
/// Each topic has its own broadcast channel: only subscribers of the matching
|
||||
/// topic are woken up on `emit` (targeted wake-up).
|
||||
///
|
||||
/// # Minimal example — sync callback
|
||||
/// ```rust,no_run
|
||||
/// use std::sync::Arc;
|
||||
/// use oxspeak_server_lib::event_bus::EventBus;
|
||||
///
|
||||
/// #[derive(Clone, Debug)]
|
||||
/// struct User { name: String }
|
||||
///
|
||||
/// # tokio_test::block_on(async {
|
||||
/// let bus = Arc::new(EventBus::new());
|
||||
///
|
||||
/// bus.on::<User>("user-connected", |user| {
|
||||
/// println!("Connected: {:?}", user);
|
||||
/// });
|
||||
///
|
||||
/// bus.emit("user-connected", User { name: "Alice".into() });
|
||||
/// # tokio::time::sleep(std::time::Duration::from_millis(10)).await;
|
||||
/// # });
|
||||
/// ```
|
||||
///
|
||||
/// # Example — async callback
|
||||
/// ```rust,no_run
|
||||
/// use std::sync::Arc;
|
||||
/// use oxspeak_server_lib::event_bus::EventBus;
|
||||
///
|
||||
/// #[derive(Clone, Debug)]
|
||||
/// struct User { name: String }
|
||||
///
|
||||
/// # tokio_test::block_on(async {
|
||||
/// let bus = Arc::new(EventBus::new());
|
||||
///
|
||||
/// bus.on_async::<User, _, _>("user-connected", |user| async move {
|
||||
/// println!("(async) Connected: {:?}", user);
|
||||
/// });
|
||||
///
|
||||
/// bus.emit("user-connected", User { name: "Bob".into() });
|
||||
/// # tokio::time::sleep(std::time::Duration::from_millis(10)).await;
|
||||
/// # });
|
||||
/// ```
|
||||
#[derive(Debug)]
|
||||
pub struct EventBus {
|
||||
/// Channels indexed by exact topic.
|
||||
channels: RwLock<HashMap<String, broadcast::Sender<AnyEvent>>>,
|
||||
struct EventBusInner {
|
||||
channels: RwLock<HashMap<TypeId, Box<dyn Any + Send + Sync>>>,
|
||||
capacity: usize,
|
||||
}
|
||||
|
||||
impl EventBus {
|
||||
/// Creates a bus with the default capacity (64 messages per channel).
|
||||
pub fn new() -> Self {
|
||||
debug!(
|
||||
"EventBus created with default capacity ({})",
|
||||
DEFAULT_CAPACITY
|
||||
);
|
||||
Self {
|
||||
channels: RwLock::new(HashMap::new()),
|
||||
capacity: DEFAULT_CAPACITY,
|
||||
}
|
||||
}
|
||||
|
||||
/// Creates a bus with a custom buffer capacity.
|
||||
pub fn with_capacity(capacity: usize) -> Self {
|
||||
debug!("EventBus created with capacity {}", capacity);
|
||||
Self {
|
||||
channels: RwLock::new(HashMap::new()),
|
||||
capacity,
|
||||
}
|
||||
}
|
||||
|
||||
// ─────────────────────────────────────────────────────────────────────────
|
||||
// Internal
|
||||
// ─────────────────────────────────────────────────────────────────────────
|
||||
|
||||
fn get_or_create_sender(&self, topic: &str) -> broadcast::Sender<AnyEvent> {
|
||||
{
|
||||
let channels = self.channels.read();
|
||||
if let Some(tx) = channels.get(topic) {
|
||||
return tx.clone();
|
||||
}
|
||||
}
|
||||
let mut channels = self.channels.write();
|
||||
let created = !channels.contains_key(topic);
|
||||
let tx = channels
|
||||
.entry(topic.to_string())
|
||||
.or_insert_with(|| {
|
||||
let (tx, _) = broadcast::channel(self.capacity);
|
||||
tx
|
||||
})
|
||||
.clone();
|
||||
if created {
|
||||
debug!(topic, "New broadcast channel created");
|
||||
}
|
||||
tx
|
||||
}
|
||||
|
||||
// ─────────────────────────────────────────────────────────────────────────
|
||||
// Emission
|
||||
// ─────────────────────────────────────────────────────────────────────────
|
||||
|
||||
/// Emits an event on a topic.
|
||||
///
|
||||
/// - Pushes the event into the exact-topic channel (if subscribers exist).
|
||||
/// - If nobody is listening, the event is silently dropped.
|
||||
///
|
||||
/// # Example
|
||||
/// ```rust,no_run
|
||||
/// # use std::sync::Arc;
|
||||
/// # use oxspeak_server_lib::event_bus::EventBus;
|
||||
/// # #[derive(Clone)] struct User;
|
||||
/// # let bus = Arc::new(EventBus::new());
|
||||
/// bus.emit("user-connected", User);
|
||||
/// bus.emit("user-deleted", uuid::Uuid::new_v4());
|
||||
/// ```
|
||||
pub fn emit<T: Any + Send + Sync + 'static>(&self, topic: &str, event: T) {
|
||||
trace!(topic, "Emitting event");
|
||||
let event: AnyEvent = Arc::new(event);
|
||||
|
||||
self.emit_arc(topic, event);
|
||||
}
|
||||
|
||||
// todo : undocumented...
|
||||
pub fn emit_scoped<T>(&self, topic: &str, scopes: impl IntoIterator<Item = Scope>, event: T)
|
||||
where
|
||||
T: Any + Send + Sync + 'static,
|
||||
{
|
||||
let event: AnyEvent = Arc::new(event);
|
||||
|
||||
// Émission sur le topic général.
|
||||
self.emit_arc(topic, Arc::clone(&event));
|
||||
|
||||
// Émission sur chaque topic scoped.
|
||||
for scope in scopes {
|
||||
let scoped_topic = format!("{}:{}:{}", scope.key, scope.value.into_string(), topic);
|
||||
|
||||
self.emit_arc(&scoped_topic, Arc::clone(&event));
|
||||
}
|
||||
}
|
||||
|
||||
// todo : undocumented...
|
||||
fn emit_arc(&self, topic: &str, event: AnyEvent) {
|
||||
trace!(topic, "Emitting event");
|
||||
|
||||
if let Some(tx) = self.channels.read().get(topic) {
|
||||
let receiver_count = tx.receiver_count();
|
||||
let _ = tx.send(event);
|
||||
|
||||
trace!(topic, receiver_count, "Event delivered to channel");
|
||||
}
|
||||
}
|
||||
|
||||
// ─────────────────────────────────────────────────────────────────────────
|
||||
// Subscription — callbacks (main API)
|
||||
// ─────────────────────────────────────────────────────────────────────────
|
||||
|
||||
/// Subscribes to a topic and calls `handler` on each event of type `T`.
|
||||
///
|
||||
/// The handler runs in a dedicated Tokio task (fire-and-forget).
|
||||
/// Events of a different type are silently ignored.
|
||||
/// Returns a [`JoinHandle`] to cancel the subscription if needed.
|
||||
///
|
||||
/// # Example
|
||||
/// ```rust,no_run
|
||||
/// # use std::sync::Arc;
|
||||
/// # use oxspeak_server_lib::event_bus::EventBus;
|
||||
/// # #[derive(Clone, Debug)] struct User { name: String }
|
||||
/// # let bus = Arc::new(EventBus::new());
|
||||
/// bus.on::<User>("user-connected", |user| {
|
||||
/// println!("Connected: {:?}", user);
|
||||
/// });
|
||||
/// ```
|
||||
pub fn on<T, F>(&self, topic: &str, handler: F) -> JoinHandle<()>
|
||||
where
|
||||
T: Any + Send + Sync + Clone + 'static,
|
||||
F: Fn(T) + Send + Sync + 'static,
|
||||
{
|
||||
let mut rx = self.get_or_create_sender(topic).subscribe();
|
||||
let topic_owned = topic.to_string();
|
||||
|
||||
debug!(topic, "Sync subscriber registered");
|
||||
|
||||
tokio::spawn(async move {
|
||||
loop {
|
||||
match rx.recv().await {
|
||||
Ok(evt) => {
|
||||
if let Some(typed) = evt.downcast_ref::<T>() {
|
||||
trace!(topic = topic_owned, "Sync handler invoked");
|
||||
handler(typed.clone());
|
||||
}
|
||||
}
|
||||
Err(broadcast::error::RecvError::Lagged(n)) => {
|
||||
warn!(
|
||||
topic = topic_owned,
|
||||
skipped = n,
|
||||
"Subscriber lagged, messages dropped"
|
||||
);
|
||||
}
|
||||
Err(broadcast::error::RecvError::Closed) => {
|
||||
debug!(
|
||||
topic = topic_owned,
|
||||
"Channel closed, sync subscriber exiting"
|
||||
);
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
/// Subscribes to a topic and calls an **async** handler on each event of type `T`.
|
||||
///
|
||||
/// Ideal for performing async operations in the handler
|
||||
/// (DB query, HTTP call, WebSocket broadcast, …).
|
||||
/// Returns a [`JoinHandle`] to cancel the subscription if needed.
|
||||
///
|
||||
/// # Example
|
||||
/// ```rust,no_run
|
||||
/// # use std::sync::Arc;
|
||||
/// # use oxspeak_server_lib::event_bus::EventBus;
|
||||
/// # #[derive(Clone, Debug)] struct User { name: String }
|
||||
/// # let bus = Arc::new(EventBus::new());
|
||||
/// bus.on_async::<User, _, _>("user-connected", |user| async move {
|
||||
/// println!("(async) Connected: {:?}", user);
|
||||
/// // async work here: DB query, HTTP, etc.
|
||||
/// });
|
||||
/// ```
|
||||
pub fn on_async<T, F, Fut>(&self, topic: &str, handler: F) -> JoinHandle<()>
|
||||
where
|
||||
T: Any + Send + Sync + Clone + 'static,
|
||||
F: Fn(T) -> Fut + Send + Sync + 'static,
|
||||
Fut: Future<Output = ()> + Send + 'static,
|
||||
{
|
||||
let mut rx = self.get_or_create_sender(topic).subscribe();
|
||||
let topic_owned = topic.to_string();
|
||||
|
||||
debug!(topic, "Async subscriber registered");
|
||||
|
||||
tokio::spawn(async move {
|
||||
loop {
|
||||
match rx.recv().await {
|
||||
Ok(evt) => {
|
||||
if let Some(typed) = evt.downcast_ref::<T>() {
|
||||
trace!(topic = topic_owned, "Async handler invoked");
|
||||
handler(typed.clone()).await;
|
||||
}
|
||||
}
|
||||
Err(broadcast::error::RecvError::Lagged(n)) => {
|
||||
warn!(
|
||||
topic = topic_owned,
|
||||
skipped = n,
|
||||
"Subscriber lagged, messages dropped"
|
||||
);
|
||||
}
|
||||
Err(broadcast::error::RecvError::Closed) => {
|
||||
debug!(
|
||||
topic = topic_owned,
|
||||
"Channel closed, async subscriber exiting"
|
||||
);
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
// todo : Undocumented
|
||||
pub fn on_async_with<T, C, F, Fut>(&self, topic: &str, context: C, handler: F) -> JoinHandle<()>
|
||||
where
|
||||
T: Any + Send + Sync + Clone + 'static,
|
||||
C: Clone + Send + Sync + 'static,
|
||||
F: Fn(C, T) -> Fut + Send + Sync + 'static,
|
||||
Fut: Future<Output = ()> + Send + 'static,
|
||||
{
|
||||
let mut rx = self.get_or_create_sender(topic).subscribe();
|
||||
let topic_owned = topic.to_string();
|
||||
|
||||
debug!(topic, "Async subscriber registered");
|
||||
|
||||
tokio::spawn(async move {
|
||||
loop {
|
||||
match rx.recv().await {
|
||||
Ok(evt) => {
|
||||
if let Some(typed) = evt.downcast_ref::<T>() {
|
||||
trace!(topic = topic_owned, "Async handler invoked");
|
||||
|
||||
handler(context.clone(), typed.clone()).await;
|
||||
}
|
||||
}
|
||||
Err(broadcast::error::RecvError::Lagged(n)) => {
|
||||
warn!(
|
||||
topic = topic_owned,
|
||||
skipped = n,
|
||||
"Subscriber lagged, messages dropped"
|
||||
);
|
||||
}
|
||||
Err(broadcast::error::RecvError::Closed) => {
|
||||
debug!(
|
||||
topic = topic_owned,
|
||||
"Channel closed, async subscriber exiting"
|
||||
);
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
// ─────────────────────────────────────────────────────────────────────────
|
||||
// Subscription — low-level access (advanced use cases)
|
||||
// ─────────────────────────────────────────────────────────────────────────
|
||||
|
||||
/// Returns a raw [`AnyEvent`] receiver to manage the loop yourself.
|
||||
///
|
||||
/// Useful with the [`match_event!`][crate::match_event] macro to handle
|
||||
/// multiple different types on the same topic.
|
||||
///
|
||||
/// # Example
|
||||
/// ```rust,no_run
|
||||
/// # use std::sync::Arc;
|
||||
/// # use oxspeak_server_lib::event_bus::EventBus;
|
||||
/// # use oxspeak_server_lib::match_event;
|
||||
/// # #[derive(Clone, Debug)] struct User { name: String }
|
||||
/// # #[derive(Clone, Debug)] struct UdpMetric { value: f32 }
|
||||
/// # let bus = Arc::new(EventBus::new());
|
||||
/// # tokio_test::block_on(async {
|
||||
/// let mut rx = bus.on_raw("user-connected");
|
||||
/// bus.emit("user-connected", User { name: "Alice".into() });
|
||||
///
|
||||
/// if let Ok(evt) = rx.recv().await {
|
||||
/// match_event!(evt,
|
||||
/// User => |u| println!("User: {:?}", u),
|
||||
/// UdpMetric => |m| println!("Metric: {:?}", m),
|
||||
/// );
|
||||
/// }
|
||||
/// # });
|
||||
/// ```
|
||||
pub fn on_raw(&self, topic: &str) -> broadcast::Receiver<AnyEvent> {
|
||||
debug!(topic, "Raw subscriber registered");
|
||||
self.get_or_create_sender(topic).subscribe()
|
||||
}
|
||||
|
||||
// ─────────────────────────────────────────────────────────────────────────
|
||||
// Utilities
|
||||
// ─────────────────────────────────────────────────────────────────────────
|
||||
|
||||
/// Returns the list of currently registered topics.
|
||||
pub fn topics(&self) -> Vec<String> {
|
||||
self.channels.read().keys().cloned().collect()
|
||||
}
|
||||
/// A central, strongly-typed broadcast event bus.
|
||||
///
|
||||
/// Unlike string/topic-based event buses, [`EventBus`] dispatches events directly
|
||||
/// based on the concrete Rust type of the event (using [`TypeId`]).
|
||||
///
|
||||
/// Under the hood, each event type is backed by an independent [`tokio::sync::broadcast`]
|
||||
/// ring buffer. Dispatching an event via [`emit`](Self::emit) is $O(1)$ and non-blocking.
|
||||
///
|
||||
/// # Sync callback example
|
||||
/// ```rust,no_run
|
||||
/// use event_bus::EventBus;
|
||||
///
|
||||
/// #[derive(Clone, Debug)]
|
||||
/// struct MessageCreatedEvent {
|
||||
/// content: String,
|
||||
/// }
|
||||
///
|
||||
/// #[tokio::main]
|
||||
/// async fn main() {
|
||||
/// let bus = EventBus::new();
|
||||
///
|
||||
/// bus.on::<MessageCreatedEvent>(|event| {
|
||||
/// println!("Created message: {:?}", event);
|
||||
/// });
|
||||
///
|
||||
/// bus.emit(MessageCreatedEvent {
|
||||
/// content: "Hello!".into(),
|
||||
/// });
|
||||
/// }
|
||||
/// ```
|
||||
///
|
||||
/// # Async callback example
|
||||
/// ```rust,no_run
|
||||
/// use event_bus::EventBus;
|
||||
///
|
||||
/// #[derive(Clone, Debug)]
|
||||
/// struct MessageUpdatedEvent {
|
||||
/// id: u64,
|
||||
/// content: String,
|
||||
/// }
|
||||
///
|
||||
/// #[tokio::main]
|
||||
/// async fn main() {
|
||||
/// let bus = EventBus::new();
|
||||
///
|
||||
/// bus.on_async::<MessageUpdatedEvent>(|event| async move {
|
||||
/// println!("(async) Updated message: {:?}", event);
|
||||
/// });
|
||||
///
|
||||
/// bus.emit(MessageUpdatedEvent {
|
||||
/// id: 42,
|
||||
/// content: "World!".into(),
|
||||
/// });
|
||||
/// }
|
||||
/// ```
|
||||
#[derive(Debug, Clone)]
|
||||
pub struct EventBus {
|
||||
inner: Arc<EventBusInner>,
|
||||
}
|
||||
|
||||
impl Default for EventBus {
|
||||
@@ -422,3 +92,342 @@ impl Default for EventBus {
|
||||
Self::new()
|
||||
}
|
||||
}
|
||||
|
||||
impl EventBus {
|
||||
/// Creates a new `EventBus` with default capacity (1024 messages per channel).
|
||||
pub fn new() -> Self {
|
||||
Self::with_capacity(DEFAULT_CAPACITY)
|
||||
}
|
||||
|
||||
/// Creates a new `EventBus` with custom buffer capacity per channel.
|
||||
pub fn with_capacity(capacity: usize) -> Self {
|
||||
debug!(capacity, "EventBus created");
|
||||
Self {
|
||||
inner: Arc::new(EventBusInner {
|
||||
channels: RwLock::new(HashMap::new()),
|
||||
capacity,
|
||||
}),
|
||||
}
|
||||
}
|
||||
|
||||
// ─────────────────────────────────────────────────────────────────────────
|
||||
// Internal Channel Management
|
||||
// ─────────────────────────────────────────────────────────────────────────
|
||||
|
||||
fn get_or_create_sender<E: Event>(&self) -> broadcast::Sender<E> {
|
||||
let type_id = TypeId::of::<E>();
|
||||
|
||||
if let Some(tx) = self
|
||||
.inner
|
||||
.channels
|
||||
.read()
|
||||
.get(&type_id)
|
||||
.and_then(|entry| entry.downcast_ref::<broadcast::Sender<E>>())
|
||||
{
|
||||
return tx.clone();
|
||||
}
|
||||
|
||||
let mut channels = self.inner.channels.write();
|
||||
if let Some(tx) = channels
|
||||
.get(&type_id)
|
||||
.and_then(|entry| entry.downcast_ref::<broadcast::Sender<E>>())
|
||||
{
|
||||
return tx.clone();
|
||||
}
|
||||
|
||||
let (tx, _) = broadcast::channel::<E>(self.inner.capacity);
|
||||
debug!(
|
||||
event_type = std::any::type_name::<E>(),
|
||||
capacity = self.inner.capacity,
|
||||
"New broadcast channel created"
|
||||
);
|
||||
channels.insert(type_id, Box::new(tx.clone()));
|
||||
tx
|
||||
}
|
||||
|
||||
// ─────────────────────────────────────────────────────────────────────────
|
||||
// Emission
|
||||
// ─────────────────────────────────────────────────────────────────────────
|
||||
|
||||
/// Emits a strongly-typed event to all subscribers of `E` without blocking ($O(1)$).
|
||||
///
|
||||
/// If no subscribers exist for this event type, the event is dropped immediately
|
||||
/// without cloning or allocating.
|
||||
///
|
||||
/// # Example
|
||||
/// ```rust,no_run
|
||||
/// # use event_bus::EventBus;
|
||||
/// # #[derive(Clone)] struct UserConnected { id: u64 }
|
||||
/// # let bus = EventBus::new();
|
||||
/// bus.emit(UserConnected { id: 1 });
|
||||
/// ```
|
||||
pub fn emit<E: Event>(&self, event: E) {
|
||||
trace!(
|
||||
event_type = std::any::type_name::<E>(),
|
||||
"Emitting event"
|
||||
);
|
||||
|
||||
let type_id = TypeId::of::<E>();
|
||||
|
||||
if let Some(tx) = self
|
||||
.inner
|
||||
.channels
|
||||
.read()
|
||||
.get(&type_id)
|
||||
.and_then(|entry| entry.downcast_ref::<broadcast::Sender<E>>())
|
||||
.filter(|tx| tx.receiver_count() > 0)
|
||||
{
|
||||
let _ = tx.send(event);
|
||||
}
|
||||
}
|
||||
|
||||
// ─────────────────────────────────────────────────────────────────────────
|
||||
// Subscriptions — Callbacks
|
||||
// ─────────────────────────────────────────────────────────────────────────
|
||||
|
||||
/// Registers a synchronous callback for events of type `E`.
|
||||
///
|
||||
/// The handler runs in a dedicated background Tokio task.
|
||||
/// Returns a [`JoinHandle`] allowing to cancel the subscription via [`.abort()`](JoinHandle::abort).
|
||||
pub fn on<E: Event>(&self, handler: impl Fn(E) + Send + Sync + 'static) -> JoinHandle<()> {
|
||||
let mut rx = self.subscribe::<E>();
|
||||
let type_name = std::any::type_name::<E>();
|
||||
|
||||
debug!(event_type = type_name, "Sync subscriber registered");
|
||||
|
||||
tokio::spawn(async move {
|
||||
loop {
|
||||
match rx.recv().await {
|
||||
Ok(event) => {
|
||||
trace!(event_type = type_name, "Sync handler invoked");
|
||||
handler(event);
|
||||
}
|
||||
Err(broadcast::error::RecvError::Lagged(skipped)) => {
|
||||
warn!(
|
||||
event_type = type_name,
|
||||
skipped,
|
||||
"Subscriber lagged behind and skipped messages"
|
||||
);
|
||||
}
|
||||
Err(broadcast::error::RecvError::Closed) => {
|
||||
debug!(event_type = type_name, "Channel closed, subscriber exiting");
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
/// Registers an asynchronous callback for events of type `E`.
|
||||
///
|
||||
/// The handler runs in a dedicated background Tokio task.
|
||||
/// Returns a [`JoinHandle`] allowing to cancel the subscription via [`.abort()`](JoinHandle::abort).
|
||||
///
|
||||
/// # Example
|
||||
/// ```rust,no_run
|
||||
/// # use event_bus::EventBus;
|
||||
/// # #[derive(Clone)] struct MessageUpdatedEvent { id: u64 }
|
||||
/// # let bus = EventBus::new();
|
||||
/// bus.on_async::<MessageUpdatedEvent>(|event| async move {
|
||||
/// println!("Updated: {:?}", event.id);
|
||||
/// });
|
||||
/// ```
|
||||
pub fn on_async<E: Event>(&self, handler: impl AsyncHandler<E>) -> JoinHandle<()> {
|
||||
let mut rx = self.subscribe::<E>();
|
||||
let type_name = std::any::type_name::<E>();
|
||||
|
||||
debug!(event_type = type_name, "Async subscriber registered");
|
||||
|
||||
tokio::spawn(async move {
|
||||
loop {
|
||||
match rx.recv().await {
|
||||
Ok(event) => {
|
||||
trace!(event_type = type_name, "Async handler invoked");
|
||||
handler(event).await;
|
||||
}
|
||||
Err(broadcast::error::RecvError::Lagged(skipped)) => {
|
||||
warn!(
|
||||
event_type = type_name,
|
||||
skipped,
|
||||
"Subscriber lagged behind and skipped messages"
|
||||
);
|
||||
}
|
||||
Err(broadcast::error::RecvError::Closed) => {
|
||||
debug!(event_type = type_name, "Channel closed, subscriber exiting");
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
/// Registers an asynchronous callback for events of type `E` with an injected context `C`.
|
||||
pub fn on_async_with<E: Event, C: Clone + Send + Sync + 'static>(
|
||||
&self,
|
||||
context: C,
|
||||
handler: impl AsyncHandlerWith<E, C>,
|
||||
) -> JoinHandle<()> {
|
||||
let mut rx = self.subscribe::<E>();
|
||||
let type_name = std::any::type_name::<E>();
|
||||
|
||||
debug!(event_type = type_name, "Async subscriber with context registered");
|
||||
|
||||
tokio::spawn(async move {
|
||||
loop {
|
||||
match rx.recv().await {
|
||||
Ok(event) => {
|
||||
trace!(event_type = type_name, "Async handler with context invoked");
|
||||
handler(context.clone(), event).await;
|
||||
}
|
||||
Err(broadcast::error::RecvError::Lagged(skipped)) => {
|
||||
warn!(
|
||||
event_type = type_name,
|
||||
skipped,
|
||||
"Subscriber lagged behind and skipped messages"
|
||||
);
|
||||
}
|
||||
Err(broadcast::error::RecvError::Closed) => {
|
||||
debug!(event_type = type_name, "Channel closed, subscriber exiting");
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
// ─────────────────────────────────────────────────────────────────────────
|
||||
// Low-Level Subscription (Direct Stream / Receiver)
|
||||
// ─────────────────────────────────────────────────────────────────────────
|
||||
|
||||
/// Returns a direct [`broadcast::Receiver<E>`] for events of type `E`.
|
||||
///
|
||||
/// Allows writing custom event processing loops without callback wrappers.
|
||||
///
|
||||
/// # Example
|
||||
/// ```rust,no_run
|
||||
/// use event_bus::EventBus;
|
||||
///
|
||||
/// #[derive(Clone)]
|
||||
/// struct MyEvent;
|
||||
///
|
||||
/// #[tokio::main]
|
||||
/// async fn main() {
|
||||
/// let bus = EventBus::new();
|
||||
/// let mut rx = bus.subscribe::<MyEvent>();
|
||||
/// bus.emit(MyEvent);
|
||||
/// if let Ok(event) = rx.recv().await {
|
||||
/// // direct typed `event`
|
||||
/// }
|
||||
/// }
|
||||
/// ```
|
||||
pub fn subscribe<E: Event>(&self) -> broadcast::Receiver<E> {
|
||||
self.get_or_create_sender::<E>().subscribe()
|
||||
}
|
||||
|
||||
/// Waits for the next event of type `E` to be emitted.
|
||||
///
|
||||
/// Creates a temporary one-shot subscription and resolves as soon as an event of type `E`
|
||||
/// is emitted. The subscription is automatically dropped after receiving the event.
|
||||
///
|
||||
/// # Example
|
||||
/// ```rust,no_run
|
||||
/// # use event_bus::EventBus;
|
||||
/// # #[derive(Clone)] struct MyEvent { id: u64 }
|
||||
/// #[tokio::main]
|
||||
/// async fn main() {
|
||||
/// let bus = EventBus::new();
|
||||
/// let event = bus.wait_next::<MyEvent>().await.unwrap();
|
||||
/// println!("Received next event: {}", event.id);
|
||||
/// }
|
||||
/// ```
|
||||
pub async fn wait_next<E: Event>(&self) -> Result<E, broadcast::error::RecvError> {
|
||||
let mut rx = self.subscribe::<E>();
|
||||
rx.recv().await
|
||||
}
|
||||
|
||||
/// Waits for an event of type `E` satisfying the given predicate to be emitted.
|
||||
///
|
||||
/// Creates a temporary subscription, receives events of type `E`, and resolves
|
||||
/// when the predicate returns `true`. The subscription is automatically dropped afterwards.
|
||||
///
|
||||
/// # Example
|
||||
/// ```rust,no_run
|
||||
/// # use event_bus::EventBus;
|
||||
/// # #[derive(Clone)] struct MessageSaved { id: u64 }
|
||||
/// #[tokio::main]
|
||||
/// async fn main() {
|
||||
/// let bus = EventBus::new();
|
||||
/// let target_id = 42;
|
||||
/// let event = bus.wait_for::<MessageSaved>(|e| e.id == target_id).await.unwrap();
|
||||
/// println!("Saved message confirmed: {}", event.id);
|
||||
/// }
|
||||
/// ```
|
||||
pub async fn wait_for<E: Event>(
|
||||
&self,
|
||||
mut predicate: impl FnMut(&E) -> bool,
|
||||
) -> Result<E, broadcast::error::RecvError> {
|
||||
let mut rx = self.subscribe::<E>();
|
||||
loop {
|
||||
match rx.recv().await {
|
||||
Ok(event) => {
|
||||
if predicate(&event) {
|
||||
return Ok(event);
|
||||
}
|
||||
}
|
||||
Err(broadcast::error::RecvError::Lagged(skipped)) => {
|
||||
warn!(
|
||||
event_type = std::any::type_name::<E>(),
|
||||
skipped,
|
||||
"wait_for subscriber lagged behind and skipped messages"
|
||||
);
|
||||
}
|
||||
Err(broadcast::error::RecvError::Closed) => {
|
||||
return Err(broadcast::error::RecvError::Closed);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// ─────────────────────────────────────────────────────────────────────────
|
||||
// Metrics & Utilities
|
||||
// ─────────────────────────────────────────────────────────────────────────
|
||||
|
||||
/// Returns the total number of active subscribers for event type `E`.
|
||||
pub fn subscriber_count<E: Event>(&self) -> usize {
|
||||
let type_id = TypeId::of::<E>();
|
||||
|
||||
self.inner
|
||||
.channels
|
||||
.read()
|
||||
.get(&type_id)
|
||||
.and_then(|e| e.downcast_ref::<broadcast::Sender<E>>())
|
||||
.map(|tx| tx.receiver_count())
|
||||
.unwrap_or(0)
|
||||
}
|
||||
|
||||
/// Returns `true` if there are any active subscribers for event type `E`.
|
||||
pub fn has_subscribers<E: Event>(&self) -> bool {
|
||||
self.subscriber_count::<E>() > 0
|
||||
}
|
||||
|
||||
/// Returns the total number of broadcast channels currently instantiated in the bus.
|
||||
pub fn channel_count(&self) -> usize {
|
||||
self.inner.channels.read().len()
|
||||
}
|
||||
|
||||
/// Returns `true` if no channels are currently registered in the bus.
|
||||
pub fn is_empty(&self) -> bool {
|
||||
self.inner.channels.read().is_empty()
|
||||
}
|
||||
|
||||
/// Removes and drops all channels in the event bus.
|
||||
pub fn clear(&self) {
|
||||
self.inner.channels.write().clear();
|
||||
}
|
||||
|
||||
/// Removes and drops the channel associated with event type `E`.
|
||||
pub fn clear_type<E: Event>(&self) {
|
||||
let type_id = TypeId::of::<E>();
|
||||
self.inner.channels.write().remove(&type_id);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,47 @@
|
||||
use std::future::Future;
|
||||
|
||||
/// Handler trait for asynchronous event callbacks.
|
||||
///
|
||||
/// This trait is automatically implemented for any closure matching
|
||||
/// `Fn(E) -> Future<Output = ()> + Send + Sync + 'static`.
|
||||
///
|
||||
/// By using `Fn(E) -> <Self as AsyncHandler<E>>::Fut` as a supertrait,
|
||||
/// Rust propagates the event type `E` directly into the closure's parameter,
|
||||
/// enabling seamless turbofish syntax with clean field access:
|
||||
///
|
||||
/// ```rust,no_run
|
||||
/// # use event_bus::EventBus;
|
||||
/// # #[derive(Clone)] struct MessageUpdatedEvent { id: u64 }
|
||||
/// # let bus = EventBus::new();
|
||||
/// bus.on_async::<MessageUpdatedEvent>(|event| async move {
|
||||
/// println!("Updated: {:?}", event.id);
|
||||
/// });
|
||||
/// ```
|
||||
pub trait AsyncHandler<E>: Fn(E) -> <Self as AsyncHandler<E>>::Fut + Send + Sync + 'static {
|
||||
type Fut: Future<Output = ()> + Send + 'static;
|
||||
}
|
||||
|
||||
impl<E, F, Fut> AsyncHandler<E> for F
|
||||
where
|
||||
F: Fn(E) -> Fut + Send + Sync + 'static,
|
||||
Fut: Future<Output = ()> + Send + 'static,
|
||||
{
|
||||
type Fut = Fut;
|
||||
}
|
||||
|
||||
/// Handler trait for asynchronous event callbacks with an injected context.
|
||||
///
|
||||
/// Automatically implemented for closures matching
|
||||
/// `Fn(C, E) -> Future<Output = ()> + Send + Sync + 'static`.
|
||||
pub trait AsyncHandlerWith<E, C>: Fn(C, E) -> <Self as AsyncHandlerWith<E, C>>::Fut + Send + Sync + 'static {
|
||||
type Fut: Future<Output = ()> + Send + 'static;
|
||||
}
|
||||
|
||||
impl<E, C, F, Fut> AsyncHandlerWith<E, C> for F
|
||||
where
|
||||
C: Clone + Send + Sync + 'static,
|
||||
F: Fn(C, E) -> Fut + Send + Sync + 'static,
|
||||
Fut: Future<Output = ()> + Send + 'static,
|
||||
{
|
||||
type Fut = Fut;
|
||||
}
|
||||
+44
-44
@@ -1,49 +1,49 @@
|
||||
/// Downcasts an [`AnyEvent`] to one or more concrete types and executes
|
||||
/// the matching closure if the type matches.
|
||||
///
|
||||
/// Non-matching branches are silently ignored.
|
||||
///
|
||||
/// # Syntax
|
||||
/// ```text
|
||||
/// match_event!(evt, Type1 => |val| { ... }, Type2 => |val| { ... })
|
||||
/// ```
|
||||
///
|
||||
/// # Example
|
||||
/// ```rust,no_run
|
||||
/// # use std::sync::Arc;
|
||||
/// # use oxspeak_server_lib::event_bus::EventBus;
|
||||
/// # use oxspeak_server_lib::match_event;
|
||||
/// # #[derive(Clone, Debug)] struct User { name: String }
|
||||
/// # #[derive(Clone, Debug)] struct UdpMetric { value: f32 }
|
||||
/// # let bus = Arc::new(EventBus::new());
|
||||
/// # tokio_test::block_on(async {
|
||||
/// let mut rx = bus.on_raw("user-connected");
|
||||
/// bus.emit("user-connected", User { name: "Alice".into() });
|
||||
///
|
||||
/// if let Ok(evt) = rx.recv().await {
|
||||
/// match_event!(evt,
|
||||
/// User => |u| println!("User: {:?}", u),
|
||||
/// UdpMetric => |m| println!("Metric: {:?}", m),
|
||||
/// );
|
||||
/// }
|
||||
/// # });
|
||||
/// ```
|
||||
#[macro_export]
|
||||
macro_rules! match_event {
|
||||
($evt:expr, $($type:ty => $handler:expr),+ $(,)?) => {
|
||||
$(
|
||||
if let Some(val) = ($evt).downcast_ref::<$type>() {
|
||||
($handler)(val.clone());
|
||||
} else
|
||||
)+
|
||||
{
|
||||
// No matching type → silently ignored
|
||||
}
|
||||
};
|
||||
}
|
||||
//! # event_bus
|
||||
//!
|
||||
//! A strongly-typed, high-performance in-memory event bus for Tokio.
|
||||
//!
|
||||
//! ## Overview
|
||||
//!
|
||||
//! Unlike string/topic-based event buses, `event_bus` routes events using
|
||||
//! their concrete Rust types ([`std::any::TypeId`]).
|
||||
//!
|
||||
//! - **Strong typing**: No string keys required for event types, no manual `match_event!`
|
||||
//! macros, and no runtime downcasting (`downcast_ref`) inside the subscriber loops.
|
||||
//! - **Ergonomic async subscribers**: Handlers can be registered with clean turbofish syntax:
|
||||
//! `bus.on_async::<MessageUpdatedEvent>(|event| async move { ... })`.
|
||||
//! - **Targeted wake-up**: Tokio broadcast channels are isolated per event type.
|
||||
//!
|
||||
//! ## Example
|
||||
//!
|
||||
//! ```rust,no_run
|
||||
//! use event_bus::EventBus;
|
||||
//!
|
||||
//! #[derive(Clone, Debug, PartialEq)]
|
||||
//! struct MessageCreatedEvent {
|
||||
//! content: String,
|
||||
//! }
|
||||
//!
|
||||
//! #[tokio::main]
|
||||
//! async fn main() {
|
||||
//! let bus = EventBus::new();
|
||||
//!
|
||||
//! // Async subscriber
|
||||
//! bus.on_async::<MessageCreatedEvent>(|event| async move {
|
||||
//! println!("Received message: {}", event.content);
|
||||
//! });
|
||||
//!
|
||||
//! // Emit event
|
||||
//! bus.emit(MessageCreatedEvent {
|
||||
//! content: "Hello from typed event bus!".into(),
|
||||
//! });
|
||||
//! }
|
||||
//! ```
|
||||
|
||||
mod bus;
|
||||
pub use bus::{AnyEvent, EventBus, Scope, ScopeValue};
|
||||
mod handler;
|
||||
|
||||
pub use bus::{DEFAULT_CAPACITY, Event, EventBus};
|
||||
pub use handler::{AsyncHandler, AsyncHandlerWith};
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests;
|
||||
|
||||
+314
-142
@@ -1,192 +1,364 @@
|
||||
use crate::{EventBus, match_event};
|
||||
use std::sync::Arc;
|
||||
use std::sync::atomic::{AtomicBool, AtomicU32, Ordering};
|
||||
use uuid::Uuid;
|
||||
|
||||
use crate::EventBus;
|
||||
|
||||
#[derive(Clone, Debug, PartialEq)]
|
||||
struct User {
|
||||
name: String,
|
||||
struct MessageCreatedEvent {
|
||||
channel_id: Uuid,
|
||||
content: String,
|
||||
}
|
||||
|
||||
#[derive(Clone, Debug, PartialEq)]
|
||||
struct UdpMetric {
|
||||
struct MessageUpdatedEvent {
|
||||
id: u64,
|
||||
content: String,
|
||||
}
|
||||
|
||||
#[derive(Clone, Debug, PartialEq)]
|
||||
struct MessageDeletedEvent {
|
||||
id: u64,
|
||||
}
|
||||
|
||||
#[derive(Clone, Debug, PartialEq)]
|
||||
struct UdpMetricEvent {
|
||||
value: f32,
|
||||
}
|
||||
|
||||
// ── on (callback sync) ────────────────────────────────────────────────────
|
||||
// ── Sync Callbacks ──────────────────────────────────────────────────────────
|
||||
|
||||
#[tokio::test]
|
||||
async fn test_on_callback_sync() {
|
||||
let bus = Arc::new(EventBus::new());
|
||||
let bus = EventBus::new();
|
||||
let received = Arc::new(AtomicBool::new(false));
|
||||
let flag = Arc::clone(&received);
|
||||
|
||||
bus.on::<User, _>("user-connected", move |user| {
|
||||
if user.name == "Alice" {
|
||||
bus.on::<MessageCreatedEvent>(move |event| {
|
||||
if event.content == "Hello" {
|
||||
flag.store(true, Ordering::SeqCst);
|
||||
}
|
||||
});
|
||||
|
||||
bus.emit(
|
||||
"user-connected",
|
||||
User {
|
||||
name: "Alice".into(),
|
||||
},
|
||||
);
|
||||
tokio::time::sleep(std::time::Duration::from_millis(20)).await;
|
||||
bus.emit(MessageCreatedEvent {
|
||||
channel_id: Uuid::new_v4(),
|
||||
content: "Hello".into(),
|
||||
});
|
||||
|
||||
tokio::time::sleep(std::time::Duration::from_millis(20)).await;
|
||||
assert!(received.load(Ordering::SeqCst));
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn test_on_targeted_wakeup() {
|
||||
// Émettre sur "user-connected" ne doit pas réveiller "udp-metrics-updated"
|
||||
let bus = Arc::new(EventBus::new());
|
||||
let metric_called = Arc::new(AtomicBool::new(false));
|
||||
let flag = Arc::clone(&metric_called);
|
||||
|
||||
bus.on::<UdpMetric, _>("udp-metrics-updated", move |_| {
|
||||
flag.store(true, Ordering::SeqCst);
|
||||
});
|
||||
|
||||
bus.emit(
|
||||
"user-connected",
|
||||
User {
|
||||
name: "Carol".into(),
|
||||
},
|
||||
);
|
||||
tokio::time::sleep(std::time::Duration::from_millis(20)).await;
|
||||
|
||||
assert!(!metric_called.load(Ordering::SeqCst));
|
||||
}
|
||||
// ── Async Callbacks (Exact User Requirement) ────────────────────────────────
|
||||
|
||||
#[tokio::test]
|
||||
async fn test_on_type_mismatch_ignored() {
|
||||
// Émettre un UdpMetric sur un topic écouté en User → handler pas appelé
|
||||
let bus = Arc::new(EventBus::new());
|
||||
let called = Arc::new(AtomicBool::new(false));
|
||||
let flag = Arc::clone(&called);
|
||||
async fn test_on_async_callback_turbofish() {
|
||||
let bus = EventBus::new();
|
||||
let received_content = Arc::new(tokio::sync::Mutex::new(String::new()));
|
||||
let rc = Arc::clone(&received_content);
|
||||
|
||||
bus.on::<User, _>("mixed-topic", move |_| {
|
||||
flag.store(true, Ordering::SeqCst);
|
||||
});
|
||||
|
||||
bus.emit("mixed-topic", UdpMetric { value: 1.0 });
|
||||
tokio::time::sleep(std::time::Duration::from_millis(20)).await;
|
||||
|
||||
assert!(!called.load(Ordering::SeqCst));
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn test_on_multiple_subscribers_same_topic() {
|
||||
let bus = Arc::new(EventBus::new());
|
||||
let count = Arc::new(AtomicU32::new(0));
|
||||
|
||||
for _ in 0..3 {
|
||||
let c = Arc::clone(&count);
|
||||
bus.on::<User, _>("user-connected", move |_| {
|
||||
c.fetch_add(1, Ordering::SeqCst);
|
||||
});
|
||||
}
|
||||
|
||||
bus.emit(
|
||||
"user-connected",
|
||||
User {
|
||||
name: "Grace".into(),
|
||||
},
|
||||
);
|
||||
tokio::time::sleep(std::time::Duration::from_millis(20)).await;
|
||||
|
||||
assert_eq!(count.load(Ordering::SeqCst), 3);
|
||||
}
|
||||
|
||||
// ── on_async ──────────────────────────────────────────────────────────────
|
||||
|
||||
#[tokio::test]
|
||||
async fn test_on_async_callback() {
|
||||
let bus = Arc::new(EventBus::new());
|
||||
let received = Arc::new(AtomicBool::new(false));
|
||||
let flag = Arc::clone(&received);
|
||||
|
||||
bus.on_async::<User, _, _>("user-connected", move |user| {
|
||||
let f = Arc::clone(&flag);
|
||||
// Exact syntax specified by the user:
|
||||
// event_bus.on_async::<MessageUpdatedEvent>(|event| async move { ... });
|
||||
bus.on_async::<MessageUpdatedEvent>(move |event| {
|
||||
let rc = Arc::clone(&rc);
|
||||
async move {
|
||||
if user.name == "Async" {
|
||||
let mut lock = rc.lock().await;
|
||||
*lock = event.content;
|
||||
}
|
||||
});
|
||||
|
||||
// Exact syntax specified by the user:
|
||||
// event_bus.emit(MessageUpdatedEvent { ... });
|
||||
bus.emit(MessageUpdatedEvent {
|
||||
id: 42,
|
||||
content: "Updated message content".into(),
|
||||
});
|
||||
|
||||
tokio::time::sleep(std::time::Duration::from_millis(20)).await;
|
||||
|
||||
let result = received_content.lock().await.clone();
|
||||
assert_eq!(result, "Updated message content");
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn test_on_async_callback_type_inferred() {
|
||||
let bus = EventBus::new();
|
||||
let flag = Arc::new(AtomicBool::new(false));
|
||||
let f = Arc::clone(&flag);
|
||||
|
||||
// Also supports inferring the event type from closure parameter
|
||||
bus.on_async(move |event: MessageUpdatedEvent| {
|
||||
let f = Arc::clone(&f);
|
||||
async move {
|
||||
if event.id == 99 {
|
||||
f.store(true, Ordering::SeqCst);
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
bus.emit(
|
||||
"user-connected",
|
||||
User {
|
||||
name: "Async".into(),
|
||||
},
|
||||
);
|
||||
tokio::time::sleep(std::time::Duration::from_millis(20)).await;
|
||||
bus.emit(MessageUpdatedEvent {
|
||||
id: 99,
|
||||
content: "Inferred".into(),
|
||||
});
|
||||
|
||||
assert!(received.load(Ordering::SeqCst));
|
||||
tokio::time::sleep(std::time::Duration::from_millis(20)).await;
|
||||
assert!(flag.load(Ordering::SeqCst));
|
||||
}
|
||||
|
||||
// ── on_raw + match_event! ─────────────────────────────────────────────────
|
||||
// ── Targeted Wake-Up & Type Isolation ───────────────────────────────────────
|
||||
|
||||
#[tokio::test]
|
||||
async fn test_on_raw_and_match_event_macro() {
|
||||
let bus = Arc::new(EventBus::new());
|
||||
let mut rx = bus.on_raw("user-connected");
|
||||
|
||||
bus.emit(
|
||||
"user-connected",
|
||||
User {
|
||||
name: "Frank".into(),
|
||||
},
|
||||
);
|
||||
|
||||
let evt = rx.recv().await.unwrap();
|
||||
let mut received_name = String::new();
|
||||
match_event!(evt,
|
||||
User => |u: User| { received_name = u.name.clone(); },
|
||||
UdpMetric => |_m: UdpMetric| { panic!("mauvais type"); }
|
||||
);
|
||||
assert_eq!(received_name, "Frank");
|
||||
}
|
||||
|
||||
// ── Utilitaires ────────────────────────────────────────────────────────────
|
||||
|
||||
#[test]
|
||||
fn test_topics_list() {
|
||||
async fn test_on_targeted_wakeup() {
|
||||
let bus = EventBus::new();
|
||||
// on_raw enregistre le canal (get_or_create)
|
||||
let _rx1 = bus.on_raw("user-connected");
|
||||
let _rx2 = bus.on_raw("udp-metrics-updated");
|
||||
let metric_called = Arc::new(AtomicBool::new(false));
|
||||
let flag = Arc::clone(&metric_called);
|
||||
|
||||
let mut topics = bus.topics();
|
||||
topics.sort();
|
||||
assert_eq!(topics, vec!["udp-metrics-updated", "user-connected"]);
|
||||
bus.on::<UdpMetricEvent>(move |_| {
|
||||
flag.store(true, Ordering::SeqCst);
|
||||
});
|
||||
|
||||
// Emitting MessageCreatedEvent must never wake up UdpMetricEvent subscribers
|
||||
bus.emit(MessageCreatedEvent {
|
||||
channel_id: Uuid::new_v4(),
|
||||
content: "Ignore me".into(),
|
||||
});
|
||||
|
||||
tokio::time::sleep(std::time::Duration::from_millis(20)).await;
|
||||
assert!(!metric_called.load(Ordering::SeqCst));
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn test_emit_multiple_types_same_bus() {
|
||||
let bus = Arc::new(EventBus::new());
|
||||
let user_ok = Arc::new(AtomicBool::new(false));
|
||||
let metric_ok = Arc::new(AtomicBool::new(false));
|
||||
let u = Arc::clone(&user_ok);
|
||||
let m = Arc::clone(&metric_ok);
|
||||
async fn test_multiple_subscribers_same_type() {
|
||||
let bus = EventBus::new();
|
||||
let count = Arc::new(AtomicU32::new(0));
|
||||
|
||||
bus.on::<User, _>("user-connected", move |user| {
|
||||
if user.name == "Bob" {
|
||||
u.store(true, Ordering::SeqCst);
|
||||
}
|
||||
});
|
||||
bus.on::<UdpMetric, _>("udp-metrics-updated", move |metric| {
|
||||
if (metric.value - 3.14).abs() < 0.001 {
|
||||
m.store(true, Ordering::SeqCst);
|
||||
}
|
||||
for _ in 0..3 {
|
||||
let c = Arc::clone(&count);
|
||||
bus.on::<MessageCreatedEvent>(move |_| {
|
||||
c.fetch_add(1, Ordering::SeqCst);
|
||||
});
|
||||
}
|
||||
|
||||
bus.emit(MessageCreatedEvent {
|
||||
channel_id: Uuid::new_v4(),
|
||||
content: "Broadcast".into(),
|
||||
});
|
||||
|
||||
bus.emit("user-connected", User { name: "Bob".into() });
|
||||
bus.emit("udp-metrics-updated", UdpMetric { value: 3.14 });
|
||||
tokio::time::sleep(std::time::Duration::from_millis(20)).await;
|
||||
assert_eq!(count.load(Ordering::SeqCst), 3);
|
||||
}
|
||||
|
||||
assert!(user_ok.load(Ordering::SeqCst));
|
||||
#[tokio::test]
|
||||
async fn test_multiple_different_types_same_bus() {
|
||||
let bus = EventBus::new();
|
||||
let msg_ok = Arc::new(AtomicBool::new(false));
|
||||
let metric_ok = Arc::new(AtomicBool::new(false));
|
||||
let m_flag = Arc::clone(&msg_ok);
|
||||
let u_flag = Arc::clone(&metric_ok);
|
||||
|
||||
bus.on::<MessageCreatedEvent>(move |event| {
|
||||
if event.content == "Test" {
|
||||
m_flag.store(true, Ordering::SeqCst);
|
||||
}
|
||||
});
|
||||
|
||||
bus.on::<UdpMetricEvent>(move |metric| {
|
||||
if (metric.value - 42.5).abs() < 0.001 {
|
||||
u_flag.store(true, Ordering::SeqCst);
|
||||
}
|
||||
});
|
||||
|
||||
bus.emit(MessageCreatedEvent {
|
||||
channel_id: Uuid::new_v4(),
|
||||
content: "Test".into(),
|
||||
});
|
||||
bus.emit(UdpMetricEvent { value: 42.5 });
|
||||
|
||||
tokio::time::sleep(std::time::Duration::from_millis(20)).await;
|
||||
assert!(msg_ok.load(Ordering::SeqCst));
|
||||
assert!(metric_ok.load(Ordering::SeqCst));
|
||||
}
|
||||
|
||||
// ── Direct Stream / Receiver (No match_event! needed) ───────────────────────
|
||||
|
||||
#[tokio::test]
|
||||
async fn test_subscribe_direct_typed_receiver() {
|
||||
let bus = EventBus::new();
|
||||
let mut rx = bus.subscribe::<MessageUpdatedEvent>();
|
||||
|
||||
bus.emit(MessageUpdatedEvent {
|
||||
id: 123,
|
||||
content: "Direct typed".into(),
|
||||
});
|
||||
|
||||
let event = rx.recv().await.expect("failed to receive event");
|
||||
// event is directly of type MessageUpdatedEvent, no downcast needed!
|
||||
assert_eq!(event.id, 123);
|
||||
assert_eq!(event.content, "Direct typed");
|
||||
}
|
||||
|
||||
// ── In-Handler Filtering (Direct Field Access) ──────────────────────────────
|
||||
|
||||
#[tokio::test]
|
||||
async fn test_filter_by_field_in_subscriber() {
|
||||
let bus = EventBus::new();
|
||||
let channel_a = Uuid::new_v4();
|
||||
let channel_b = Uuid::new_v4();
|
||||
|
||||
let count_a = Arc::new(AtomicU32::new(0));
|
||||
let count_b = Arc::new(AtomicU32::new(0));
|
||||
let count_global = Arc::new(AtomicU32::new(0));
|
||||
|
||||
let ca = Arc::clone(&count_a);
|
||||
bus.on_async::<MessageCreatedEvent>(move |event| {
|
||||
let ca = Arc::clone(&ca);
|
||||
async move {
|
||||
if event.channel_id == channel_a {
|
||||
ca.fetch_add(1, Ordering::SeqCst);
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
let cb = Arc::clone(&count_b);
|
||||
bus.on_async::<MessageCreatedEvent>(move |event| {
|
||||
let cb = Arc::clone(&cb);
|
||||
async move {
|
||||
if event.channel_id == channel_b {
|
||||
cb.fetch_add(1, Ordering::SeqCst);
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
let cg = Arc::clone(&count_global);
|
||||
bus.on::<MessageCreatedEvent>(move |_| {
|
||||
cg.fetch_add(1, Ordering::SeqCst);
|
||||
});
|
||||
|
||||
// Emit event with channel_a
|
||||
bus.emit(MessageCreatedEvent {
|
||||
channel_id: channel_a,
|
||||
content: "For A".into(),
|
||||
});
|
||||
|
||||
tokio::time::sleep(std::time::Duration::from_millis(20)).await;
|
||||
|
||||
// Both channel_a handler and global handler processed it, but not channel_b
|
||||
assert_eq!(count_a.load(Ordering::SeqCst), 1);
|
||||
assert_eq!(count_b.load(Ordering::SeqCst), 0);
|
||||
assert_eq!(count_global.load(Ordering::SeqCst), 1);
|
||||
}
|
||||
|
||||
// ── Async with Context ──────────────────────────────────────────────────────
|
||||
|
||||
#[tokio::test]
|
||||
async fn test_on_async_with_context() {
|
||||
let bus = EventBus::new();
|
||||
let prefix = Arc::new("Prefix: ".to_string());
|
||||
let result = Arc::new(tokio::sync::Mutex::new(String::new()));
|
||||
let r = Arc::clone(&result);
|
||||
|
||||
bus.on_async_with::<MessageCreatedEvent, _>(prefix, move |ctx, event| {
|
||||
let r = Arc::clone(&r);
|
||||
async move {
|
||||
let mut lock = r.lock().await;
|
||||
*lock = format!("{}{}", ctx, event.content);
|
||||
}
|
||||
});
|
||||
|
||||
bus.emit(MessageCreatedEvent {
|
||||
channel_id: Uuid::new_v4(),
|
||||
content: "Hello Context".into(),
|
||||
});
|
||||
|
||||
tokio::time::sleep(std::time::Duration::from_millis(20)).await;
|
||||
let final_str = result.lock().await.clone();
|
||||
assert_eq!(final_str, "Prefix: Hello Context");
|
||||
}
|
||||
|
||||
// ── Metrics, Utilities & Cleanup ────────────────────────────────────────────
|
||||
|
||||
#[test]
|
||||
fn test_subscriber_count_and_clear() {
|
||||
let bus = EventBus::new();
|
||||
assert_eq!(bus.subscriber_count::<MessageCreatedEvent>(), 0);
|
||||
assert!(!bus.has_subscribers::<MessageCreatedEvent>());
|
||||
|
||||
let _sub = bus.subscribe::<MessageCreatedEvent>();
|
||||
assert_eq!(bus.subscriber_count::<MessageCreatedEvent>(), 1);
|
||||
assert!(bus.has_subscribers::<MessageCreatedEvent>());
|
||||
assert_eq!(bus.channel_count(), 1);
|
||||
|
||||
bus.clear_type::<MessageCreatedEvent>();
|
||||
assert_eq!(bus.subscriber_count::<MessageCreatedEvent>(), 0);
|
||||
assert_eq!(bus.channel_count(), 0);
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn test_subscription_abort() {
|
||||
let bus = EventBus::new();
|
||||
let count = Arc::new(AtomicU32::new(0));
|
||||
let c = Arc::clone(&count);
|
||||
|
||||
let handle = bus.on::<MessageDeletedEvent>(move |_| {
|
||||
c.fetch_add(1, Ordering::SeqCst);
|
||||
});
|
||||
|
||||
bus.emit(MessageDeletedEvent { id: 1 });
|
||||
tokio::time::sleep(std::time::Duration::from_millis(20)).await;
|
||||
assert_eq!(count.load(Ordering::SeqCst), 1);
|
||||
|
||||
// Cancel the subscription
|
||||
handle.abort();
|
||||
tokio::time::sleep(std::time::Duration::from_millis(10)).await;
|
||||
|
||||
bus.emit(MessageDeletedEvent { id: 2 });
|
||||
tokio::time::sleep(std::time::Duration::from_millis(20)).await;
|
||||
// Count should not increase after abort
|
||||
assert_eq!(count.load(Ordering::SeqCst), 1);
|
||||
}
|
||||
|
||||
// ── One-Shot Listeners (wait_next & wait_for) ────────────────────────────────
|
||||
|
||||
#[tokio::test]
|
||||
async fn test_wait_next() {
|
||||
let bus = EventBus::new();
|
||||
let b = bus.clone();
|
||||
|
||||
tokio::spawn(async move {
|
||||
tokio::time::sleep(std::time::Duration::from_millis(10)).await;
|
||||
b.emit(MessageUpdatedEvent {
|
||||
id: 777,
|
||||
content: "Next event".into(),
|
||||
});
|
||||
});
|
||||
|
||||
let event = bus.wait_next::<MessageUpdatedEvent>().await.unwrap();
|
||||
assert_eq!(event.id, 777);
|
||||
assert_eq!(event.content, "Next event");
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn test_wait_for() {
|
||||
let bus = EventBus::new();
|
||||
let b = bus.clone();
|
||||
|
||||
tokio::spawn(async move {
|
||||
tokio::time::sleep(std::time::Duration::from_millis(10)).await;
|
||||
b.emit(MessageUpdatedEvent {
|
||||
id: 1,
|
||||
content: "Ignore".into(),
|
||||
});
|
||||
tokio::time::sleep(std::time::Duration::from_millis(10)).await;
|
||||
b.emit(MessageUpdatedEvent {
|
||||
id: 2,
|
||||
content: "Target".into(),
|
||||
});
|
||||
});
|
||||
|
||||
let event = bus
|
||||
.wait_for::<MessageUpdatedEvent>(|e| e.id == 2)
|
||||
.await
|
||||
.unwrap();
|
||||
assert_eq!(event.id, 2);
|
||||
assert_eq!(event.content, "Target");
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user