use std::future::Future;
use std::sync::Arc;
use std::time::Duration;
use tokio::sync::watch;
use tokio::time;
use lsp_types_max::Uri as Url;
use super::DocumentStore;
#[derive(Clone, Debug)]
pub struct DebounceHandle {
tx: Arc<watch::Sender<()>>,
}
impl DebounceHandle {
pub fn trigger(&self) {
let _ = self.tx.send(());
}
}
pub fn debounce<F, Fut>(delay: Duration, f: F) -> DebounceHandle
where
F: Fn() -> Fut + Send + 'static,
Fut: Future<Output = ()> + Send + 'static,
{
let (tx, mut rx) = watch::channel(());
let handle = DebounceHandle { tx: Arc::new(tx) };
tokio::spawn(async move {
loop {
if rx.changed().await.is_err() {
break;
}
while let Ok(Ok(())) = time::timeout(delay, rx.changed()).await {}
f().await;
}
});
handle
}
#[tracing::instrument(
name = "debounce_adaptive",
skip(store, f),
fields(
uri = ?uri,
base_delay_ms = base_delay.as_millis(),
activations = tracing::field::Empty,
multiplier = tracing::field::Empty,
)
)]
pub fn debounce_adaptive<F, Fut>(
store: DocumentStore,
uri: Url,
base_delay: Duration,
f: F,
) -> DebounceHandle
where
F: Fn() -> Fut + Send + 'static,
Fut: Future<Output = ()> + Send + 'static,
{
let (tx, mut rx) = watch::channel(());
let handle = DebounceHandle { tx: Arc::new(tx) };
let span = tracing::Span::current();
tokio::spawn(async move {
loop {
if rx.changed().await.is_err() {
break;
}
let acts = store.activation_count(&uri);
let multiplier = (acts as f64 / 10.0).sqrt().clamp(1.0, 8.0);
span.record("activations", acts);
span.record("multiplier", multiplier);
let delay = base_delay.mul_f64(multiplier);
while let Ok(Ok(())) = time::timeout(delay, rx.changed()).await {}
f().await;
}
});
handle
}
#[cfg(test)]
mod tests {
use super::*;
use std::sync::{
atomic::{AtomicU64, Ordering},
Arc,
};
#[tokio::test]
#[ignore]
async fn debounce_fires_callback_after_quiet_window() {
let counter = Arc::new(AtomicU64::new(0));
let c = Arc::clone(&counter);
let handle = debounce(Duration::from_millis(20), move || {
let c = Arc::clone(&c);
async move {
c.fetch_add(1, Ordering::SeqCst);
}
});
handle.trigger();
tokio::time::sleep(Duration::from_millis(80)).await;
assert_eq!(counter.load(Ordering::SeqCst), 1);
}
#[tokio::test]
#[ignore]
async fn debounce_coalesces_rapid_triggers_into_one_call() {
let counter = Arc::new(AtomicU64::new(0));
let c = Arc::clone(&counter);
let handle = debounce(Duration::from_millis(30), move || {
let c = Arc::clone(&c);
async move {
c.fetch_add(1, Ordering::SeqCst);
}
});
for _ in 0..5 {
handle.trigger();
}
tokio::time::sleep(Duration::from_millis(100)).await;
assert_eq!(
counter.load(Ordering::SeqCst),
1,
"five rapid triggers must coalesce into one callback"
);
}
#[tokio::test]
#[ignore]
async fn debounce_handle_clone_shares_trigger_channel() {
let counter = Arc::new(AtomicU64::new(0));
let c = Arc::clone(&counter);
let handle = debounce(Duration::from_millis(20), move || {
let c = Arc::clone(&c);
async move {
c.fetch_add(1, Ordering::SeqCst);
}
});
let handle2 = handle.clone();
handle2.trigger();
tokio::time::sleep(Duration::from_millis(80)).await;
assert_eq!(counter.load(Ordering::SeqCst), 1);
}
}