Skip to main content

shared_framework/utils/
time_debouncers.rs

1//! Time-based debouncers.
2//!
3//! [`TimeDebouncers`] builds trailing debouncers that run after a quiet period,
4//! leading debouncers that run at most once per window, and trailing-value
5//! debouncers that deliver the latest value after a quiet period.
6//!
7//! ```ignore
8//! let d = TimeDebouncers::trailing(Duration::from_millis(200), || println!("fired"));
9//! d.trigger();
10//! ```
11
12use std::sync::{Arc, Mutex};
13use std::time::Duration;
14use tokio::task::JoinHandle;
15
16/// Debouncer without a payload: trailing or leading.
17pub trait Debouncer: Send + Sync {
18    /// Requests execution according to the debouncer policy.
19    fn trigger(&self);
20    /// Cancels a pending trailing execution. Returns true when one was pending.
21    /// Leading debouncers have nothing pending and always return false.
22    fn cancel_pending(&self) -> bool;
23}
24
25/// Debouncer that delivers a payload of type `T`.
26pub trait ValueDebouncer<T>: Send + Sync {
27    /// Offers `value`, replacing any pending value, and (re)starts the delay.
28    fn trigger(&self, value: T);
29    /// Discards the pending value and timer. Returns true when one was pending.
30    fn cancel_pending(&self) -> bool;
31}
32
33/// Factory for trailing, leading, and trailing-value debouncers.
34pub struct TimeDebouncers;
35
36impl TimeDebouncers {
37    /// Builds a debouncer that runs `action` once after `delay` elapses without another trigger.
38    /// Panics via the debouncer constructor when `delay` is zero.
39    pub fn trailing<F>(delay: Duration, action: F) -> Arc<TrailingDebouncer>
40    where
41        F: Fn() + Send + Sync + 'static,
42    {
43        Arc::new(TrailingDebouncer::new(delay, action))
44    }
45
46    /// Builds a debouncer that runs `action` immediately when outside `window`,
47    /// otherwise ignores the trigger. Panics via the constructor when `window` is zero.
48    pub fn leading<F>(window: Duration, action: F) -> Arc<LeadingDebouncer>
49    where
50        F: Fn() + Send + Sync + 'static,
51    {
52        Arc::new(LeadingDebouncer::new(window, action))
53    }
54
55    /// Builds a debouncer that runs `action` with the latest value after `delay`
56    /// elapses without another trigger. Panics via the constructor when `delay` is zero.
57    pub fn trailing_value<T, F>(delay: Duration, action: F) -> Arc<TrailingValueDebouncer<T>>
58    where
59        T: Clone + Send + Sync + 'static,
60        F: Fn(T) + Send + Sync + 'static,
61    {
62        Arc::new(TrailingValueDebouncer::new(delay, action))
63    }
64}
65
66// ── Trailing ─────────────────────────────────────────────────────────────────
67
68/// Runs the action once after a quiet `delay`; each trigger restarts the timer.
69pub struct TrailingDebouncer {
70    delay: Duration,
71    action: Arc<dyn Fn() + Send + Sync>,
72    handle: Mutex<Option<JoinHandle<()>>>,
73}
74
75impl TrailingDebouncer {
76    fn new<F>(delay: Duration, action: F) -> Self
77    where
78        F: Fn() + Send + Sync + 'static,
79    {
80        assert!(!delay.is_zero(), "delay must be > 0");
81        Self {
82            delay,
83            action: Arc::new(action),
84            handle: Mutex::new(None),
85        }
86    }
87}
88
89impl Debouncer for TrailingDebouncer {
90    fn trigger(&self) {
91        let mut guard = self.handle.lock().unwrap();
92        if let Some(h) = guard.take() {
93            h.abort();
94        }
95        let action = Arc::clone(&self.action);
96        let delay = self.delay;
97        // Use weak handle to clear on completion
98        let handle = tokio::spawn(async move {
99            tokio::time::sleep(delay).await;
100            action();
101        });
102        *guard = Some(handle);
103    }
104
105    fn cancel_pending(&self) -> bool {
106        let mut guard = self.handle.lock().unwrap();
107        if let Some(h) = guard.take() {
108            h.abort();
109            true
110        } else {
111            false
112        }
113    }
114}
115
116// ── Leading ──────────────────────────────────────────────────────────────────
117
118/// Runs the action immediately at most once per `window`; triggers inside the window are ignored.
119pub struct LeadingDebouncer {
120    window: Duration,
121    action: Arc<dyn Fn() + Send + Sync>,
122    next_allowed: Mutex<std::time::Instant>,
123}
124
125impl LeadingDebouncer {
126    fn new<F>(window: Duration, action: F) -> Self
127    where
128        F: Fn() + Send + Sync + 'static,
129    {
130        assert!(!window.is_zero(), "window must be > 0");
131        Self {
132            window,
133            action: Arc::new(action),
134            next_allowed: Mutex::new(std::time::Instant::now() - window),
135        }
136    }
137}
138
139impl Debouncer for LeadingDebouncer {
140    fn trigger(&self) {
141        let now = std::time::Instant::now();
142        let mut guard = self.next_allowed.lock().unwrap();
143        if now < *guard {
144            return;
145        }
146        *guard = now + self.window;
147        drop(guard);
148        (self.action)();
149    }
150
151    fn cancel_pending(&self) -> bool {
152        false
153    }
154}
155
156// ── Trailing value ───────────────────────────────────────────────────────────
157
158/// Runs the action with the latest offered value of type `T` after a quiet delay.
159pub struct TrailingValueDebouncer<T> {
160    delay: Duration,
161    action: Arc<dyn Fn(T) + Send + Sync>,
162    latest: Arc<Mutex<Option<T>>>,
163    handle: Mutex<Option<JoinHandle<()>>>,
164}
165
166impl<T> TrailingValueDebouncer<T>
167where
168    T: Clone + Send + Sync + 'static,
169{
170    fn new<F>(delay: Duration, action: F) -> Self
171    where
172        F: Fn(T) + Send + Sync + 'static,
173    {
174        assert!(!delay.is_zero(), "delay must be > 0");
175        Self {
176            delay,
177            action: Arc::new(action),
178            latest: Arc::new(Mutex::new(None)),
179            handle: Mutex::new(None),
180        }
181    }
182}
183
184impl<T> ValueDebouncer<T> for TrailingValueDebouncer<T>
185where
186    T: Clone + Send + Sync + 'static,
187{
188    fn trigger(&self, value: T) {
189        *self.latest.lock().unwrap() = Some(value);
190        let mut guard = self.handle.lock().unwrap();
191        if let Some(h) = guard.take() {
192            h.abort();
193        }
194        let latest = Arc::clone(&self.latest);
195        let action = Arc::clone(&self.action);
196        let delay = self.delay;
197        let handle = tokio::spawn(async move {
198            tokio::time::sleep(delay).await;
199            let val = latest.lock().unwrap().take();
200            if let Some(v) = val {
201                action(v);
202            }
203        });
204        *guard = Some(handle);
205    }
206
207    fn cancel_pending(&self) -> bool {
208        let mut guard = self.handle.lock().unwrap();
209        let had = guard.is_some();
210        if let Some(h) = guard.take() {
211            h.abort();
212        }
213        // Also clear latest so no emission after cancel
214        *self.latest.lock().unwrap() = None;
215        had
216    }
217}