rskit-stream 0.2.0-alpha.5

Foundational async stream toolkit: bounded fan-out broadcaster, sources, cancellable tasks, and futures::Stream extension operators
Documentation
//! Trailing-edge rate-limiting operators.
//!
//! These operators control *emission timing* rather than partitioning a stream into fixed windows:
//! their timers are **trailing-edge**,
//! driven by quiet gaps between arrivals (`debounce`, `debounce_batch`)
//! or by a minimum spacing between emissions (`throttle`).
//! Contrast the leading-edge windowing operators in [`crate::operators::windowing`],
//! whose timers run from the first buffered item.

use std::time::Duration;

use futures::{Stream, StreamExt as _};

use super::buffer::{take_window, window_capacity};

/// Emit only when no new item arrives within `delay`.
///
/// Useful for rate-limiting high-frequency event streams.
pub fn debounce<S, T>(stream: S, delay: Duration) -> impl Stream<Item = T> + Send + 'static
where
    S: Stream<Item = T> + Send + 'static,
    T: Send + 'static,
{
    async_stream::stream! {
        tokio::pin!(stream);
        let mut pending: Option<T> = None;

        loop {
            let has_pending = pending.is_some();
            tokio::select! {
                item = stream.next() => {
                    if let Some(v) = item {
                        pending = Some(v);
                    } else {
                        if let Some(v) = pending.take() { yield v; }
                        break;
                    }
                }
                () = async move {
                    if has_pending {
                        tokio::time::sleep(delay).await;
                    } else {
                        std::future::pending::<()>().await;
                    }
                } => {
                    if let Some(v) = pending.take() { yield v; }
                }
            }
        }
    }
}

/// Accumulate items while they keep arriving,
/// emitting the collected batch once `quiet` elapses with no new item (trailing-edge, timer reset per item),
/// or early once `max_items` have accumulated.
///
/// This is [`debounce`] that keeps *every* item seen during the burst instead of only the last:
/// each arrival resets the quiet-window timer,
/// and when the timer finally fires the whole accumulated window is emitted as one `Vec<T>` in arrival order.
/// It differs from [`crate::operators::windowing::batch`] / [`crate::operators::windowing::tumbling_window`],
/// whose timers run from the *first* item (leading-edge); here the timer is trailing-edge.
///
/// `max_items` (clamped to at least 1) is a safety cap that force-flushes the window:
/// because the timer resets on every item,
/// a sustained input rate faster than `quiet` would otherwise never reach a quiet window
/// and grow the buffer without bound. Reaching `max_items` emits the accumulated window immediately
/// and starts a fresh one — mirroring the size cap on the windowing operators.
pub fn debounce_batch<S, T>(
    stream: S,
    quiet: Duration,
    max_items: usize,
) -> impl Stream<Item = Vec<T>> + Send + 'static
where
    S: Stream<Item = T> + Send + 'static,
    T: Send + 'static,
{
    async_stream::stream! {
        tokio::pin!(stream);
        let (max_items, initial_capacity) = window_capacity(max_items);
        let mut buf: Vec<T> = Vec::with_capacity(initial_capacity);
        // `None` parks the timer arm on `pending()` while the buffer is empty,
        // so a quiet stream never emits;
        // the first item arms a trailing-edge deadline that every subsequent item pushes further out.
        let mut deadline: Option<tokio::time::Instant> = None;

        loop {
            tokio::select! {
                item = stream.next() => {
                    if let Some(v) = item {
                        buf.push(v);
                        deadline = Some(tokio::time::Instant::now() + quiet);
                        if buf.len() >= max_items {
                            deadline = None;
                            yield take_window(&mut buf, initial_capacity);
                        }
                    } else {
                        if !buf.is_empty() {
                            yield take_window(&mut buf, initial_capacity);
                        }
                        break;
                    }
                }
                () = async {
                    match deadline {
                        Some(d) => tokio::time::sleep_until(d).await,
                        None => std::future::pending::<()>().await,
                    }
                } => {
                    deadline = None;
                    if !buf.is_empty() {
                        yield take_window(&mut buf, initial_capacity);
                    }
                }
            }
        }
    }
}

/// Emit at most one item per `interval`, dropping faster arrivals.
pub fn throttle<S, T>(stream: S, interval: Duration) -> impl Stream<Item = T> + Send + 'static
where
    S: Stream<Item = T> + Send + 'static,
    T: Send + 'static,
{
    async_stream::stream! {
        tokio::pin!(stream);
        // `None` until the first item is emitted,
        // so the first arrival is always yielded without a potentially-underflowing `now - interval`.
        let mut last_emit: Option<tokio::time::Instant> = None;
        while let Some(item) = stream.next().await {
            let now = tokio::time::Instant::now();
            let due = last_emit.is_none_or(|prev| now.duration_since(prev) >= interval);
            if due {
                last_emit = Some(now);
                yield item;
            }
        }
    }
}