Skip to main content

tuwunel_core/utils/stream/
tools.rs

1//! StreamTools for futures::Stream
2
3use std::{collections::HashMap, hash::Hash};
4
5use arrayvec::ArrayVec;
6use futures::{Stream, StreamExt};
7
8use super::ReadyExt;
9use crate::{expected, utils::rand::index};
10
11/// Adds aggregation, sampling, and folding operations to streams.
12///
13/// Counting adapters consume the source into hash maps, while folding avoids an
14/// intermediate collection. Reservoir sampling retains a uniform subset of up
15/// to a fixed size in one pass.
16pub trait Tools<Item>
17where
18	Self: Stream<Item = Item> + Send + Sized,
19	<Self as Stream>::Item: Send,
20{
21	/// Counts occurrences of each distinct stream item.
22	///
23	/// The entire stream is consumed into a hash map that starts at zero
24	/// capacity and grows as needed. Equal items share one incrementing
25	/// counter.
26	///
27	/// # Panics
28	///
29	/// Panics if an item's occurrence count overflows `usize`.
30	fn counts(self) -> impl Future<Output = HashMap<Item, usize>> + Send
31	where
32		<Self as Stream>::Item: Eq + Hash;
33
34	/// Counts occurrences of keys derived from stream items.
35	///
36	/// `f` is applied once to every item before counting. The result map starts
37	/// at zero capacity and grows as needed.
38	///
39	/// # Panics
40	///
41	/// Panics if a derived key's occurrence count overflows `usize`.
42	fn counts_by<K, F>(self, f: F) -> impl Future<Output = HashMap<K, usize>> + Send
43	where
44		F: Fn(Item) -> K + Send,
45		K: Eq + Hash + Send;
46
47	/// Counts derived keys into a map with initial capacity `CAP`.
48	///
49	/// `f` is applied once to every stream item. The map initially reserves
50	/// space for at least `CAP` distinct keys and grows as needed.
51	///
52	/// # Panics
53	///
54	/// Panics if a derived key's occurrence count overflows `usize`.
55	fn counts_by_with_cap<const CAP: usize, K, F>(
56		self,
57		f: F,
58	) -> impl Future<Output = HashMap<K, usize>> + Send
59	where
60		F: Fn(Item) -> K + Send,
61		K: Eq + Hash + Send;
62
63	/// Counts items into a map with initial capacity `CAP`.
64	///
65	/// Equal items share one counter as the entire stream is consumed. The map
66	/// initially reserves space for at least `CAP` distinct items and grows as
67	/// needed.
68	///
69	/// # Panics
70	///
71	/// Panics if an item's occurrence count overflows `usize`.
72	fn counts_with_cap<const CAP: usize>(
73		self,
74	) -> impl Future<Output = HashMap<Item, usize>> + Send
75	where
76		<Self as Stream>::Item: Eq + Hash;
77
78	/// Reservoir-samples up to `N` items uniformly without replacement.
79	///
80	/// The stream is consumed in one pass, and `f` is applied only when an item
81	/// enters the reservoir, including entries later replaced. Rejected items
82	/// do not invoke it, and derived keys may repeat.
83	///
84	/// # Panics
85	///
86	/// Panics if the number of observed stream items overflows `usize`.
87	fn sample_by<const N: usize, K, F>(self, f: F) -> impl Future<Output = ArrayVec<K, N>> + Send
88	where
89		F: Fn(Item) -> K + Send,
90		K: Send;
91
92	/// Folds the stream from `T::default()` with an asynchronous accumulator.
93	///
94	/// Each item is processed in source order after the previous fold future
95	/// resolves. The final accumulator is returned when the stream ends.
96	fn fold_default<T, F, Fut>(self, f: F) -> impl Future<Output = T> + Send
97	where
98		F: Fn(T, Item) -> Fut + Send,
99		Fut: Future<Output = T> + Send,
100		T: Default + Send;
101}
102
103impl<Item, S> Tools<Item> for S
104where
105	S: Stream<Item = Item> + Send + Sized,
106	<Self as Stream>::Item: Send,
107{
108	#[inline]
109	fn counts(self) -> impl Future<Output = HashMap<Item, usize>> + Send
110	where
111		<Self as Stream>::Item: Eq + Hash,
112	{
113		self.counts_with_cap::<0>()
114	}
115
116	#[inline]
117	fn counts_by<K, F>(self, f: F) -> impl Future<Output = HashMap<K, usize>> + Send
118	where
119		F: Fn(Item) -> K + Send,
120		K: Eq + Hash + Send,
121	{
122		self.counts_by_with_cap::<0, K, F>(f)
123	}
124
125	#[inline]
126	fn counts_by_with_cap<const CAP: usize, K, F>(
127		self,
128		f: F,
129	) -> impl Future<Output = HashMap<K, usize>> + Send
130	where
131		F: Fn(Item) -> K + Send,
132		K: Eq + Hash + Send,
133	{
134		self.map(f).counts_with_cap::<CAP>()
135	}
136
137	#[inline]
138	fn counts_with_cap<const CAP: usize>(
139		self,
140	) -> impl Future<Output = HashMap<Item, usize>> + Send
141	where
142		<Self as Stream>::Item: Eq + Hash,
143	{
144		self.ready_fold(HashMap::with_capacity(CAP), |mut counts, item| {
145			let entry = counts.entry(item).or_default();
146			let value = *entry;
147			*entry = expected!(value + 1);
148			counts
149		})
150	}
151
152	#[inline]
153	fn sample_by<const N: usize, K, F>(self, f: F) -> impl Future<Output = ArrayVec<K, N>> + Send
154	where
155		F: Fn(Item) -> K + Send,
156		K: Send,
157	{
158		self.enumerate()
159			.ready_fold(ArrayVec::<K, N>::new(), move |mut reservoir, (i, item)| {
160				if reservoir.len() < N {
161					reservoir.push(f(item));
162				} else {
163					let slot = index(expected!(i + 1));
164					if slot < N {
165						reservoir[slot] = f(item);
166					}
167				}
168
169				reservoir
170			})
171	}
172
173	#[inline]
174	fn fold_default<T, F, Fut>(self, f: F) -> impl Future<Output = T> + Send
175	where
176		F: Fn(T, Item) -> Fut + Send,
177		Fut: Future<Output = T> + Send,
178		T: Default + Send,
179	{
180		self.fold(T::default(), f)
181	}
182}