Skip to main content

oximedia_workflow/
event_bus.rs

1//! Publish/subscribe event bus for workflow task communication.
2//!
3//! Provides a typed, thread-safe event bus that enables decoupled
4//! communication between workflow tasks. Tasks can publish events
5//! by topic, and other tasks can subscribe to topics with optional
6//! filters. Supports event history, topic-based routing, and
7//! dead letter tracking for undelivered events.
8
9use serde::{Deserialize, Serialize};
10use std::collections::HashMap;
11use std::sync::{Arc, Mutex};
12
13/// Unique identifier for a subscription.
14#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
15pub struct SubscriptionId(u64);
16
17impl SubscriptionId {
18    /// Get the raw numeric ID.
19    #[must_use]
20    pub const fn as_u64(self) -> u64 {
21        self.0
22    }
23}
24
25impl std::fmt::Display for SubscriptionId {
26    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
27        write!(f, "sub-{}", self.0)
28    }
29}
30
31/// An event published on the bus.
32#[derive(Debug, Clone, Serialize, Deserialize)]
33pub struct BusEvent {
34    /// Event topic (e.g. "task.completed", "workflow.error").
35    pub topic: String,
36    /// Source identifier (task ID, workflow ID, etc.).
37    pub source: String,
38    /// Event payload as JSON value.
39    pub payload: serde_json::Value,
40    /// Timestamp in milliseconds since epoch.
41    pub timestamp_ms: u64,
42    /// Optional correlation ID for tracing related events.
43    pub correlation_id: Option<String>,
44    /// Event metadata.
45    pub metadata: HashMap<String, String>,
46}
47
48impl BusEvent {
49    /// Create a new event.
50    #[must_use]
51    pub fn new(
52        topic: impl Into<String>,
53        source: impl Into<String>,
54        payload: serde_json::Value,
55        timestamp_ms: u64,
56    ) -> Self {
57        Self {
58            topic: topic.into(),
59            source: source.into(),
60            payload,
61            timestamp_ms,
62            correlation_id: None,
63            metadata: HashMap::new(),
64        }
65    }
66
67    /// Set correlation ID.
68    #[must_use]
69    pub fn with_correlation_id(mut self, id: impl Into<String>) -> Self {
70        self.correlation_id = Some(id.into());
71        self
72    }
73
74    /// Add metadata.
75    #[must_use]
76    pub fn with_metadata(mut self, key: impl Into<String>, value: impl Into<String>) -> Self {
77        self.metadata.insert(key.into(), value.into());
78        self
79    }
80}
81
82/// Filter criteria for subscriptions.
83#[derive(Debug, Clone)]
84pub struct EventFilter {
85    /// Required source prefix (if set, event source must start with this).
86    pub source_prefix: Option<String>,
87    /// Required metadata keys and values (all must match).
88    pub metadata_match: HashMap<String, String>,
89    /// Minimum payload field value (for numeric comparisons).
90    /// Format: ("field_name", minimum_value).
91    pub min_values: Vec<(String, f64)>,
92}
93
94impl Default for EventFilter {
95    fn default() -> Self {
96        Self {
97            source_prefix: None,
98            metadata_match: HashMap::new(),
99            min_values: Vec::new(),
100        }
101    }
102}
103
104impl EventFilter {
105    /// Create an empty filter (matches everything).
106    #[must_use]
107    pub fn new() -> Self {
108        Self::default()
109    }
110
111    /// Filter by source prefix.
112    #[must_use]
113    pub fn with_source_prefix(mut self, prefix: impl Into<String>) -> Self {
114        self.source_prefix = Some(prefix.into());
115        self
116    }
117
118    /// Require a metadata key-value match.
119    #[must_use]
120    pub fn with_metadata(mut self, key: impl Into<String>, value: impl Into<String>) -> Self {
121        self.metadata_match.insert(key.into(), value.into());
122        self
123    }
124
125    /// Require a minimum value for a payload field.
126    #[must_use]
127    pub fn with_min_value(mut self, field: impl Into<String>, min: f64) -> Self {
128        self.min_values.push((field.into(), min));
129        self
130    }
131
132    /// Check if an event matches this filter.
133    #[must_use]
134    pub fn matches(&self, event: &BusEvent) -> bool {
135        // Check source prefix
136        if let Some(ref prefix) = self.source_prefix {
137            if !event.source.starts_with(prefix) {
138                return false;
139            }
140        }
141
142        // Check metadata
143        for (key, expected) in &self.metadata_match {
144            match event.metadata.get(key) {
145                Some(actual) if actual == expected => {}
146                _ => return false,
147            }
148        }
149
150        // Check minimum values in payload
151        for (field, min_val) in &self.min_values {
152            if let Some(val) = event.payload.get(field).and_then(|v| v.as_f64()) {
153                if val < *min_val {
154                    return false;
155                }
156            } else {
157                return false; // field not found or not a number
158            }
159        }
160
161        true
162    }
163}
164
165/// Internal subscription record.
166#[derive(Debug, Clone)]
167struct Subscription {
168    id: SubscriptionId,
169    topic_pattern: String,
170    filter: EventFilter,
171    /// Delivered events for this subscription.
172    delivered: Vec<BusEvent>,
173    /// Maximum events to retain (0 = unlimited).
174    max_retained: usize,
175    /// Whether this subscription is active.
176    active: bool,
177}
178
179impl Subscription {
180    /// Check if the subscription's topic pattern matches the event topic.
181    fn topic_matches(&self, topic: &str) -> bool {
182        if self.topic_pattern == "*" {
183            return true;
184        }
185        if self.topic_pattern.ends_with(".*") {
186            let prefix = &self.topic_pattern[..self.topic_pattern.len() - 2];
187            return topic.starts_with(prefix);
188        }
189        self.topic_pattern == topic
190    }
191}
192
193/// Dead letter entry for events that had no subscribers.
194#[derive(Debug, Clone, Serialize, Deserialize)]
195pub struct DeadLetterEntry {
196    /// The undelivered event.
197    pub event: BusEvent,
198    /// Reason it was undelivered.
199    pub reason: String,
200}
201
202/// Thread-safe event bus for publish/subscribe communication.
203#[derive(Debug, Clone)]
204pub struct EventBus {
205    inner: Arc<Mutex<EventBusInner>>,
206}
207
208#[derive(Debug)]
209struct EventBusInner {
210    subscriptions: Vec<Subscription>,
211    next_sub_id: u64,
212    /// All published events (for replay/history).
213    event_history: Vec<BusEvent>,
214    /// Events with no matching subscribers.
215    dead_letters: Vec<DeadLetterEntry>,
216    /// Maximum history size (0 = unlimited).
217    max_history: usize,
218    /// Maximum dead letter size.
219    max_dead_letters: usize,
220    /// Total events published.
221    total_published: u64,
222    /// Total deliveries across all subscriptions.
223    total_delivered: u64,
224}
225
226/// Statistics for the event bus.
227#[derive(Debug, Clone)]
228pub struct EventBusStats {
229    /// Number of active subscriptions.
230    pub active_subscriptions: usize,
231    /// Total subscriptions (including inactive).
232    pub total_subscriptions: usize,
233    /// Total events published.
234    pub total_published: u64,
235    /// Total deliveries across all subscriptions.
236    pub total_delivered: u64,
237    /// Events in history.
238    pub history_size: usize,
239    /// Dead letter count.
240    pub dead_letter_count: usize,
241}
242
243/// Configuration for the event bus.
244#[derive(Debug, Clone)]
245pub struct EventBusConfig {
246    /// Maximum event history size (0 = unlimited).
247    pub max_history: usize,
248    /// Maximum dead letter queue size.
249    pub max_dead_letters: usize,
250    /// Default max retained events per subscription.
251    pub default_max_retained: usize,
252}
253
254impl Default for EventBusConfig {
255    fn default() -> Self {
256        Self {
257            max_history: 10_000,
258            max_dead_letters: 1_000,
259            default_max_retained: 100,
260        }
261    }
262}
263
264impl EventBus {
265    /// Create a new event bus with default config.
266    #[must_use]
267    pub fn new() -> Self {
268        Self::with_config(EventBusConfig::default())
269    }
270
271    /// Create a new event bus with custom config.
272    #[must_use]
273    pub fn with_config(config: EventBusConfig) -> Self {
274        Self {
275            inner: Arc::new(Mutex::new(EventBusInner {
276                subscriptions: Vec::new(),
277                next_sub_id: 1,
278                event_history: Vec::new(),
279                dead_letters: Vec::new(),
280                max_history: config.max_history,
281                max_dead_letters: config.max_dead_letters,
282                total_published: 0,
283                total_delivered: 0,
284            })),
285        }
286    }
287
288    /// Subscribe to a topic pattern.
289    ///
290    /// Topic patterns:
291    /// - Exact: `"task.completed"` matches only `"task.completed"`
292    /// - Wildcard: `"task.*"` matches `"task.completed"`, `"task.failed"`, etc.
293    /// - Catch-all: `"*"` matches everything
294    ///
295    /// Returns a subscription ID that can be used to retrieve delivered events
296    /// or unsubscribe.
297    pub fn subscribe(
298        &self,
299        topic_pattern: impl Into<String>,
300        filter: EventFilter,
301    ) -> SubscriptionId {
302        let mut inner = self.inner.lock().unwrap_or_else(|e| e.into_inner());
303        let id = SubscriptionId(inner.next_sub_id);
304        inner.next_sub_id += 1;
305
306        inner.subscriptions.push(Subscription {
307            id,
308            topic_pattern: topic_pattern.into(),
309            filter,
310            delivered: Vec::new(),
311            max_retained: 100,
312            active: true,
313        });
314
315        id
316    }
317
318    /// Subscribe with a custom max retained events count.
319    pub fn subscribe_with_retention(
320        &self,
321        topic_pattern: impl Into<String>,
322        filter: EventFilter,
323        max_retained: usize,
324    ) -> SubscriptionId {
325        let mut inner = self.inner.lock().unwrap_or_else(|e| e.into_inner());
326        let id = SubscriptionId(inner.next_sub_id);
327        inner.next_sub_id += 1;
328
329        inner.subscriptions.push(Subscription {
330            id,
331            topic_pattern: topic_pattern.into(),
332            filter,
333            delivered: Vec::new(),
334            max_retained,
335            active: true,
336        });
337
338        id
339    }
340
341    /// Unsubscribe (marks subscription as inactive).
342    pub fn unsubscribe(&self, sub_id: SubscriptionId) -> bool {
343        let mut inner = self.inner.lock().unwrap_or_else(|e| e.into_inner());
344        if let Some(sub) = inner.subscriptions.iter_mut().find(|s| s.id == sub_id) {
345            sub.active = false;
346            true
347        } else {
348            false
349        }
350    }
351
352    /// Publish an event to the bus.
353    ///
354    /// Returns the number of subscriptions that received the event.
355    pub fn publish(&self, event: BusEvent) -> usize {
356        let mut inner = self.inner.lock().unwrap_or_else(|e| e.into_inner());
357        inner.total_published += 1;
358        let mut delivery_count = 0usize;
359
360        // Deliver to matching active subscriptions
361        for sub in inner.subscriptions.iter_mut() {
362            if !sub.active {
363                continue;
364            }
365            if !sub.topic_matches(&event.topic) {
366                continue;
367            }
368            if !sub.filter.matches(&event) {
369                continue;
370            }
371
372            sub.delivered.push(event.clone());
373            delivery_count += 1;
374
375            // Trim retained events if over limit
376            if sub.max_retained > 0 && sub.delivered.len() > sub.max_retained {
377                let excess = sub.delivered.len() - sub.max_retained;
378                sub.delivered.drain(..excess);
379            }
380        }
381
382        inner.total_delivered += delivery_count as u64;
383
384        // Track dead letters
385        if delivery_count == 0 {
386            inner.dead_letters.push(DeadLetterEntry {
387                event: event.clone(),
388                reason: "no matching subscribers".to_string(),
389            });
390            if inner.max_dead_letters > 0 && inner.dead_letters.len() > inner.max_dead_letters {
391                inner.dead_letters.remove(0);
392            }
393        }
394
395        // Add to history
396        inner.event_history.push(event);
397        if inner.max_history > 0 && inner.event_history.len() > inner.max_history {
398            inner.event_history.remove(0);
399        }
400
401        delivery_count
402    }
403
404    /// Get events delivered to a subscription.
405    #[must_use]
406    pub fn get_events(&self, sub_id: SubscriptionId) -> Vec<BusEvent> {
407        let inner = self.inner.lock().unwrap_or_else(|e| e.into_inner());
408        inner
409            .subscriptions
410            .iter()
411            .find(|s| s.id == sub_id)
412            .map_or_else(Vec::new, |s| s.delivered.clone())
413    }
414
415    /// Drain (consume) events from a subscription, returning and clearing them.
416    pub fn drain_events(&self, sub_id: SubscriptionId) -> Vec<BusEvent> {
417        let mut inner = self.inner.lock().unwrap_or_else(|e| e.into_inner());
418        inner
419            .subscriptions
420            .iter_mut()
421            .find(|s| s.id == sub_id)
422            .map_or_else(Vec::new, |s| std::mem::take(&mut s.delivered))
423    }
424
425    /// Get dead letters.
426    #[must_use]
427    pub fn dead_letters(&self) -> Vec<DeadLetterEntry> {
428        let inner = self.inner.lock().unwrap_or_else(|e| e.into_inner());
429        inner.dead_letters.clone()
430    }
431
432    /// Get event history.
433    #[must_use]
434    pub fn event_history(&self) -> Vec<BusEvent> {
435        let inner = self.inner.lock().unwrap_or_else(|e| e.into_inner());
436        inner.event_history.clone()
437    }
438
439    /// Get events from history matching a specific topic.
440    #[must_use]
441    pub fn events_by_topic(&self, topic: &str) -> Vec<BusEvent> {
442        let inner = self.inner.lock().unwrap_or_else(|e| e.into_inner());
443        inner
444            .event_history
445            .iter()
446            .filter(|e| e.topic == topic)
447            .cloned()
448            .collect()
449    }
450
451    /// Get bus statistics.
452    #[must_use]
453    pub fn stats(&self) -> EventBusStats {
454        let inner = self.inner.lock().unwrap_or_else(|e| e.into_inner());
455        EventBusStats {
456            active_subscriptions: inner.subscriptions.iter().filter(|s| s.active).count(),
457            total_subscriptions: inner.subscriptions.len(),
458            total_published: inner.total_published,
459            total_delivered: inner.total_delivered,
460            history_size: inner.event_history.len(),
461            dead_letter_count: inner.dead_letters.len(),
462        }
463    }
464
465    /// Clear all history, dead letters, and delivered events.
466    pub fn clear(&self) {
467        let mut inner = self.inner.lock().unwrap_or_else(|e| e.into_inner());
468        inner.event_history.clear();
469        inner.dead_letters.clear();
470        for sub in &mut inner.subscriptions {
471            sub.delivered.clear();
472        }
473    }
474
475    /// Replay events from history to a new subscription.
476    ///
477    /// Returns the number of events replayed.
478    pub fn replay_to(&self, sub_id: SubscriptionId) -> usize {
479        let mut inner = self.inner.lock().unwrap_or_else(|e| e.into_inner());
480
481        // Find the subscription index
482        let sub_idx = match inner.subscriptions.iter().position(|s| s.id == sub_id) {
483            Some(idx) => idx,
484            None => return 0,
485        };
486
487        // Clone history to avoid borrow conflicts
488        let history = inner.event_history.clone();
489        let mut count = 0;
490
491        for event in &history {
492            let sub = &inner.subscriptions[sub_idx];
493            if sub.active && sub.topic_matches(&event.topic) && sub.filter.matches(event) {
494                inner.subscriptions[sub_idx].delivered.push(event.clone());
495                count += 1;
496            }
497        }
498
499        count
500    }
501}
502
503impl Default for EventBus {
504    fn default() -> Self {
505        Self::new()
506    }
507}
508
509#[cfg(test)]
510mod tests {
511    use super::*;
512
513    fn make_event(topic: &str, source: &str) -> BusEvent {
514        BusEvent::new(topic, source, serde_json::json!({}), 1000)
515    }
516
517    // --- BusEvent ---
518
519    #[test]
520    fn test_event_creation() {
521        let event = make_event("task.done", "task-1");
522        assert_eq!(event.topic, "task.done");
523        assert_eq!(event.source, "task-1");
524        assert_eq!(event.timestamp_ms, 1000);
525        assert!(event.correlation_id.is_none());
526    }
527
528    #[test]
529    fn test_event_with_correlation() {
530        let event = make_event("task.done", "task-1").with_correlation_id("corr-123");
531        assert_eq!(event.correlation_id, Some("corr-123".to_string()));
532    }
533
534    #[test]
535    fn test_event_with_metadata() {
536        let event = make_event("task.done", "task-1").with_metadata("workflow", "wf-1");
537        assert_eq!(event.metadata.get("workflow"), Some(&"wf-1".to_string()));
538    }
539
540    // --- EventFilter ---
541
542    #[test]
543    fn test_empty_filter_matches_all() {
544        let filter = EventFilter::new();
545        let event = make_event("any.topic", "any-source");
546        assert!(filter.matches(&event));
547    }
548
549    #[test]
550    fn test_source_prefix_filter() {
551        let filter = EventFilter::new().with_source_prefix("task-");
552        assert!(filter.matches(&make_event("x", "task-1")));
553        assert!(!filter.matches(&make_event("x", "workflow-1")));
554    }
555
556    #[test]
557    fn test_metadata_filter() {
558        let filter = EventFilter::new().with_metadata("env", "production");
559        let event = make_event("x", "y").with_metadata("env", "production");
560        assert!(filter.matches(&event));
561
562        let event2 = make_event("x", "y").with_metadata("env", "staging");
563        assert!(!filter.matches(&event2));
564    }
565
566    #[test]
567    fn test_min_value_filter() {
568        let filter = EventFilter::new().with_min_value("quality", 90.0);
569        let event = BusEvent::new("x", "y", serde_json::json!({"quality": 95.0}), 0);
570        assert!(filter.matches(&event));
571
572        let event2 = BusEvent::new("x", "y", serde_json::json!({"quality": 80.0}), 0);
573        assert!(!filter.matches(&event2));
574    }
575
576    #[test]
577    fn test_min_value_missing_field() {
578        let filter = EventFilter::new().with_min_value("quality", 90.0);
579        let event = BusEvent::new("x", "y", serde_json::json!({"other": 95.0}), 0);
580        assert!(!filter.matches(&event));
581    }
582
583    // --- EventBus core ---
584
585    #[test]
586    fn test_bus_subscribe_and_publish() {
587        let bus = EventBus::new();
588        let sub = bus.subscribe("task.done", EventFilter::new());
589        let delivered = bus.publish(make_event("task.done", "task-1"));
590        assert_eq!(delivered, 1);
591
592        let events = bus.get_events(sub);
593        assert_eq!(events.len(), 1);
594        assert_eq!(events[0].source, "task-1");
595    }
596
597    #[test]
598    fn test_bus_no_match() {
599        let bus = EventBus::new();
600        bus.subscribe("task.done", EventFilter::new());
601        let delivered = bus.publish(make_event("workflow.started", "wf-1"));
602        assert_eq!(delivered, 0);
603    }
604
605    #[test]
606    fn test_bus_wildcard_topic() {
607        let bus = EventBus::new();
608        let sub = bus.subscribe("task.*", EventFilter::new());
609        bus.publish(make_event("task.done", "t1"));
610        bus.publish(make_event("task.failed", "t2"));
611        bus.publish(make_event("workflow.started", "wf1"));
612
613        let events = bus.get_events(sub);
614        assert_eq!(events.len(), 2);
615    }
616
617    #[test]
618    fn test_bus_catch_all() {
619        let bus = EventBus::new();
620        let sub = bus.subscribe("*", EventFilter::new());
621        bus.publish(make_event("task.done", "t1"));
622        bus.publish(make_event("workflow.started", "wf1"));
623
624        let events = bus.get_events(sub);
625        assert_eq!(events.len(), 2);
626    }
627
628    #[test]
629    fn test_bus_multiple_subscribers() {
630        let bus = EventBus::new();
631        let sub1 = bus.subscribe("task.done", EventFilter::new());
632        let sub2 = bus.subscribe("task.done", EventFilter::new());
633        let sub3 = bus.subscribe("task.failed", EventFilter::new());
634
635        let delivered = bus.publish(make_event("task.done", "t1"));
636        assert_eq!(delivered, 2);
637        assert_eq!(bus.get_events(sub1).len(), 1);
638        assert_eq!(bus.get_events(sub2).len(), 1);
639        assert_eq!(bus.get_events(sub3).len(), 0);
640    }
641
642    #[test]
643    fn test_bus_unsubscribe() {
644        let bus = EventBus::new();
645        let sub = bus.subscribe("task.done", EventFilter::new());
646
647        bus.publish(make_event("task.done", "t1"));
648        assert_eq!(bus.get_events(sub).len(), 1);
649
650        assert!(bus.unsubscribe(sub));
651        bus.publish(make_event("task.done", "t2"));
652        // No new events after unsubscribe
653        assert_eq!(bus.get_events(sub).len(), 1);
654    }
655
656    #[test]
657    fn test_bus_unsubscribe_nonexistent() {
658        let bus = EventBus::new();
659        assert!(!bus.unsubscribe(SubscriptionId(999)));
660    }
661
662    #[test]
663    fn test_bus_drain_events() {
664        let bus = EventBus::new();
665        let sub = bus.subscribe("task.done", EventFilter::new());
666        bus.publish(make_event("task.done", "t1"));
667        bus.publish(make_event("task.done", "t2"));
668
669        let drained = bus.drain_events(sub);
670        assert_eq!(drained.len(), 2);
671        assert_eq!(bus.get_events(sub).len(), 0);
672    }
673
674    // --- Dead letters ---
675
676    #[test]
677    fn test_dead_letters() {
678        let bus = EventBus::new();
679        // No subscribers, so event goes to dead letters
680        bus.publish(make_event("orphan.event", "src"));
681        let dead = bus.dead_letters();
682        assert_eq!(dead.len(), 1);
683        assert_eq!(dead[0].event.topic, "orphan.event");
684    }
685
686    #[test]
687    fn test_dead_letters_not_for_delivered() {
688        let bus = EventBus::new();
689        bus.subscribe("task.done", EventFilter::new());
690        bus.publish(make_event("task.done", "t1"));
691        assert!(bus.dead_letters().is_empty());
692    }
693
694    // --- History ---
695
696    #[test]
697    fn test_event_history() {
698        let bus = EventBus::new();
699        bus.publish(make_event("a", "s1"));
700        bus.publish(make_event("b", "s2"));
701
702        let history = bus.event_history();
703        assert_eq!(history.len(), 2);
704    }
705
706    #[test]
707    fn test_events_by_topic() {
708        let bus = EventBus::new();
709        bus.publish(make_event("task.done", "t1"));
710        bus.publish(make_event("task.failed", "t2"));
711        bus.publish(make_event("task.done", "t3"));
712
713        let done_events = bus.events_by_topic("task.done");
714        assert_eq!(done_events.len(), 2);
715    }
716
717    // --- Stats ---
718
719    #[test]
720    fn test_stats() {
721        let bus = EventBus::new();
722        let _sub1 = bus.subscribe("task.done", EventFilter::new());
723        let sub2 = bus.subscribe("task.done", EventFilter::new());
724        bus.unsubscribe(sub2);
725
726        bus.publish(make_event("task.done", "t1"));
727        bus.publish(make_event("orphan", "s1"));
728
729        let stats = bus.stats();
730        assert_eq!(stats.active_subscriptions, 1);
731        assert_eq!(stats.total_subscriptions, 2);
732        assert_eq!(stats.total_published, 2);
733        assert_eq!(stats.total_delivered, 1);
734        assert_eq!(stats.history_size, 2);
735        assert_eq!(stats.dead_letter_count, 1);
736    }
737
738    // --- Retention limits ---
739
740    #[test]
741    fn test_retention_limit() {
742        let bus = EventBus::new();
743        let sub = bus.subscribe_with_retention("task.done", EventFilter::new(), 3);
744
745        for i in 0..5 {
746            bus.publish(make_event("task.done", &format!("t{i}")));
747        }
748
749        let events = bus.get_events(sub);
750        assert_eq!(events.len(), 3);
751        // Should have the latest 3
752        assert_eq!(events[0].source, "t2");
753        assert_eq!(events[2].source, "t4");
754    }
755
756    // --- Clear ---
757
758    #[test]
759    fn test_clear() {
760        let bus = EventBus::new();
761        let sub = bus.subscribe("task.done", EventFilter::new());
762        bus.publish(make_event("task.done", "t1"));
763        bus.publish(make_event("orphan", "s1"));
764
765        bus.clear();
766        assert!(bus.event_history().is_empty());
767        assert!(bus.dead_letters().is_empty());
768        assert!(bus.get_events(sub).is_empty());
769    }
770
771    // --- Replay ---
772
773    #[test]
774    fn test_replay_to_subscription() {
775        let bus = EventBus::new();
776
777        // Publish before subscribing
778        bus.publish(make_event("task.done", "t1"));
779        bus.publish(make_event("task.done", "t2"));
780        bus.publish(make_event("workflow.started", "wf1"));
781
782        // Late subscriber
783        let sub = bus.subscribe("task.done", EventFilter::new());
784        assert_eq!(bus.get_events(sub).len(), 0);
785
786        // Replay history
787        let replayed = bus.replay_to(sub);
788        assert_eq!(replayed, 2);
789        assert_eq!(bus.get_events(sub).len(), 2);
790    }
791
792    // --- Filter combined with topic ---
793
794    #[test]
795    fn test_topic_and_filter_combined() {
796        let bus = EventBus::new();
797        let filter = EventFilter::new().with_source_prefix("encoder-");
798        let sub = bus.subscribe("task.done", filter);
799
800        bus.publish(make_event("task.done", "encoder-1"));
801        bus.publish(make_event("task.done", "decoder-1"));
802
803        let events = bus.get_events(sub);
804        assert_eq!(events.len(), 1);
805        assert_eq!(events[0].source, "encoder-1");
806    }
807
808    #[test]
809    fn test_subscription_id_display() {
810        let id = SubscriptionId(42);
811        assert_eq!(id.to_string(), "sub-42");
812        assert_eq!(id.as_u64(), 42);
813    }
814
815    // --- Thread safety ---
816
817    #[test]
818    fn test_bus_is_clone_and_send() {
819        let bus = EventBus::new();
820        let bus2 = bus.clone();
821        let sub = bus.subscribe("test", EventFilter::new());
822        bus2.publish(make_event("test", "src"));
823        assert_eq!(bus.get_events(sub).len(), 1);
824    }
825}