Skip to main content

rskit_stream/
ext.rs

1//! [`RskitStreamExt`] — ergonomic extension methods on [`futures::Stream`].
2
3use std::future::Future;
4use std::hash::Hash;
5use std::time::Duration;
6
7use futures::Stream;
8use futures::StreamExt as _;
9use rskit_errors::AppResult;
10
11use crate::operators::{concurrent, rate, transform, windowing};
12
13/// Extension trait adding rskit-specific operators to any [`Stream`].
14///
15/// Imported with `use rskit_stream::RskitStreamExt;`.
16#[allow(async_fn_in_trait)]
17pub trait RskitStreamExt: Stream + Sized + Send + 'static
18where
19    Self::Item: Send + 'static,
20{
21    /// Async map — apply a fallible async function to each item.
22    fn rmap<O, F, Fut>(self, f: F) -> impl Stream<Item = AppResult<O>> + Send + 'static
23    where
24        O: Send + 'static,
25        F: FnMut(Self::Item) -> Fut + Send + 'static,
26        Fut: Future<Output = AppResult<O>> + Send + 'static,
27    {
28        self.then(f)
29    }
30
31    /// Async flat-map — apply a function that returns a stream and flatten one level.
32    fn rflatmap<O, F, Fut, St>(self, f: F) -> impl Stream<Item = AppResult<O>> + Send + 'static
33    where
34        O: Send + 'static,
35        F: FnMut(Self::Item) -> Fut + Send + 'static,
36        Fut: Future<Output = AppResult<St>> + Send + 'static,
37        St: Stream<Item = AppResult<O>> + Send + Unpin + 'static,
38    {
39        self.then(f).flat_map(|result| match result {
40            Ok(s) => s.left_stream(),
41            Err(e) => futures::stream::once(futures::future::ready(Err(e))).right_stream(),
42        })
43    }
44
45    /// Keep only items that satisfy the (synchronous) predicate.
46    fn rfilter<F>(self, mut f: F) -> impl Stream<Item = Self::Item> + Send + 'static
47    where
48        F: FnMut(&Self::Item) -> bool + Send + 'static,
49    {
50        self.filter(move |item| {
51            let keep = f(item);
52            std::future::ready(keep)
53        })
54    }
55
56    /// Emit only the first occurrence of each item.
57    fn rdistinct(self) -> impl Stream<Item = Self::Item> + Send + 'static
58    where
59        Self::Item: Clone + Eq + Hash,
60    {
61        transform::distinct(self)
62    }
63
64    /// Side-effect for each item — does not modify the stream.
65    fn rtap<F, Fut>(self, mut f: F) -> impl Stream<Item = Self::Item> + Send + 'static
66    where
67        F: FnMut(&Self::Item) -> Fut + Send + 'static,
68        Fut: Future<Output = ()> + Send + 'static,
69    {
70        self.then(move |item| {
71            let fut = f(&item);
72            async move {
73                fut.await;
74                item
75            }
76        })
77    }
78
79    /// Take the first `n` items from the stream.
80    fn rtake(self, n: usize) -> impl Stream<Item = Self::Item> + Send + 'static {
81        self.take(n)
82    }
83
84    /// Skip the first `n` items from the stream.
85    fn rskip(self, n: usize) -> impl Stream<Item = Self::Item> + Send + 'static {
86        self.skip(n)
87    }
88
89    /// Split the stream into `(matching, remainder)` streams.
90    fn rpartition<F>(
91        self,
92        predicate: F,
93    ) -> (
94        impl Stream<Item = Self::Item> + Send + 'static,
95        impl Stream<Item = Self::Item> + Send + 'static,
96    )
97    where
98        F: FnMut(&Self::Item) -> bool + Send + 'static,
99    {
100        transform::partition(self, predicate)
101    }
102
103    /// Fold the entire stream into a single value.
104    async fn rreduce<Acc, F>(self, init: Acc, mut f: F) -> Acc
105    where
106        Acc: Send + 'static,
107        F: FnMut(Acc, Self::Item) -> Acc + Send + 'static,
108    {
109        let mut acc = init;
110        let this = self;
111        tokio::pin!(this);
112        while let Some(item) = this.next().await {
113            acc = f(acc, item);
114        }
115        acc
116    }
117
118    /// Process up to `concurrency` items in parallel (unordered output).
119    fn rparallel<O, F, Fut>(
120        self,
121        concurrency: usize,
122        f: F,
123    ) -> impl Stream<Item = AppResult<O>> + Send + 'static
124    where
125        O: Send + 'static,
126        F: Fn(Self::Item) -> Fut + Clone + Send + Sync + 'static,
127        Fut: Future<Output = AppResult<O>> + Send + 'static,
128    {
129        concurrent::parallel(self, concurrency, f)
130    }
131
132    /// Apply multiple functions to the same item and collect all results.
133    fn rfan_out<O, F, Fut>(
134        self,
135        max_branches: usize,
136        fns: Vec<F>,
137    ) -> impl Stream<Item = AppResult<Vec<O>>> + Send + 'static
138    where
139        O: Send + 'static,
140        Self::Item: Clone,
141        F: Fn(Self::Item) -> Fut + Clone + Send + Sync + 'static,
142        Fut: Future<Output = AppResult<O>> + Send + 'static,
143    {
144        concurrent::fan_out(self, max_branches, fns)
145    }
146
147    /// Collect items into non-overlapping windows bounded by time and item count.
148    fn rtumbling_window(
149        self,
150        duration: Duration,
151        max_items: usize,
152    ) -> impl Stream<Item = Vec<Self::Item>> + Send + 'static {
153        windowing::tumbling_window(self, duration, max_items)
154    }
155
156    /// Emit sliding windows of `size` items, advancing by `step` items each time.
157    fn rsliding_window(
158        self,
159        size: usize,
160        step: usize,
161    ) -> impl Stream<Item = Vec<Self::Item>> + Send + 'static
162    where
163        Self::Item: Clone,
164    {
165        windowing::sliding_window(self, size, step)
166    }
167
168    /// Emit batches of up to `size` items or when `timeout` elapses.
169    fn rbatch(
170        self,
171        size: usize,
172        timeout: Duration,
173    ) -> impl Stream<Item = Vec<Self::Item>> + Send + 'static {
174        windowing::batch(self, size, timeout)
175    }
176
177    /// Emit only when no new item arrives within `delay`.
178    fn rdebounce(self, delay: Duration) -> impl Stream<Item = Self::Item> + Send + 'static {
179        rate::debounce(self, delay)
180    }
181
182    /// Accumulate items into a batch,
183    /// emitting it once `quiet` elapses with no new item (trailing-edge debounce that keeps every item, not just the last)
184    /// or early once `max_items` accumulate.
185    fn rdebounce_batch(
186        self,
187        quiet: Duration,
188        max_items: usize,
189    ) -> impl Stream<Item = Vec<Self::Item>> + Send + 'static {
190        rate::debounce_batch(self, quiet, max_items)
191    }
192
193    /// Emit at most one item per `interval`.
194    fn rthrottle(self, interval: Duration) -> impl Stream<Item = Self::Item> + Send + 'static {
195        rate::throttle(self, interval)
196    }
197
198    /// Merge this stream with another, yielding items from whichever is ready first.
199    fn rmerge(
200        self,
201        other: impl Stream<Item = Self::Item> + Send + 'static,
202    ) -> impl Stream<Item = Self::Item> + Send + 'static {
203        futures::stream::select(self, other)
204    }
205}
206
207impl<S> RskitStreamExt for S
208where
209    S: Stream + Send + 'static,
210    S::Item: Send + 'static,
211{
212}