Skip to main content

ipfrs_network/
event_bus.rs

1//! NetworkEventBus — synchronous in-process publish-subscribe bus for network events.
2//!
3//! Decouples protocol handlers from application logic by providing a simple,
4//! zero-dependency pub/sub mechanism with per-subscriber event queues and
5//! configurable event filtering.
6
7use std::collections::{HashMap, VecDeque};
8
9// ---------------------------------------------------------------------------
10// NetworkEvent
11// ---------------------------------------------------------------------------
12
13/// All events that can be emitted by the network layer.
14#[derive(Clone, Debug)]
15pub enum NetworkEvent {
16    /// A remote peer successfully connected.
17    PeerConnected { peer_id: String, address: String },
18    /// A remote peer disconnected.
19    PeerDisconnected { peer_id: String, reason: String },
20    /// A block was received from a remote peer.
21    BlockReceived {
22        from: String,
23        cid: String,
24        size_bytes: usize,
25    },
26    /// A remote peer requested a block from us.
27    BlockRequested { from: String, cid: String },
28    /// A DHT provider record was found for a given CID.
29    DhtProviderFound { cid: String, provider: String },
30    /// A gossip message was received on a topic.
31    GossipMessage {
32        topic: String,
33        from: String,
34        payload_len: usize,
35    },
36    /// A dial attempt to a peer failed.
37    DialFailed {
38        peer_id: String,
39        address: String,
40        error: String,
41    },
42}
43
44// ---------------------------------------------------------------------------
45// EventFilter
46// ---------------------------------------------------------------------------
47
48/// Determines which events a subscription will receive.
49#[derive(Clone, Debug)]
50pub enum EventFilter {
51    /// Receive every event.
52    All,
53    /// Receive only peer-related events: `PeerConnected`, `PeerDisconnected`, `DialFailed`.
54    PeerEvents,
55    /// Receive only block-related events: `BlockReceived`, `BlockRequested`.
56    BlockEvents,
57    /// Receive only DHT events: `DhtProviderFound`.
58    DhtEvents,
59    /// Receive only gossip events: `GossipMessage`.
60    GossipEvents,
61}
62
63// ---------------------------------------------------------------------------
64// Subscription
65// ---------------------------------------------------------------------------
66
67/// Maximum number of events that can be buffered per subscription before
68/// events are dropped.
69const MAX_QUEUE_CAPACITY: usize = 200;
70
71/// A single subscription to the `NetworkEventBus`.
72#[derive(Debug)]
73pub struct Subscription {
74    /// Unique subscription identifier.
75    pub id: u64,
76    /// Filter controlling which events are delivered.
77    pub filter: EventFilter,
78    /// Buffered events waiting to be drained by the subscriber.
79    pub queue: VecDeque<NetworkEvent>,
80    /// Number of events dropped because the queue was full.
81    pub dropped: u64,
82}
83
84impl Subscription {
85    fn new(id: u64, filter: EventFilter) -> Self {
86        Self {
87            id,
88            filter,
89            queue: VecDeque::new(),
90            dropped: 0,
91        }
92    }
93
94    /// Returns `true` if this subscription should receive `event`.
95    pub fn matches(&self, event: &NetworkEvent) -> bool {
96        match &self.filter {
97            EventFilter::All => true,
98            EventFilter::PeerEvents => matches!(
99                event,
100                NetworkEvent::PeerConnected { .. }
101                    | NetworkEvent::PeerDisconnected { .. }
102                    | NetworkEvent::DialFailed { .. }
103            ),
104            EventFilter::BlockEvents => matches!(
105                event,
106                NetworkEvent::BlockReceived { .. } | NetworkEvent::BlockRequested { .. }
107            ),
108            EventFilter::DhtEvents => matches!(event, NetworkEvent::DhtProviderFound { .. }),
109            EventFilter::GossipEvents => matches!(event, NetworkEvent::GossipMessage { .. }),
110        }
111    }
112}
113
114// ---------------------------------------------------------------------------
115// BusStats
116// ---------------------------------------------------------------------------
117
118/// Aggregate statistics for the `NetworkEventBus`.
119#[derive(Clone, Debug, Default)]
120pub struct BusStats {
121    /// Total number of `publish` calls made.
122    pub total_published: u64,
123    /// Total number of event copies successfully enqueued across all subscriptions.
124    pub total_delivered: u64,
125    /// Total number of event copies dropped because a subscription queue was full.
126    pub total_dropped: u64,
127    /// Current number of active subscriptions.
128    pub subscriber_count: usize,
129}
130
131// ---------------------------------------------------------------------------
132// NetworkEventBus
133// ---------------------------------------------------------------------------
134
135/// Synchronous in-process publish-subscribe bus for `NetworkEvent`s.
136///
137/// # Example
138///
139/// ```rust
140/// use ipfrs_network::event_bus::{NetworkEventBus, NetworkEvent, EventFilter};
141///
142/// let mut bus = NetworkEventBus::new();
143/// let id = bus.subscribe(EventFilter::PeerEvents);
144///
145/// bus.publish(NetworkEvent::PeerConnected {
146///     peer_id: "peer1".into(),
147///     address: "/ip4/1.2.3.4/tcp/4001".into(),
148/// });
149///
150/// let events = bus.drain(id);
151/// assert_eq!(events.len(), 1);
152/// ```
153#[derive(Debug, Default)]
154pub struct NetworkEventBus {
155    subscriptions: HashMap<u64, Subscription>,
156    next_id: u64,
157    stats: BusStats,
158}
159
160impl NetworkEventBus {
161    /// Create a new, empty `NetworkEventBus`.
162    pub fn new() -> Self {
163        Self::default()
164    }
165
166    /// Subscribe to events matching `filter`.
167    ///
168    /// Returns a unique subscription id that can be used to drain events or
169    /// unsubscribe later.
170    pub fn subscribe(&mut self, filter: EventFilter) -> u64 {
171        let id = self.next_id;
172        self.next_id += 1;
173        self.subscriptions.insert(id, Subscription::new(id, filter));
174        id
175    }
176
177    /// Remove the subscription identified by `id`.
178    ///
179    /// Returns `true` if the subscription existed, `false` otherwise.
180    pub fn unsubscribe(&mut self, id: u64) -> bool {
181        self.subscriptions.remove(&id).is_some()
182    }
183
184    /// Publish an event to all matching subscribers.
185    ///
186    /// Each matching subscriber receives its own clone of `event`.  If a
187    /// subscriber's queue is full (`>= 200` entries) the event is dropped and
188    /// both the per-subscription and global drop counters are incremented.
189    pub fn publish(&mut self, event: NetworkEvent) {
190        self.stats.total_published += 1;
191
192        for sub in self.subscriptions.values_mut() {
193            if sub.matches(&event) {
194                if sub.queue.len() < MAX_QUEUE_CAPACITY {
195                    sub.queue.push_back(event.clone());
196                    self.stats.total_delivered += 1;
197                } else {
198                    sub.dropped += 1;
199                    self.stats.total_dropped += 1;
200                }
201            }
202        }
203    }
204
205    /// Drain all buffered events for the subscription identified by `id`.
206    ///
207    /// Returns an empty `Vec` if the id does not exist.
208    pub fn drain(&mut self, id: u64) -> Vec<NetworkEvent> {
209        match self.subscriptions.get_mut(&id) {
210            Some(sub) => sub.queue.drain(..).collect(),
211            None => Vec::new(),
212        }
213    }
214
215    /// Return the number of events currently buffered for subscription `id`.
216    ///
217    /// Returns `0` for unknown ids.
218    pub fn peek_count(&self, id: u64) -> usize {
219        self.subscriptions
220            .get(&id)
221            .map(|s| s.queue.len())
222            .unwrap_or(0)
223    }
224
225    /// Return a snapshot of the current bus statistics.
226    pub fn stats(&self) -> BusStats {
227        BusStats {
228            subscriber_count: self.subscriptions.len(),
229            ..self.stats.clone()
230        }
231    }
232
233    /// Drain all queues for all active subscriptions, discarding pending events.
234    pub fn clear_all_queues(&mut self) {
235        for sub in self.subscriptions.values_mut() {
236            sub.queue.clear();
237        }
238    }
239}
240
241// ---------------------------------------------------------------------------
242// Tests
243// ---------------------------------------------------------------------------
244
245#[cfg(test)]
246mod tests {
247    use super::*;
248
249    fn peer_connected() -> NetworkEvent {
250        NetworkEvent::PeerConnected {
251            peer_id: "peer1".into(),
252            address: "/ip4/127.0.0.1/tcp/4001".into(),
253        }
254    }
255
256    fn peer_disconnected() -> NetworkEvent {
257        NetworkEvent::PeerDisconnected {
258            peer_id: "peer1".into(),
259            reason: "timeout".into(),
260        }
261    }
262
263    fn block_received() -> NetworkEvent {
264        NetworkEvent::BlockReceived {
265            from: "peer1".into(),
266            cid: "bafy123".into(),
267            size_bytes: 1024,
268        }
269    }
270
271    fn block_requested() -> NetworkEvent {
272        NetworkEvent::BlockRequested {
273            from: "peer2".into(),
274            cid: "bafy456".into(),
275        }
276    }
277
278    fn dht_provider_found() -> NetworkEvent {
279        NetworkEvent::DhtProviderFound {
280            cid: "bafy789".into(),
281            provider: "peer3".into(),
282        }
283    }
284
285    fn gossip_message() -> NetworkEvent {
286        NetworkEvent::GossipMessage {
287            topic: "blocks".into(),
288            from: "peer4".into(),
289            payload_len: 256,
290        }
291    }
292
293    fn dial_failed() -> NetworkEvent {
294        NetworkEvent::DialFailed {
295            peer_id: "peer5".into(),
296            address: "/ip4/1.2.3.4/tcp/4001".into(),
297            error: "connection refused".into(),
298        }
299    }
300
301    // 1. new() empty state
302    #[test]
303    fn test_new_empty_state() {
304        let bus = NetworkEventBus::new();
305        let stats = bus.stats();
306        assert_eq!(stats.total_published, 0);
307        assert_eq!(stats.total_delivered, 0);
308        assert_eq!(stats.total_dropped, 0);
309        assert_eq!(stats.subscriber_count, 0);
310    }
311
312    // 2. subscribe() returns unique IDs
313    #[test]
314    fn test_subscribe_unique_ids() {
315        let mut bus = NetworkEventBus::new();
316        let id1 = bus.subscribe(EventFilter::All);
317        let id2 = bus.subscribe(EventFilter::All);
318        let id3 = bus.subscribe(EventFilter::PeerEvents);
319        assert_ne!(id1, id2);
320        assert_ne!(id2, id3);
321        assert_ne!(id1, id3);
322    }
323
324    // 3. unsubscribe() returns true/false
325    #[test]
326    fn test_unsubscribe_true_false() {
327        let mut bus = NetworkEventBus::new();
328        let id = bus.subscribe(EventFilter::All);
329        assert!(bus.unsubscribe(id));
330        assert!(!bus.unsubscribe(id)); // already removed
331        assert!(!bus.unsubscribe(9999)); // never existed
332    }
333
334    // 4. publish() delivers to All filter
335    #[test]
336    fn test_publish_delivers_to_all_filter() {
337        let mut bus = NetworkEventBus::new();
338        let id = bus.subscribe(EventFilter::All);
339        bus.publish(peer_connected());
340        bus.publish(block_received());
341        bus.publish(dht_provider_found());
342        assert_eq!(bus.peek_count(id), 3);
343    }
344
345    // 5. publish() PeerEvents filter matches PeerConnected
346    #[test]
347    fn test_peer_events_filter_matches_peer_connected() {
348        let mut bus = NetworkEventBus::new();
349        let id = bus.subscribe(EventFilter::PeerEvents);
350        bus.publish(peer_connected());
351        assert_eq!(bus.peek_count(id), 1);
352    }
353
354    // 6. publish() PeerEvents filter ignores BlockReceived
355    #[test]
356    fn test_peer_events_filter_ignores_block_received() {
357        let mut bus = NetworkEventBus::new();
358        let id = bus.subscribe(EventFilter::PeerEvents);
359        bus.publish(block_received());
360        assert_eq!(bus.peek_count(id), 0);
361    }
362
363    // 7. publish() BlockEvents filter matches BlockReceived and BlockRequested
364    #[test]
365    fn test_block_events_filter_matches_block_events() {
366        let mut bus = NetworkEventBus::new();
367        let id = bus.subscribe(EventFilter::BlockEvents);
368        bus.publish(block_received());
369        bus.publish(block_requested());
370        bus.publish(peer_connected()); // should be ignored
371        assert_eq!(bus.peek_count(id), 2);
372    }
373
374    // 8. publish() DhtEvents filter matches DhtProviderFound only
375    #[test]
376    fn test_dht_events_filter_matches_dht_provider_found_only() {
377        let mut bus = NetworkEventBus::new();
378        let id = bus.subscribe(EventFilter::DhtEvents);
379        bus.publish(dht_provider_found());
380        bus.publish(block_received());
381        bus.publish(peer_connected());
382        assert_eq!(bus.peek_count(id), 1);
383    }
384
385    // 9. publish() GossipEvents filter matches GossipMessage only
386    #[test]
387    fn test_gossip_events_filter_matches_gossip_message_only() {
388        let mut bus = NetworkEventBus::new();
389        let id = bus.subscribe(EventFilter::GossipEvents);
390        bus.publish(gossip_message());
391        bus.publish(block_received());
392        bus.publish(dht_provider_found());
393        assert_eq!(bus.peek_count(id), 1);
394    }
395
396    // 10. drain() returns buffered events
397    #[test]
398    fn test_drain_returns_buffered_events() {
399        let mut bus = NetworkEventBus::new();
400        let id = bus.subscribe(EventFilter::All);
401        bus.publish(peer_connected());
402        bus.publish(block_received());
403        let events = bus.drain(id);
404        assert_eq!(events.len(), 2);
405        // Queue is now empty
406        assert_eq!(bus.peek_count(id), 0);
407    }
408
409    // 11. drain() unknown id returns empty vec
410    #[test]
411    fn test_drain_unknown_id_returns_empty() {
412        let mut bus = NetworkEventBus::new();
413        let events = bus.drain(9999);
414        assert!(events.is_empty());
415    }
416
417    // 12. peek_count() accurate
418    #[test]
419    fn test_peek_count_accurate() {
420        let mut bus = NetworkEventBus::new();
421        let id = bus.subscribe(EventFilter::All);
422        assert_eq!(bus.peek_count(id), 0);
423        bus.publish(peer_connected());
424        assert_eq!(bus.peek_count(id), 1);
425        bus.publish(block_received());
426        assert_eq!(bus.peek_count(id), 2);
427        bus.drain(id);
428        assert_eq!(bus.peek_count(id), 0);
429    }
430
431    // 13. queue bounded at 200, overflow increments dropped
432    #[test]
433    fn test_queue_bounded_at_200_overflow_drops() {
434        let mut bus = NetworkEventBus::new();
435        let id = bus.subscribe(EventFilter::All);
436
437        for _ in 0..205 {
438            bus.publish(peer_connected());
439        }
440
441        assert_eq!(bus.peek_count(id), 200);
442
443        let sub = bus.subscriptions.get(&id).expect("subscription must exist");
444        assert_eq!(sub.dropped, 5);
445        assert_eq!(bus.stats().total_dropped, 5);
446    }
447
448    // 14. stats totals updated correctly
449    #[test]
450    fn test_stats_totals_updated_correctly() {
451        let mut bus = NetworkEventBus::new();
452        let id1 = bus.subscribe(EventFilter::All);
453        let id2 = bus.subscribe(EventFilter::PeerEvents);
454
455        bus.publish(peer_connected()); // delivered to both → 2 delivered
456        bus.publish(block_received()); // delivered to id1 only → 1 delivered
457
458        let stats = bus.stats();
459        assert_eq!(stats.total_published, 2);
460        assert_eq!(stats.total_delivered, 3);
461        assert_eq!(stats.total_dropped, 0);
462        assert_eq!(stats.subscriber_count, 2);
463
464        let _ = (id1, id2); // suppress unused warnings
465    }
466
467    // 15. clear_all_queues empties all queues
468    #[test]
469    fn test_clear_all_queues_empties_all() {
470        let mut bus = NetworkEventBus::new();
471        let id1 = bus.subscribe(EventFilter::All);
472        let id2 = bus.subscribe(EventFilter::BlockEvents);
473        bus.publish(peer_connected());
474        bus.publish(block_received());
475        assert!(bus.peek_count(id1) > 0);
476        assert!(bus.peek_count(id2) > 0);
477
478        bus.clear_all_queues();
479
480        assert_eq!(bus.peek_count(id1), 0);
481        assert_eq!(bus.peek_count(id2), 0);
482    }
483
484    // 16. multiple subscribers receive same event (each gets own copy)
485    #[test]
486    fn test_multiple_subscribers_each_get_copy() {
487        let mut bus = NetworkEventBus::new();
488        let ids: Vec<u64> = (0..5).map(|_| bus.subscribe(EventFilter::All)).collect();
489
490        bus.publish(peer_connected());
491
492        for id in &ids {
493            assert_eq!(
494                bus.peek_count(*id),
495                1,
496                "subscriber {id} should have 1 event"
497            );
498        }
499    }
500
501    // 17. unsubscribed id no longer receives events
502    #[test]
503    fn test_unsubscribed_id_no_longer_receives_events() {
504        let mut bus = NetworkEventBus::new();
505        let id = bus.subscribe(EventFilter::All);
506        bus.publish(peer_connected());
507        assert_eq!(bus.peek_count(id), 1);
508
509        assert!(bus.unsubscribe(id));
510        bus.publish(block_received()); // should not be delivered
511
512        // After unsubscribe peek_count returns 0 (id is gone)
513        assert_eq!(bus.peek_count(id), 0);
514    }
515
516    // 18. PeerEvents filter also matches PeerDisconnected and DialFailed
517    #[test]
518    fn test_peer_events_filter_matches_all_peer_variants() {
519        let mut bus = NetworkEventBus::new();
520        let id = bus.subscribe(EventFilter::PeerEvents);
521        bus.publish(peer_connected());
522        bus.publish(peer_disconnected());
523        bus.publish(dial_failed());
524        bus.publish(gossip_message()); // should not match
525        assert_eq!(bus.peek_count(id), 3);
526    }
527
528    // 19. subscriber_count reflects current subscriptions
529    #[test]
530    fn test_subscriber_count_reflects_current() {
531        let mut bus = NetworkEventBus::new();
532        assert_eq!(bus.stats().subscriber_count, 0);
533        let id1 = bus.subscribe(EventFilter::All);
534        assert_eq!(bus.stats().subscriber_count, 1);
535        let id2 = bus.subscribe(EventFilter::BlockEvents);
536        assert_eq!(bus.stats().subscriber_count, 2);
537        bus.unsubscribe(id1);
538        assert_eq!(bus.stats().subscriber_count, 1);
539        bus.unsubscribe(id2);
540        assert_eq!(bus.stats().subscriber_count, 0);
541    }
542}