Skip to main content

nautilus_common/msgbus/
typed_router.rs

1// -------------------------------------------------------------------------------------------------
2//  Copyright (C) 2015-2026 Nautech Systems Pty Ltd. All rights reserved.
3//  https://nautechsystems.io
4//
5//  Licensed under the GNU Lesser General Public License Version 3.0 (the "License");
6//  You may not use this file except in compliance with the License.
7//  You may obtain a copy of the License at https://www.gnu.org/licenses/lgpl-3.0.en.html
8//
9//  Unless required by applicable law or agreed to in writing, software
10//  distributed under the License is distributed on an "AS IS" BASIS,
11//  WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12//  See the License for the specific language governing permissions and
13//  limitations under the License.
14// -------------------------------------------------------------------------------------------------
15
16//! Type-safe topic routing for pub/sub messaging.
17//!
18//! This module provides [`TopicRouter<T>`] for routing messages of a specific type
19//! to subscribed handlers based on topic patterns.
20
21use std::{
22    cmp::Ordering,
23    fmt::Debug,
24    hash::{Hash, Hasher},
25};
26
27use indexmap::{Equivalent, IndexMap, IndexSet};
28use smallvec::SmallVec;
29use ustr::Ustr;
30
31use super::{
32    matching::is_matching_backtracking,
33    mstr::{MStr, Pattern, Topic},
34    typed_handler::TypedHandler,
35};
36
37/// A typed subscription for pub/sub messaging.
38///
39/// Associates a handler with a topic pattern and priority.
40#[derive(Clone)]
41pub struct TypedSubscription<T: 'static> {
42    /// The typed message handler.
43    pub handler: TypedHandler<T>,
44    /// Cached handler ID for faster equality checks.
45    pub handler_id: Ustr,
46    /// The pattern for matching topics.
47    pub pattern: MStr<Pattern>,
48    /// Higher priority handlers receive messages first.
49    pub priority: u32,
50}
51
52impl<T: 'static> TypedSubscription<T> {
53    /// Creates a new typed subscription.
54    #[must_use]
55    pub fn new(pattern: MStr<Pattern>, handler: TypedHandler<T>, priority: Option<u32>) -> Self {
56        Self {
57            handler_id: handler.id(),
58            pattern,
59            handler,
60            priority: priority.unwrap_or(0),
61        }
62    }
63
64    fn delivery_order(&self, other: &Self) -> Ordering {
65        other
66            .priority
67            .cmp(&self.priority)
68            .then_with(|| self.pattern.cmp(&other.pattern))
69            .then_with(|| self.handler_id.cmp(&other.handler_id))
70    }
71}
72
73impl<T: 'static> Debug for TypedSubscription<T> {
74    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
75        f.debug_struct(stringify!(TypedSubscription))
76            .field("handler_id", &self.handler_id)
77            .field("pattern", &self.pattern)
78            .field("priority", &self.priority)
79            .field("type", &std::any::type_name::<T>())
80            .finish()
81    }
82}
83
84impl<T: 'static> PartialEq for TypedSubscription<T> {
85    fn eq(&self, other: &Self) -> bool {
86        self.pattern == other.pattern && self.handler_id == other.handler_id
87    }
88}
89
90impl<T: 'static> Eq for TypedSubscription<T> {}
91
92impl<T: 'static> PartialOrd for TypedSubscription<T> {
93    fn partial_cmp(&self, other: &Self) -> Option<Ordering> {
94        Some(self.cmp(other))
95    }
96}
97
98impl<T: 'static> Ord for TypedSubscription<T> {
99    fn cmp(&self, other: &Self) -> Ordering {
100        self.pattern
101            .cmp(&other.pattern)
102            .then_with(|| self.handler_id.cmp(&other.handler_id))
103    }
104}
105
106impl<T: 'static> Hash for TypedSubscription<T> {
107    fn hash<H: Hasher>(&self, state: &mut H) {
108        self.pattern.hash(state);
109        self.handler_id.hash(state);
110    }
111}
112
113impl<T: 'static> Equivalent<TypedSubscription<T>> for (MStr<Pattern>, Ustr) {
114    fn equivalent(&self, other: &TypedSubscription<T>) -> bool {
115        self.0 == other.pattern && self.1 == other.handler_id
116    }
117}
118
119/// Routes messages of type `T` to subscribed handlers based on topic patterns.
120///
121/// Supports wildcard patterns (`*` and `?`) and priority-based ordering.
122/// Caches topic-to-subscription mappings for efficient repeated lookups.
123#[derive(Debug)]
124pub struct TopicRouter<T: 'static> {
125    /// All active subscriptions.
126    pub(crate) subscriptions: IndexSet<TypedSubscription<T>>,
127    /// Cache mapping topics to matching subscription indices (inline for ≤64 handlers).
128    topic_cache: IndexMap<MStr<Topic>, SmallVec<[usize; 64]>>,
129}
130
131impl<T: 'static> Default for TopicRouter<T> {
132    fn default() -> Self {
133        Self::new()
134    }
135}
136
137impl<T: 'static> TopicRouter<T> {
138    /// Creates a new empty topic router.
139    #[must_use]
140    pub fn new() -> Self {
141        Self {
142            subscriptions: IndexSet::new(),
143            topic_cache: IndexMap::new(),
144        }
145    }
146
147    /// Returns the number of active subscriptions.
148    #[must_use]
149    pub fn subscription_count(&self) -> usize {
150        self.subscriptions.len()
151    }
152
153    /// Returns whether there are any subscriptions.
154    #[must_use]
155    pub fn is_empty(&self) -> bool {
156        self.subscriptions.is_empty()
157    }
158
159    /// Returns all active subscription patterns.
160    #[must_use]
161    pub fn patterns(&self) -> Vec<&str> {
162        self.ordered_subscriptions()
163            .into_iter()
164            .map(|s| s.pattern.as_str())
165            .collect()
166    }
167
168    /// Returns all subscription handler IDs.
169    #[must_use]
170    pub fn handler_ids(&self) -> Vec<&str> {
171        self.ordered_subscriptions()
172            .into_iter()
173            .map(|s| s.handler_id.as_str())
174            .collect()
175    }
176
177    /// Subscribes a handler to a topic pattern.
178    ///
179    /// # Warning
180    ///
181    /// Assigning priority is an advanced feature. Higher priority handlers
182    /// receive messages before lower priority handlers.
183    pub fn subscribe(&mut self, pattern: MStr<Pattern>, handler: TypedHandler<T>, priority: u32) {
184        let sub = TypedSubscription::new(pattern, handler, Some(priority));
185
186        // Re-subscribing the same handler is expected (e.g. book deltas + snapshots
187        // share one BookUpdater), so dedup at debug rather than warn.
188        if self.subscriptions.contains(&sub) {
189            log::debug!("{sub:?} already exists; skipping duplicate subscription");
190            return;
191        }
192
193        log::debug!("Subscribing {sub:?}");
194
195        self.subscriptions.insert(sub);
196        self.topic_cache.clear();
197    }
198
199    /// Unsubscribes a handler from a topic pattern.
200    pub fn unsubscribe(&mut self, pattern: MStr<Pattern>, handler: &TypedHandler<T>) {
201        log::debug!(
202            "Unsubscribing handler {} from pattern '{pattern}'",
203            handler.id()
204        );
205
206        let handler_id = handler.id();
207
208        if self.subscriptions.swap_remove(&(pattern, handler_id)) {
209            // Must clear entire cache since swap_remove() can move an index
210            self.topic_cache.clear();
211
212            log::debug!("Handler for pattern '{pattern}' was removed");
213        } else {
214            log::debug!("No matching handler for pattern '{pattern}' was found");
215        }
216    }
217
218    /// Removes a specific handler from a pattern by handler ID.
219    pub fn remove_handler(&mut self, pattern: MStr<Pattern>, handler_id: Ustr) {
220        if self.subscriptions.swap_remove(&(pattern, handler_id)) {
221            // Must clear entire cache since swap_remove() can move an index
222            self.topic_cache.clear();
223            log::debug!("Handler {handler_id} for pattern '{pattern}' was removed");
224        }
225    }
226
227    /// Checks if a handler is subscribed to a pattern.
228    #[must_use]
229    pub fn is_subscribed(&self, pattern: MStr<Pattern>, handler: &TypedHandler<T>) -> bool {
230        let handler_id = handler.id();
231        self.subscriptions.contains(&(pattern, handler_id))
232    }
233
234    /// Returns whether there are subscribers for the topic.
235    #[must_use]
236    pub fn has_subscribers(&self, topic: MStr<Topic>) -> bool {
237        self.get_matching_indices(topic).map_or_else(
238            || !Self::find_matches(&self.subscriptions, topic).is_empty(),
239            |indices| !indices.is_empty(),
240        )
241    }
242
243    /// Returns the count of subscribers for a topic.
244    #[must_use]
245    pub fn subscriber_count(&self, topic: MStr<Topic>) -> usize {
246        self.get_matching_indices(topic).map_or_else(
247            || Self::find_matches(&self.subscriptions, topic).len(),
248            <[usize]>::len,
249        )
250    }
251
252    /// Returns the count of subscribers with an exact topic match,
253    /// excluding wildcard pattern subscriptions.
254    #[must_use]
255    pub fn exact_subscriber_count(&self, topic: MStr<Topic>) -> usize {
256        let pattern: MStr<Pattern> = topic.into();
257        self.subscriptions
258            .iter()
259            .filter(|s| s.pattern == pattern)
260            .count()
261    }
262
263    /// Publishes a message to all handlers subscribed to matching patterns.
264    pub fn publish(&mut self, topic: MStr<Topic>, message: &T) {
265        // Split borrow to avoid copying indices
266        let Self {
267            subscriptions,
268            topic_cache,
269        } = self;
270
271        let indices = topic_cache
272            .entry(topic)
273            .or_insert_with(|| Self::find_matches(subscriptions, topic));
274        let subscriptions = subscriptions.as_slice();
275
276        for &idx in indices.iter() {
277            subscriptions[idx].handler.handle(message);
278        }
279    }
280
281    /// Returns cloned handlers matching a topic for safe out-of-borrow calling.
282    ///
283    /// Use this when handlers may need to access the message bus during execution.
284    /// Note: Allocates a Vec on each call. For hot paths, prefer the thread-local
285    /// buffer pattern used by `publish_*` functions.
286    pub fn get_matching_handlers(&mut self, topic: MStr<Topic>) -> Vec<TypedHandler<T>> {
287        let indices: SmallVec<[usize; 64]> = self
288            .get_or_compute_matching_indices(topic)
289            .iter()
290            .copied()
291            .collect();
292        indices
293            .into_iter()
294            .map(|idx| self.subscriptions[idx].handler.clone())
295            .collect()
296    }
297
298    /// Gets cached matching indices for a topic, if available.
299    fn get_matching_indices(&self, topic: MStr<Topic>) -> Option<&[usize]> {
300        self.topic_cache.get(&topic).map(SmallVec::as_slice)
301    }
302
303    /// Gets or computes matching subscription indices for a topic.
304    fn get_or_compute_matching_indices(&mut self, topic: MStr<Topic>) -> &[usize] {
305        let Self {
306            subscriptions,
307            topic_cache,
308        } = self;
309
310        topic_cache
311            .entry(topic)
312            .or_insert_with(|| Self::find_matches(subscriptions, topic))
313            .as_slice()
314    }
315
316    /// Fills a buffer with handlers matching a topic.
317    pub(crate) fn fill_matching_handlers(
318        &mut self,
319        topic: MStr<Topic>,
320        buf: &mut SmallVec<[TypedHandler<T>; 64]>,
321    ) {
322        let Self {
323            subscriptions,
324            topic_cache,
325        } = self;
326
327        let indices = topic_cache
328            .entry(topic)
329            .or_insert_with(|| Self::find_matches(subscriptions, topic));
330        let subscriptions = subscriptions.as_slice();
331
332        for &idx in indices.iter() {
333            buf.push(subscriptions[idx].handler.clone());
334        }
335    }
336
337    /// Finds subscription indices matching a topic (without caching).
338    fn find_matches(
339        subscriptions: &IndexSet<TypedSubscription<T>>,
340        topic: MStr<Topic>,
341    ) -> SmallVec<[usize; 64]> {
342        let mut indices: SmallVec<[usize; 64]> = subscriptions
343            .iter()
344            .enumerate()
345            .filter_map(|(idx, sub)| is_matching_backtracking(topic, sub.pattern).then_some(idx))
346            .collect();
347
348        // Sort only matching subscriptions; registration and removal leave storage unordered
349        indices.sort_by(|&a, &b| subscriptions[a].delivery_order(&subscriptions[b]));
350        indices
351    }
352
353    fn ordered_subscriptions(&self) -> Vec<&TypedSubscription<T>> {
354        let mut subscriptions: Vec<_> = self.subscriptions.iter().collect();
355        subscriptions.sort_by(|a, b| a.delivery_order(b));
356        subscriptions
357    }
358
359    /// Clears all subscriptions and cache.
360    pub fn clear(&mut self) {
361        self.subscriptions.clear();
362        self.topic_cache.clear();
363    }
364}
365
366#[cfg(test)]
367mod tests {
368    use std::{
369        cell::RefCell,
370        collections::hash_map::DefaultHasher,
371        hash::{Hash, Hasher},
372        rc::Rc,
373    };
374
375    use rstest::rstest;
376
377    use super::*;
378
379    #[rstest]
380    fn test_typed_subscription_ordering_laws() {
381        let handler = TypedHandler::from_with_id("handler-b", |_: &i32| {});
382        let base = TypedSubscription::new("pattern-b".into(), handler.clone(), Some(1));
383        let different_priority = TypedSubscription::new("pattern-b".into(), handler, Some(2));
384        let different_pattern = TypedSubscription::new(
385            "pattern-a".into(),
386            TypedHandler::from_with_id("handler-b", |_: &i32| {}),
387            Some(1),
388        );
389        let different_handler = TypedSubscription::new(
390            "pattern-b".into(),
391            TypedHandler::from_with_id("handler-a", |_: &i32| {}),
392            Some(1),
393        );
394
395        assert_eq!(base, different_priority);
396        assert_eq!(base.cmp(&different_priority), Ordering::Equal);
397
398        let mut base_hasher = DefaultHasher::new();
399        base.hash(&mut base_hasher);
400        let mut different_priority_hasher = DefaultHasher::new();
401        different_priority.hash(&mut different_priority_hasher);
402        assert_eq!(base_hasher.finish(), different_priority_hasher.finish());
403
404        let variants = [
405            base,
406            different_priority,
407            different_pattern,
408            different_handler,
409        ];
410
411        for a in &variants {
412            for b in &variants {
413                assert_eq!(a == b, a.cmp(b).is_eq());
414                assert_eq!(a.partial_cmp(b), Some(a.cmp(b)));
415                assert_eq!(a.cmp(b), b.cmp(a).reverse());
416            }
417        }
418    }
419
420    #[rstest]
421    fn test_topic_router_subscribe_and_publish() {
422        let mut router = TopicRouter::<String>::new();
423        let received = Rc::new(RefCell::new(Vec::new()));
424        let received_clone = received.clone();
425
426        let handler = TypedHandler::from(move |msg: &String| {
427            received_clone.borrow_mut().push(msg.clone());
428        });
429
430        router.subscribe("data.quotes.*".into(), handler, 0);
431
432        let topic: MStr<Topic> = "data.quotes.AAPL".into();
433        router.publish(topic, &"quote1".to_string());
434        router.publish(topic, &"quote2".to_string());
435
436        assert_eq!(*received.borrow(), vec!["quote1", "quote2"]);
437    }
438
439    #[rstest]
440    fn test_topic_router_publish_orders_by_full_delivery_key() {
441        let mut router = TopicRouter::<i32>::new();
442        let order = Rc::new(RefCell::new(Vec::new()));
443
444        let low_order = order.clone();
445        let low = TypedHandler::from_with_id("handler-z", move |_: &i32| {
446            low_order.borrow_mut().push("low-z");
447        });
448        let exact_b_order = order.clone();
449        let exact_b = TypedHandler::from_with_id("handler-b", move |_: &i32| {
450            exact_b_order.borrow_mut().push("exact-b");
451        });
452        let exact_a_order = order.clone();
453        let exact_a = TypedHandler::from_with_id("handler-a", move |_: &i32| {
454            exact_a_order.borrow_mut().push("exact-a");
455        });
456        let wildcard_b_order = order.clone();
457        let wildcard_b = TypedHandler::from_with_id("handler-b", move |_: &i32| {
458            wildcard_b_order.borrow_mut().push("wildcard-b");
459        });
460
461        router.subscribe("delivery.*".into(), low, 1);
462        router.subscribe("delivery.topic".into(), exact_b, 10);
463        router.subscribe("delivery.topic".into(), exact_a, 10);
464        router.subscribe("delivery.*".into(), wildcard_b, 10);
465        router.subscribe(
466            "delivery.topic".into(),
467            TypedHandler::from_with_id("handler-b", |_: &i32| panic!("duplicate handler replaced")),
468            100,
469        );
470
471        let topic: MStr<Topic> = "delivery.topic".into();
472        router.publish(topic, &42);
473
474        assert_eq!(
475            *order.borrow(),
476            vec!["wildcard-b", "exact-a", "exact-b", "low-z"]
477        );
478
479        router.remove_handler("delivery.*".into(), "handler-z".into());
480        order.borrow_mut().clear();
481        router.publish(topic, &42);
482        assert_eq!(*order.borrow(), vec!["wildcard-b", "exact-a", "exact-b"]);
483    }
484
485    #[rstest]
486    fn test_topic_router_unsubscribe() {
487        let mut router = TopicRouter::<String>::new();
488        let received = Rc::new(RefCell::new(Vec::new()));
489        let received_clone = received.clone();
490
491        let handler = TypedHandler::from_with_id("test-handler", move |msg: &String| {
492            received_clone.borrow_mut().push(msg.clone());
493        });
494
495        router.subscribe("data.*".into(), handler.clone(), 0);
496        assert!(router.is_subscribed("data.*".into(), &handler));
497
498        router.unsubscribe("data.*".into(), &handler);
499        assert!(!router.is_subscribed("data.*".into(), &handler));
500
501        let topic: MStr<Topic> = "data.test".into();
502        router.publish(topic, &"test".to_string());
503
504        // Should not receive anything after unsubscribe
505        assert!(received.borrow().is_empty());
506    }
507
508    #[rstest]
509    fn test_topic_router_duplicate_subscription() {
510        let mut router = TopicRouter::<i32>::new();
511
512        let handler1 = TypedHandler::from_with_id("dup-handler", |_: &i32| {});
513        let handler2 = TypedHandler::from_with_id("dup-handler", |_: &i32| {});
514
515        router.subscribe("test.*".into(), handler1, 0);
516        router.subscribe("test.*".into(), handler2, 0);
517
518        // Should only have one subscription
519        assert_eq!(router.subscription_count(), 1);
520    }
521
522    #[rstest]
523    fn test_topic_router_wildcard_patterns() {
524        let mut router = TopicRouter::<String>::new();
525        let received = Rc::new(RefCell::new(Vec::new()));
526        let received_clone = received.clone();
527
528        let handler = TypedHandler::from(move |msg: &String| {
529            received_clone.borrow_mut().push(msg.clone());
530        });
531
532        router.subscribe("data.*.AAPL".into(), handler, 0);
533
534        // Should match
535        let topic1: MStr<Topic> = "data.quotes.AAPL".into();
536        router.publish(topic1, &"match1".to_string());
537
538        let topic2: MStr<Topic> = "data.trades.AAPL".into();
539        router.publish(topic2, &"match2".to_string());
540
541        // Should not match
542        let topic3: MStr<Topic> = "data.quotes.MSFT".into();
543        router.publish(topic3, &"no-match".to_string());
544
545        assert_eq!(*received.borrow(), vec!["match1", "match2"]);
546    }
547
548    #[rstest]
549    fn test_topic_router_cache_populated_on_publish() {
550        let mut router = TopicRouter::<i32>::new();
551        let handler = TypedHandler::from_with_id("cache-test", |_: &i32| {});
552
553        router.subscribe("data.*".into(), handler, 0);
554
555        // First publish populates cache
556        let topic: MStr<Topic> = "data.quotes".into();
557        router.publish(topic, &1);
558
559        // Verify cache is used (subscriber_count uses cache if available)
560        assert_eq!(router.subscriber_count(topic), 1);
561    }
562
563    #[rstest]
564    fn test_topic_router_cache_invalidated_on_subscribe() {
565        let mut router = TopicRouter::<i32>::new();
566        let received = Rc::new(RefCell::new(0));
567
568        let r1 = received.clone();
569        let handler1 = TypedHandler::from_with_id("h1", move |_: &i32| {
570            *r1.borrow_mut() += 1;
571        });
572
573        router.subscribe("data.*".into(), handler1, 0);
574
575        // Publish to populate cache
576        let topic: MStr<Topic> = "data.test".into();
577        router.publish(topic, &1);
578        assert_eq!(*received.borrow(), 1);
579
580        // Subscribe new handler (should invalidate cache)
581        let r2 = received.clone();
582        let handler2 = TypedHandler::from_with_id("h2", move |_: &i32| {
583            *r2.borrow_mut() += 10;
584        });
585        router.subscribe("data.*".into(), handler2, 0);
586
587        // Publish again - both handlers should receive
588        router.publish(topic, &2);
589        assert_eq!(*received.borrow(), 12); // 1 + 1 + 10
590    }
591
592    #[rstest]
593    fn test_topic_router_late_distinct_wildcard_receives_cached_topic() {
594        let mut router = TopicRouter::<String>::new();
595        let topic: MStr<Topic> = "data.instrument.POLYMARKET.TEST-SYMBOL".into();
596
597        let early = Rc::new(RefCell::new(Vec::new()));
598        let early_clone = early.clone();
599        let early_handler = TypedHandler::from_with_id("early", move |msg: &String| {
600            early_clone.borrow_mut().push(msg.clone());
601        });
602        router.subscribe("data.*.POLYMARKET.*".into(), early_handler, 0);
603
604        router.publish(topic, &"ONE".to_string());
605
606        let late = Rc::new(RefCell::new(Vec::new()));
607        let late_clone = late.clone();
608        let late_handler = TypedHandler::from_with_id("late", move |msg: &String| {
609            late_clone.borrow_mut().push(msg.clone());
610        });
611        router.subscribe("data.instrument.POLYMARKET.*".into(), late_handler, 0);
612
613        router.publish(topic, &"TWO".to_string());
614
615        assert_eq!(*early.borrow(), vec!["ONE", "TWO"]);
616        assert_eq!(*late.borrow(), vec!["TWO"]);
617    }
618
619    #[rstest]
620    fn test_topic_router_cache_invalidated_on_unsubscribe() {
621        let mut router = TopicRouter::<i32>::new();
622        let received = Rc::new(RefCell::new(0));
623
624        let r1 = received.clone();
625        let handler1 = TypedHandler::from_with_id("h1", move |_: &i32| {
626            *r1.borrow_mut() += 1;
627        });
628
629        let r2 = received.clone();
630        let handler2 = TypedHandler::from_with_id("h2", move |_: &i32| {
631            *r2.borrow_mut() += 10;
632        });
633
634        router.subscribe("data.*".into(), handler1.clone(), 0);
635        router.subscribe("data.*".into(), handler2, 0);
636
637        // Publish to populate cache
638        let topic: MStr<Topic> = "data.test".into();
639        router.publish(topic, &1);
640        assert_eq!(*received.borrow(), 11); // 1 + 10
641
642        // Unsubscribe handler1 (should invalidate cache)
643        router.unsubscribe("data.*".into(), &handler1);
644
645        // Publish again - only handler2 should receive
646        router.publish(topic, &2);
647        assert_eq!(*received.borrow(), 21); // 11 + 10
648    }
649
650    #[rstest]
651    fn test_topic_router_has_subscribers() {
652        let mut router = TopicRouter::<i32>::new();
653
654        let topic: MStr<Topic> = "data.quotes.AAPL".into();
655        assert!(!router.has_subscribers(topic));
656
657        let handler = TypedHandler::from_with_id("test", |_: &i32| {});
658        router.subscribe("data.quotes.*".into(), handler, 0);
659
660        assert!(router.has_subscribers(topic));
661    }
662
663    #[rstest]
664    fn test_topic_router_subscriber_count() {
665        let mut router = TopicRouter::<i32>::new();
666
667        let topic: MStr<Topic> = "data.quotes.AAPL".into();
668        assert_eq!(router.subscriber_count(topic), 0);
669
670        let handler1 = TypedHandler::from_with_id("h1", |_: &i32| {});
671        let handler2 = TypedHandler::from_with_id("h2", |_: &i32| {});
672        let handler3 = TypedHandler::from_with_id("h3", |_: &i32| {});
673
674        router.subscribe("data.quotes.*".into(), handler1, 0);
675        router.subscribe("data.*.AAPL".into(), handler2, 0);
676        router.subscribe("events.*".into(), handler3, 0); // Won't match
677
678        assert_eq!(router.subscriber_count(topic), 2);
679    }
680
681    #[rstest]
682    fn test_topic_router_patterns_and_handler_ids() {
683        let mut router = TopicRouter::<i32>::new();
684
685        let handler1 = TypedHandler::from_with_id("handler-a", |_: &i32| {});
686        let handler2 = TypedHandler::from_with_id("handler-b", |_: &i32| {});
687
688        router.subscribe("pattern.one".into(), handler1, 0);
689        router.subscribe("pattern.two".into(), handler2, 1);
690
691        assert_eq!(router.patterns(), vec!["pattern.two", "pattern.one"]);
692        assert_eq!(router.handler_ids(), vec!["handler-b", "handler-a"]);
693    }
694
695    #[rstest]
696    fn test_topic_router_clear() {
697        let mut router = TopicRouter::<i32>::new();
698        let handler = TypedHandler::from_with_id("clear-test", |_: &i32| {});
699
700        router.subscribe("data.*".into(), handler, 0);
701
702        // Populate cache
703        let topic: MStr<Topic> = "data.test".into();
704        router.publish(topic, &1);
705
706        assert_eq!(router.subscription_count(), 1);
707        assert!(!router.is_empty());
708
709        router.clear();
710
711        assert_eq!(router.subscription_count(), 0);
712        assert!(router.is_empty());
713        assert!(!router.has_subscribers(topic));
714    }
715
716    #[rstest]
717    fn test_topic_router_multiple_patterns_same_topic() {
718        let mut router = TopicRouter::<i32>::new();
719        let received = Rc::new(RefCell::new(Vec::new()));
720
721        let r1 = received.clone();
722        let handler1 = TypedHandler::from_with_id("specific", move |v: &i32| {
723            r1.borrow_mut().push(format!("specific:{v}"));
724        });
725
726        let r2 = received.clone();
727        let handler2 = TypedHandler::from_with_id("wildcard", move |v: &i32| {
728            r2.borrow_mut().push(format!("wildcard:{v}"));
729        });
730
731        let r3 = received.clone();
732        let handler3 = TypedHandler::from_with_id("all", move |v: &i32| {
733            r3.borrow_mut().push(format!("all:{v}"));
734        });
735
736        // All three patterns match "data.quotes.AAPL"
737        router.subscribe("data.quotes.AAPL".into(), handler1, 0);
738        router.subscribe("data.quotes.*".into(), handler2, 0);
739        router.subscribe("data.*.*".into(), handler3, 0);
740
741        let topic: MStr<Topic> = "data.quotes.AAPL".into();
742        router.publish(topic, &42);
743
744        let msgs = received.borrow();
745        assert_eq!(msgs.len(), 3);
746        assert!(msgs.contains(&"specific:42".to_string()));
747        assert!(msgs.contains(&"wildcard:42".to_string()));
748        assert!(msgs.contains(&"all:42".to_string()));
749    }
750
751    #[rstest]
752    fn test_remove_handler_invalidates_cross_pattern_cache() {
753        let mut router = TopicRouter::<i32>::new();
754        let count_a = Rc::new(RefCell::new(0));
755        let count_b = Rc::new(RefCell::new(0));
756
757        let ca = count_a.clone();
758        let handler_a = TypedHandler::from_with_id("ha", move |_: &i32| {
759            *ca.borrow_mut() += 1;
760        });
761        let handler_a_id = Ustr::from("ha");
762
763        let cb = count_b.clone();
764        let handler_b = TypedHandler::from_with_id("hb", move |_: &i32| {
765            *cb.borrow_mut() += 1;
766        });
767
768        router.subscribe("events.order.S-001".into(), handler_a, 0);
769        router.subscribe("events.order.S-002".into(), handler_b, 0);
770
771        let topic_a: MStr<Topic> = "events.order.S-001".into();
772        let topic_b: MStr<Topic> = "events.order.S-002".into();
773        router.publish(topic_a, &1);
774        router.publish(topic_b, &1);
775        assert_eq!(*count_a.borrow(), 1);
776        assert_eq!(*count_b.borrow(), 1);
777
778        // Remove handler_a - must invalidate ALL cached indices
779        router.remove_handler("events.order.S-001".into(), handler_a_id);
780
781        // handler_b must still dispatch correctly despite index shift
782        router.publish(topic_b, &2);
783        assert_eq!(*count_b.borrow(), 2);
784
785        router.publish(topic_a, &3);
786        assert_eq!(*count_a.borrow(), 1);
787    }
788
789    #[rstest]
790    fn test_remove_handler_only_removes_targeted_handler() {
791        let mut router = TopicRouter::<i32>::new();
792        let count_own = Rc::new(RefCell::new(0));
793        let count_other = Rc::new(RefCell::new(0));
794
795        let co = count_own.clone();
796        let handler_own = TypedHandler::from_with_id("strategy", move |_: &i32| {
797            *co.borrow_mut() += 1;
798        });
799        let own_id = Ustr::from("strategy");
800
801        let cother = count_other.clone();
802        let handler_other = TypedHandler::from_with_id("exec-algo", move |_: &i32| {
803            *cother.borrow_mut() += 1;
804        });
805
806        // Both handlers on the same pattern (same strategy topic)
807        let pattern: MStr<Pattern> = "events.order.S-001".into();
808        router.subscribe(pattern, handler_own, 0);
809        router.subscribe(pattern, handler_other, 0);
810
811        let topic: MStr<Topic> = "events.order.S-001".into();
812        router.publish(topic, &1);
813        assert_eq!(*count_own.borrow(), 1);
814        assert_eq!(*count_other.borrow(), 1);
815
816        router.remove_handler(pattern, own_id);
817
818        router.publish(topic, &2);
819        assert_eq!(*count_own.borrow(), 1);
820        assert_eq!(*count_other.borrow(), 2);
821    }
822
823    #[rstest]
824    fn test_unsubscribe_one_pattern_does_not_break_other_patterns() {
825        let mut router = TopicRouter::<i32>::new();
826        let received = Rc::new(RefCell::new(0));
827
828        let alpha = TypedHandler::from_with_id("alpha", |_: &i32| {});
829
830        let received_beta = received.clone();
831        let beta = TypedHandler::from_with_id("beta", move |_: &i32| {
832            *received_beta.borrow_mut() += 1;
833        });
834
835        router.subscribe("alpha.*".into(), alpha.clone(), 0);
836        router.subscribe("beta.*".into(), beta, 0);
837
838        let beta_topic: MStr<Topic> = "beta.topic".into();
839        router.publish(beta_topic, &1);
840        assert_eq!(*received.borrow(), 1);
841
842        router.unsubscribe("alpha.*".into(), &alpha);
843
844        router.publish(beta_topic, &2);
845        assert_eq!(*received.borrow(), 2);
846    }
847}