lsp_max/primitives/
debounce.rs1use std::future::Future;
2use std::sync::Arc;
3use std::time::Duration;
4use tokio::sync::watch;
5use tokio::time;
6
7use lsp_types_max::Uri as Url;
8
9use super::DocumentStore;
10
11#[derive(Clone, Debug)]
17pub struct DebounceHandle {
18 tx: Arc<watch::Sender<()>>,
19}
20
21impl DebounceHandle {
22 pub fn trigger(&self) {
25 let _ = self.tx.send(());
26 }
27}
28
29pub fn debounce<F, Fut>(delay: Duration, f: F) -> DebounceHandle
48where
49 F: Fn() -> Fut + Send + 'static,
50 Fut: Future<Output = ()> + Send + 'static,
51{
52 let (tx, mut rx) = watch::channel(());
53 let handle = DebounceHandle { tx: Arc::new(tx) };
54
55 tokio::spawn(async move {
56 loop {
57 if rx.changed().await.is_err() {
59 break;
60 }
61 while let Ok(Ok(())) = time::timeout(delay, rx.changed()).await {}
63 f().await;
64 }
65 });
66
67 handle
68}
69
70#[tracing::instrument(
88 name = "debounce_adaptive",
89 skip(store, f),
90 fields(
91 uri = ?uri,
92 base_delay_ms = base_delay.as_millis(),
93 activations = tracing::field::Empty,
94 multiplier = tracing::field::Empty,
95 )
96)]
97pub fn debounce_adaptive<F, Fut>(
98 store: DocumentStore,
99 uri: Url,
100 base_delay: Duration,
101 f: F,
102) -> DebounceHandle
103where
104 F: Fn() -> Fut + Send + 'static,
105 Fut: Future<Output = ()> + Send + 'static,
106{
107 let (tx, mut rx) = watch::channel(());
108 let handle = DebounceHandle { tx: Arc::new(tx) };
109
110 let span = tracing::Span::current();
111 tokio::spawn(async move {
112 loop {
113 if rx.changed().await.is_err() {
114 break;
115 }
116 let acts = store.activation_count(&uri);
117 let multiplier = (acts as f64 / 10.0).sqrt().clamp(1.0, 8.0);
118 span.record("activations", acts);
119 span.record("multiplier", multiplier);
120 let delay = base_delay.mul_f64(multiplier);
121 while let Ok(Ok(())) = time::timeout(delay, rx.changed()).await {}
122 f().await;
123 }
124 });
125
126 handle
127}
128
129#[cfg(test)]
130mod tests {
131 use super::*;
132 use std::sync::{
133 atomic::{AtomicU64, Ordering},
134 Arc,
135 };
136
137 #[tokio::test]
138 #[ignore]
139 async fn debounce_fires_callback_after_quiet_window() {
140 let counter = Arc::new(AtomicU64::new(0));
141 let c = Arc::clone(&counter);
142 let handle = debounce(Duration::from_millis(20), move || {
143 let c = Arc::clone(&c);
144 async move {
145 c.fetch_add(1, Ordering::SeqCst);
146 }
147 });
148 handle.trigger();
149 tokio::time::sleep(Duration::from_millis(80)).await;
150 assert_eq!(counter.load(Ordering::SeqCst), 1);
151 }
152
153 #[tokio::test]
154 #[ignore]
155 async fn debounce_coalesces_rapid_triggers_into_one_call() {
156 let counter = Arc::new(AtomicU64::new(0));
157 let c = Arc::clone(&counter);
158 let handle = debounce(Duration::from_millis(30), move || {
159 let c = Arc::clone(&c);
160 async move {
161 c.fetch_add(1, Ordering::SeqCst);
162 }
163 });
164 for _ in 0..5 {
165 handle.trigger();
166 }
167 tokio::time::sleep(Duration::from_millis(100)).await;
168 assert_eq!(
169 counter.load(Ordering::SeqCst),
170 1,
171 "five rapid triggers must coalesce into one callback"
172 );
173 }
174
175 #[tokio::test]
176 #[ignore]
177 async fn debounce_handle_clone_shares_trigger_channel() {
178 let counter = Arc::new(AtomicU64::new(0));
179 let c = Arc::clone(&counter);
180 let handle = debounce(Duration::from_millis(20), move || {
181 let c = Arc::clone(&c);
182 async move {
183 c.fetch_add(1, Ordering::SeqCst);
184 }
185 });
186 let handle2 = handle.clone();
187 handle2.trigger();
188 tokio::time::sleep(Duration::from_millis(80)).await;
189 assert_eq!(counter.load(Ordering::SeqCst), 1);
190 }
191}