shared-framework 0.0.18

Reusable building blocks for HTTP services — Hyper routing, SeaORM data layer, validation, OpenAPI docs, jobs, queues, cache.
Documentation
//! Time-based debouncers.
//!
//! [`TimeDebouncers`] builds trailing debouncers that run after a quiet period,
//! leading debouncers that run at most once per window, and trailing-value
//! debouncers that deliver the latest value after a quiet period.
//!
//! ```ignore
//! let d = TimeDebouncers::trailing(Duration::from_millis(200), || println!("fired"));
//! d.trigger();
//! ```

use std::sync::{Arc, Mutex};
use std::time::Duration;
use tokio::task::JoinHandle;

/// Debouncer without a payload: trailing or leading.
pub trait Debouncer: Send + Sync {
    /// Requests execution according to the debouncer policy.
    fn trigger(&self);
    /// Cancels a pending trailing execution. Returns true when one was pending.
    /// Leading debouncers have nothing pending and always return false.
    fn cancel_pending(&self) -> bool;
}

/// Debouncer that delivers a payload of type `T`.
pub trait ValueDebouncer<T>: Send + Sync {
    /// Offers `value`, replacing any pending value, and (re)starts the delay.
    fn trigger(&self, value: T);
    /// Discards the pending value and timer. Returns true when one was pending.
    fn cancel_pending(&self) -> bool;
}

/// Factory for trailing, leading, and trailing-value debouncers.
pub struct TimeDebouncers;

impl TimeDebouncers {
    /// Builds a debouncer that runs `action` once after `delay` elapses without another trigger.
    /// Panics via the debouncer constructor when `delay` is zero.
    pub fn trailing<F>(delay: Duration, action: F) -> Arc<TrailingDebouncer>
    where
        F: Fn() + Send + Sync + 'static,
    {
        Arc::new(TrailingDebouncer::new(delay, action))
    }

    /// Builds a debouncer that runs `action` immediately when outside `window`,
    /// otherwise ignores the trigger. Panics via the constructor when `window` is zero.
    pub fn leading<F>(window: Duration, action: F) -> Arc<LeadingDebouncer>
    where
        F: Fn() + Send + Sync + 'static,
    {
        Arc::new(LeadingDebouncer::new(window, action))
    }

    /// Builds a debouncer that runs `action` with the latest value after `delay`
    /// elapses without another trigger. Panics via the constructor when `delay` is zero.
    pub fn trailing_value<T, F>(delay: Duration, action: F) -> Arc<TrailingValueDebouncer<T>>
    where
        T: Clone + Send + Sync + 'static,
        F: Fn(T) + Send + Sync + 'static,
    {
        Arc::new(TrailingValueDebouncer::new(delay, action))
    }
}

// ── Trailing ─────────────────────────────────────────────────────────────────

/// Runs the action once after a quiet `delay`; each trigger restarts the timer.
pub struct TrailingDebouncer {
    delay: Duration,
    action: Arc<dyn Fn() + Send + Sync>,
    handle: Mutex<Option<JoinHandle<()>>>,
}

impl TrailingDebouncer {
    fn new<F>(delay: Duration, action: F) -> Self
    where
        F: Fn() + Send + Sync + 'static,
    {
        assert!(!delay.is_zero(), "delay must be > 0");
        Self {
            delay,
            action: Arc::new(action),
            handle: Mutex::new(None),
        }
    }
}

impl Debouncer for TrailingDebouncer {
    fn trigger(&self) {
        let mut guard = self.handle.lock().unwrap();
        if let Some(h) = guard.take() {
            h.abort();
        }
        let action = Arc::clone(&self.action);
        let delay = self.delay;
        // Use weak handle to clear on completion
        let handle = tokio::spawn(async move {
            tokio::time::sleep(delay).await;
            action();
        });
        *guard = Some(handle);
    }

    fn cancel_pending(&self) -> bool {
        let mut guard = self.handle.lock().unwrap();
        if let Some(h) = guard.take() {
            h.abort();
            true
        } else {
            false
        }
    }
}

// ── Leading ──────────────────────────────────────────────────────────────────

/// Runs the action immediately at most once per `window`; triggers inside the window are ignored.
pub struct LeadingDebouncer {
    window: Duration,
    action: Arc<dyn Fn() + Send + Sync>,
    next_allowed: Mutex<std::time::Instant>,
}

impl LeadingDebouncer {
    fn new<F>(window: Duration, action: F) -> Self
    where
        F: Fn() + Send + Sync + 'static,
    {
        assert!(!window.is_zero(), "window must be > 0");
        Self {
            window,
            action: Arc::new(action),
            next_allowed: Mutex::new(std::time::Instant::now() - window),
        }
    }
}

impl Debouncer for LeadingDebouncer {
    fn trigger(&self) {
        let now = std::time::Instant::now();
        let mut guard = self.next_allowed.lock().unwrap();
        if now < *guard {
            return;
        }
        *guard = now + self.window;
        drop(guard);
        (self.action)();
    }

    fn cancel_pending(&self) -> bool {
        false
    }
}

// ── Trailing value ───────────────────────────────────────────────────────────

/// Runs the action with the latest offered value of type `T` after a quiet delay.
pub struct TrailingValueDebouncer<T> {
    delay: Duration,
    action: Arc<dyn Fn(T) + Send + Sync>,
    latest: Arc<Mutex<Option<T>>>,
    handle: Mutex<Option<JoinHandle<()>>>,
}

impl<T> TrailingValueDebouncer<T>
where
    T: Clone + Send + Sync + 'static,
{
    fn new<F>(delay: Duration, action: F) -> Self
    where
        F: Fn(T) + Send + Sync + 'static,
    {
        assert!(!delay.is_zero(), "delay must be > 0");
        Self {
            delay,
            action: Arc::new(action),
            latest: Arc::new(Mutex::new(None)),
            handle: Mutex::new(None),
        }
    }
}

impl<T> ValueDebouncer<T> for TrailingValueDebouncer<T>
where
    T: Clone + Send + Sync + 'static,
{
    fn trigger(&self, value: T) {
        *self.latest.lock().unwrap() = Some(value);
        let mut guard = self.handle.lock().unwrap();
        if let Some(h) = guard.take() {
            h.abort();
        }
        let latest = Arc::clone(&self.latest);
        let action = Arc::clone(&self.action);
        let delay = self.delay;
        let handle = tokio::spawn(async move {
            tokio::time::sleep(delay).await;
            let val = latest.lock().unwrap().take();
            if let Some(v) = val {
                action(v);
            }
        });
        *guard = Some(handle);
    }

    fn cancel_pending(&self) -> bool {
        let mut guard = self.handle.lock().unwrap();
        let had = guard.is_some();
        if let Some(h) = guard.take() {
            h.abort();
        }
        // Also clear latest so no emission after cancel
        *self.latest.lock().unwrap() = None;
        had
    }
}