refactor (v2): better arch
Co-authored-by: Copilot <copilot@github.com>
This commit is contained in:
11
crates/adapters/event-publisher-memory/Cargo.toml
Normal file
11
crates/adapters/event-publisher-memory/Cargo.toml
Normal file
@@ -0,0 +1,11 @@
|
||||
[package]
|
||||
name = "event-publisher-memory"
|
||||
version = "0.1.0"
|
||||
edition = "2024"
|
||||
|
||||
[dependencies]
|
||||
domain = { workspace = true }
|
||||
async-trait = { workspace = true }
|
||||
futures = { workspace = true }
|
||||
tokio = { workspace = true }
|
||||
tracing = { workspace = true }
|
||||
85
crates/adapters/event-publisher-memory/src/lib.rs
Normal file
85
crates/adapters/event-publisher-memory/src/lib.rs
Normal file
@@ -0,0 +1,85 @@
|
||||
use std::sync::Arc;
|
||||
|
||||
use async_trait::async_trait;
|
||||
use futures::stream::BoxStream;
|
||||
use tokio::sync::broadcast;
|
||||
|
||||
use domain::{
|
||||
errors::DomainError,
|
||||
events::{DomainEvent, EventConsumer, EventEnvelope, EventPublisher},
|
||||
};
|
||||
|
||||
const CHANNEL_CAPACITY: usize = 256;
|
||||
|
||||
/// Shared in-memory event bus backed by a tokio broadcast channel.
|
||||
/// Create one bus, then hand out publisher and consumer handles from it.
|
||||
pub struct MemoryEventBus {
|
||||
sender: broadcast::Sender<DomainEvent>,
|
||||
}
|
||||
|
||||
impl MemoryEventBus {
|
||||
pub fn new() -> Self {
|
||||
let (sender, _) = broadcast::channel(CHANNEL_CAPACITY);
|
||||
Self { sender }
|
||||
}
|
||||
|
||||
pub fn publisher(&self) -> Arc<MemoryEventPublisher> {
|
||||
Arc::new(MemoryEventPublisher {
|
||||
sender: self.sender.clone(),
|
||||
})
|
||||
}
|
||||
|
||||
pub fn consumer(&self) -> Arc<MemoryEventConsumer> {
|
||||
Arc::new(MemoryEventConsumer {
|
||||
sender: self.sender.clone(),
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
impl Default for MemoryEventBus {
|
||||
fn default() -> Self {
|
||||
Self::new()
|
||||
}
|
||||
}
|
||||
|
||||
pub struct MemoryEventPublisher {
|
||||
sender: broadcast::Sender<DomainEvent>,
|
||||
}
|
||||
|
||||
#[async_trait]
|
||||
impl EventPublisher for MemoryEventPublisher {
|
||||
async fn publish(&self, event: &DomainEvent) -> Result<(), DomainError> {
|
||||
// send() only fails when there are no receivers; that is fine in dev/test.
|
||||
let _ = self.sender.send(event.clone());
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
|
||||
pub struct MemoryEventConsumer {
|
||||
sender: broadcast::Sender<DomainEvent>,
|
||||
}
|
||||
|
||||
impl EventConsumer for MemoryEventConsumer {
|
||||
fn consume(&self) -> BoxStream<'_, Result<EventEnvelope, DomainError>> {
|
||||
let rx = self.sender.subscribe();
|
||||
|
||||
Box::pin(futures::stream::unfold(rx, |mut rx| async move {
|
||||
loop {
|
||||
match rx.recv().await {
|
||||
Ok(event) => {
|
||||
let envelope = EventEnvelope::noop(event);
|
||||
return Some((Ok(envelope), rx));
|
||||
}
|
||||
Err(broadcast::error::RecvError::Lagged(n)) => {
|
||||
tracing::warn!("memory event bus: consumer lagged, skipped {n} messages");
|
||||
}
|
||||
Err(broadcast::error::RecvError::Closed) => return None,
|
||||
}
|
||||
}
|
||||
}))
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
#[path = "tests/lib.rs"]
|
||||
mod tests;
|
||||
75
crates/adapters/event-publisher-memory/src/tests/lib.rs
Normal file
75
crates/adapters/event-publisher-memory/src/tests/lib.rs
Normal file
@@ -0,0 +1,75 @@
|
||||
use futures::StreamExt;
|
||||
|
||||
use domain::{
|
||||
events::{DomainEvent, EventConsumer, EventPublisher},
|
||||
note::entity::NoteId,
|
||||
user::entity::UserId,
|
||||
};
|
||||
|
||||
use crate::MemoryEventBus;
|
||||
|
||||
fn note_updated() -> DomainEvent {
|
||||
DomainEvent::NoteUpdated {
|
||||
note_id: NoteId::new(),
|
||||
user_id: UserId::new(),
|
||||
}
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn published_event_is_received_by_consumer() {
|
||||
let bus = MemoryEventBus::new();
|
||||
let publisher = bus.publisher();
|
||||
let consumer = bus.consumer();
|
||||
|
||||
let event = note_updated();
|
||||
let mut stream = consumer.consume();
|
||||
|
||||
publisher.publish(&event).await.unwrap();
|
||||
|
||||
let envelope = stream.next().await.unwrap().unwrap();
|
||||
assert!(matches!(envelope.event, DomainEvent::NoteUpdated { .. }));
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn ack_on_memory_envelope_is_noop() {
|
||||
let bus = MemoryEventBus::new();
|
||||
let publisher = bus.publisher();
|
||||
let consumer = bus.consumer();
|
||||
|
||||
// Subscribe before publishing — broadcast drops messages sent before subscribe.
|
||||
let mut stream = consumer.consume();
|
||||
publisher.publish(¬e_updated()).await.unwrap();
|
||||
|
||||
let envelope = stream.next().await.unwrap().unwrap();
|
||||
envelope.ack().await.unwrap();
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn multiple_consumers_each_receive_the_event() {
|
||||
let bus = MemoryEventBus::new();
|
||||
let publisher = bus.publisher();
|
||||
let c1 = bus.consumer();
|
||||
let c2 = bus.consumer();
|
||||
|
||||
let mut s1 = c1.consume();
|
||||
let mut s2 = c2.consume();
|
||||
|
||||
publisher.publish(¬e_updated()).await.unwrap();
|
||||
|
||||
assert!(matches!(
|
||||
s1.next().await.unwrap().unwrap().event,
|
||||
DomainEvent::NoteUpdated { .. }
|
||||
));
|
||||
assert!(matches!(
|
||||
s2.next().await.unwrap().unwrap().event,
|
||||
DomainEvent::NoteUpdated { .. }
|
||||
));
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn publish_with_no_consumer_does_not_error() {
|
||||
let bus = MemoryEventBus::new();
|
||||
let publisher = bus.publisher();
|
||||
// No consumer — publish should silently succeed.
|
||||
publisher.publish(¬e_updated()).await.unwrap();
|
||||
}
|
||||
Reference in New Issue
Block a user