use std::{pin, task, time::Duration}; use futures_lite::{ready, Stream}; use galaxyui::r#async::Timer; use pin::Pin; use pin_project::pin_project; use task::{Context, Poll}; /// Debounce takes in a stream and limits the rate of firing events from the stream /// by bundling all events occurred within the set interval into one. /// /// For example, if the interval is set to 50 and the following events were fired: /// E1 fired at T_0 /// E2 fired at T_30 /// E3 fired at T_60 /// E4 fired at T_200 /// /// Debounce will first receive E1 at T_0 and set the timer to expire at T_50. At T_30, /// E2 came in and the previous timer has not expired, debounce will change the last /// event to E2 and set timer at T_80 (T_30 + 50). At T_60, E3 came in and similarly /// the previous timer has not expired, debounce will change the last /// event to E3 and set timer at T_110 (T_60 + 50). At T_110, the timer expired and emits /// E3. At T_200, E4 came in and debounce set the timer at T_250. At T_250, E4 was emitted. /// /// +---------+ +-----------+ +-----------+ /// | stream | | debounce | | executor | /// +---------+ +-----------+ +-----------+ /// | | | /// | T_0 E1 | | /// |-------------------------------->| | /// |-------------------------------\ | | /// || record event and start timer |-| | /// ||------------------------------| | | /// | | | /// | T_30 E2 | | /// |-------------------------------->| | /// | -----------------------------\ | | /// | | timer not expired yet: |-| | /// | | reset timer and last event | | | /// | |----------------------------| | | /// | T_60 E3 | | /// |-------------------------------->| | /// | -----------------------------\ | | /// | | timer not expired yet: |-| | /// | | reset timer and last event | | | /// | |----------------------------| | | /// | | T_110 E3 | /// | |--------------->| /// | ---------------------------\ | | /// | | time expired: emit event |-| | /// | |--------------------------| | | /// | | | /// | T_200 E4 | | /// |-------------------------------->| | /// | ---------------------------\ | | /// | | time expired: emit event |-| | /// | |--------------------------| | | /// | | | /// | | T_250 E4 | /// | |--------------->| /// | | | #[pin_project] pub struct Debounce { period: Duration, #[pin] stream: S, #[pin] timer: Option, last_item: Option, has_expired: bool, } pub fn debounce(period: Duration, stream: S) -> impl Stream where S: Stream, { Debounce { period, stream, timer: None, last_item: None, has_expired: false, } } impl Stream for Debounce where S: Stream, { type Item = S::Item; fn poll_next(mut self: Pin<&mut Self>, ctx: &mut Context<'_>) -> Poll> { let mut this = self.as_mut().project(); // Stream has expired already--return Poll::Ready(None). if *this.has_expired { return Poll::Ready(None); } let mut stream = this.stream; // Read out everything from the stream until the stream is exhausted. while let Poll::Ready(item) = stream.as_mut().poll_next(ctx) { match item { Some(item) => { *this.last_item = Some(item); *this.timer = Some(Timer::after(*this.period)); } None => { *this.timer = None; *this.has_expired = true; return Poll::Ready(this.last_item.take()); } } } if let Some(timer) = this.timer.as_pin_mut() { ready!(timer.poll_next(ctx)); // The timer is done--return the last item. let mut this = self.project(); *this.timer = None; return Poll::Ready(this.last_item.take()); } Poll::Pending } }