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}