Skip to main content

rx_rs/core/
tracker.rs

1use std::cell::RefCell;
2use std::rc::Rc;
3
4type Cleanups = Rc<RefCell<Vec<Box<dyn FnOnce()>>>>;
5type OwnerCount = Rc<()>;
6
7/// Tracks reactive subscriptions and automatically cleans them up when dropped.
8///
9/// Subscriptions registered with a Tracker will be automatically disposed when
10/// the Tracker is dropped, preventing memory leaks and ensuring proper cleanup.
11///
12/// Note: Tracker cannot be created directly. Use `DisposableTracker::new()` and
13/// get the Tracker via `.tracker()`.
14pub struct Tracker {
15    cleanups: Cleanups,
16}
17
18impl Tracker {
19    /// Creates a new empty Tracker.
20    ///
21    /// This is private - only DisposableTracker can create Trackers.
22    pub(super) fn new() -> Self {
23        Self {
24            cleanups: Rc::new(RefCell::new(Vec::new())),
25        }
26    }
27
28    /// Adds a cleanup function to be called when the tracker is dropped.
29    ///
30    /// This is an internal method used by reactive primitives to register
31    /// their cleanup logic.
32    pub(crate) fn add<F: FnOnce() + 'static>(&self, cleanup: F) {
33        self.cleanups.borrow_mut().push(Box::new(cleanup));
34    }
35
36    /// Returns the number of active subscriptions tracked.
37    pub fn subscription_count(&self) -> usize {
38        self.cleanups.borrow().len()
39    }
40
41    /// Tracks another DisposableTracker's lifetime.
42    ///
43    /// When the Tracker's parent DisposableTracker is disposed (either manually via `dispose()` or
44    /// automatically when dropped), the tracked DisposableTracker will also be disposed.
45    ///
46    /// This is useful for creating hierarchical cleanup relationships.
47    ///
48    /// # Arguments
49    /// * `child` - The DisposableTracker to track
50    ///
51    /// # Example
52    /// ```
53    /// use rx_rs::core::DisposableTracker;
54    ///
55    /// let parent = DisposableTracker::new();
56    /// let mut child = DisposableTracker::new();
57    ///
58    /// parent.tracker().track(child);
59    ///
60    /// // When parent is disposed, child is also disposed
61    /// ```
62    pub fn track(&self, mut child: DisposableTracker) {
63        self.add(move || {
64            child.dispose();
65        });
66    }
67}
68
69impl Clone for Tracker {
70    fn clone(&self) -> Self {
71        Self {
72            cleanups: self.cleanups.clone(),
73        }
74    }
75}
76
77/// A tracker that can be manually disposed before it's dropped.
78///
79/// Unlike Tracker, DisposableTracker provides a `dispose()` method to
80/// explicitly clean up all subscriptions. This is useful for long-lived
81/// objects that need to clear subscriptions mid-lifecycle.
82///
83/// DisposableTracker is Clone - all clones share the same internal state via Rc<RefCell>.
84/// Subscriptions are cleaned up when ALL DisposableTracker clones are dropped.
85/// Tracker clones do NOT count towards this - only DisposableTracker ownership matters.
86#[derive(Clone)]
87pub struct DisposableTracker {
88    tracker: Tracker,
89    // Separate Rc to track only DisposableTracker clones (not Tracker clones)
90    owner_count: OwnerCount,
91}
92
93impl DisposableTracker {
94    /// Creates a new empty DisposableTracker.
95    pub fn new() -> Self {
96        Self {
97            tracker: Tracker::new(),
98            owner_count: Rc::new(()),
99        }
100    }
101
102    /// Returns the underlying Tracker for use with subscribe methods.
103    pub fn tracker(&self) -> &Tracker {
104        &self.tracker
105    }
106
107    /// Manually disposes all tracked subscriptions.
108    ///
109    /// After calling this, the tracker is still valid and can track new
110    /// subscriptions, but all previous subscriptions are cleaned up.
111    pub fn dispose(&mut self) {
112        if let Ok(mut cleanups) = self.tracker.cleanups.try_borrow_mut() {
113            if cleanups.is_empty() {
114                return; // Already disposed or no subscriptions
115            }
116
117            #[cfg(feature = "debug")]
118            {
119                let count = cleanups.len();
120                tracing::debug!(
121                    subscription_count = count,
122                    "manually disposing DisposableTracker"
123                );
124            }
125
126            for cleanup in cleanups.drain(..) {
127                cleanup();
128            }
129
130            #[cfg(feature = "debug")]
131            tracing::debug!("manual dispose complete");
132        }
133    }
134
135    /// Returns the number of active subscriptions tracked.
136    pub fn subscription_count(&self) -> usize {
137        self.tracker.subscription_count()
138    }
139}
140
141impl Default for DisposableTracker {
142    fn default() -> Self {
143        Self::new()
144    }
145}
146
147impl Drop for DisposableTracker {
148    fn drop(&mut self) {
149        // Only clean up when the last DisposableTracker clone is dropped
150        // Use owner_count (not tracker.cleanups) to ignore Tracker clones
151        if Rc::strong_count(&self.owner_count) == 1 {
152            if let Ok(mut cleanups) = self.tracker.cleanups.try_borrow_mut() {
153                if cleanups.is_empty() {
154                    return; // Already disposed
155                }
156
157                #[cfg(feature = "debug")]
158                {
159                    let count = cleanups.len();
160                    tracing::debug!(
161                        subscription_count = count,
162                        "dropping last DisposableTracker clone"
163                    );
164                }
165
166                for cleanup in cleanups.drain(..) {
167                    cleanup();
168                }
169
170                #[cfg(feature = "debug")]
171                tracing::debug!("DisposableTracker drop complete");
172            }
173        }
174    }
175}