medea_reactive/collections/hash_set.rs
1//! Reactive hash set based on [`HashSet`].
2
3use std::{
4 collections::{HashSet as StdHashSet, hash_set::Iter},
5 hash::Hash,
6 marker::PhantomData,
7};
8
9use futures::stream::{self, LocalBoxStream};
10
11use crate::subscribers_store::{
12 SubscribersStore, common, progressable,
13 progressable::{AllProcessed, Processed},
14};
15
16/// Reactive hash set based on [`HashSet`] with an ability to recognize when all
17/// updates was processed by subscribers.
18pub type ProgressableHashSet<T> =
19 HashSet<T, progressable::SubStore<T>, progressable::Guarded<T>>;
20
21/// Reactive hash set based on [`HashSet`].
22pub type ObservableHashSet<T> = HashSet<T, common::SubStore<T>, T>;
23
24/// Reactive hash set based on [`HashSet`].
25///
26/// # Usage
27///
28/// ```rust
29/// # use futures::{executor, StreamExt as _, Stream};
30/// # use std::collections::HashSet;
31/// use medea_reactive::collections::ObservableHashSet;
32///
33/// # executor::block_on(async {
34/// let mut set = ObservableHashSet::new();
35///
36/// // You can subscribe on insert action:
37/// let mut inserts = set.on_insert();
38///
39/// set.insert("foo");
40///
41/// let item = inserts.next().await.unwrap();
42/// assert_eq!(item, "foo");
43///
44/// // Also you can subscribe on remove action:
45/// let mut removals = set.on_remove();
46///
47/// set.remove(&"foo");
48///
49/// let removed_item = removals.next().await.unwrap();
50/// assert_eq!(removed_item, "foo");
51///
52/// // When you update HashSet by another HashSet all events will
53/// // work fine:
54/// set.insert("foo-1");
55/// set.insert("foo-2");
56/// set.insert("foo-3");
57///
58/// let mut set_for_update = HashSet::new();
59/// set_for_update.insert("foo-1");
60/// set_for_update.insert("foo-4");
61/// set.update(set_for_update);
62///
63/// let removed_items: HashSet<_> = removals.take(2).collect().await;
64/// let inserted_item = inserts.skip(3).next().await.unwrap();
65/// assert!(removed_items.contains("foo-2"));
66/// assert!(removed_items.contains("foo-3"));
67/// assert_eq!(inserted_item, "foo-4");
68/// assert!(set.contains(&"foo-1"));
69/// assert!(set.contains(&"foo-4"));
70/// # });
71/// ```
72///
73/// # Waiting for subscribers to complete
74///
75/// ```rust
76/// # use futures::{executor, StreamExt as _, Stream};
77/// use medea_reactive::collections::ProgressableHashSet;
78///
79/// # executor::block_on(async {
80/// let mut hash_set = ProgressableHashSet::new();
81///
82/// let mut on_insert = hash_set.on_insert();
83/// hash_set.insert(1);
84///
85/// // hash_set.when_insert_processed().await; <- wouldn't be resolved
86/// let value = on_insert.next().await.unwrap();
87/// // hash_set.when_insert_processed().await; <- wouldn't be resolved
88/// drop(value);
89///
90/// hash_set.when_insert_processed().await; // will be resolved
91///
92/// # });
93/// ```
94#[derive(Debug)]
95pub struct HashSet<T, S: SubscribersStore<T, O>, O> {
96 /// Data stored by this [`HashSet`].
97 store: StdHashSet<T>,
98
99 /// Subscribers of the [`HashSet::on_insert()`] method.
100 on_insert_subs: S,
101
102 /// Subscribers of the [`HashSet::on_remove()`] method.
103 on_remove_subs: S,
104
105 /// Phantom type of [`HashSet::on_insert()`] and [`HashSet::on_remove()`]
106 /// output.
107 _output: PhantomData<O>,
108}
109
110impl<T> ProgressableHashSet<T>
111where
112 T: Clone + 'static,
113{
114 /// Returns [`Future`] resolving when all push updates will be processed by
115 /// [`HashSet::on_insert()`] subscribers.
116 pub fn when_insert_processed(&self) -> Processed<'static> {
117 self.on_insert_subs.when_all_processed()
118 }
119
120 /// Returns [`Future`] resolving when all remove updates will be processed
121 /// by [`HashSet::on_remove()`] subscribers.
122 pub fn when_remove_processed(&self) -> Processed<'static> {
123 self.on_remove_subs.when_all_processed()
124 }
125
126 /// Returns [`Future`] resolving when all insert and remove updates will be
127 /// processed by subscribers.
128 pub fn when_all_processed(&self) -> AllProcessed<'static> {
129 crate::when_all_processed(vec![
130 self.when_remove_processed().into(),
131 self.when_insert_processed().into(),
132 ])
133 }
134}
135
136impl<T, S: SubscribersStore<T, O>, O> HashSet<T, S, O> {
137 /// Creates new empty [`HashSet`].
138 #[must_use]
139 pub fn new() -> Self {
140 Self::default()
141 }
142
143 /// Returns [`Iterator`] visiting all values in an arbitrary order.
144 pub fn iter(&self) -> impl Iterator<Item = &T> {
145 self.into_iter()
146 }
147
148 /// Returns [`Stream`] yielding inserted values to this [`HashSet`].
149 ///
150 /// [`Stream`]: futures::Stream
151 #[must_use]
152 pub fn on_insert(&self) -> LocalBoxStream<'static, O> {
153 self.on_insert_subs.subscribe()
154 }
155
156 /// Returns the [`Stream`] yielding removed values from this [`HashSet`].
157 ///
158 /// Note, that this [`Stream`] will yield all values of this [`HashSet`] on
159 /// [`Drop`].
160 ///
161 /// [`Stream`]: futures::Stream
162 #[must_use]
163 pub fn on_remove(&self) -> LocalBoxStream<'static, O> {
164 self.on_remove_subs.subscribe()
165 }
166}
167
168impl<T, S, O> HashSet<T, S, O>
169where
170 T: Clone + 'static,
171 S: SubscribersStore<T, O>,
172 O: 'static,
173{
174 /// Returns [`Stream`] containing values from this [`HashSet`].
175 ///
176 /// Returned [`Stream`] contains only current values. It won't update on new
177 /// inserts, but you can merge returned [`Stream`] with a
178 /// [`HashSet::on_insert()`] [`Stream`] if you want to process current
179 /// values and values that will be inserted.
180 ///
181 /// [`Stream`]: futures::Stream
182 #[expect(clippy::needless_collect, reason = "false positive: lifetimes")]
183 pub fn replay_on_insert(&self) -> LocalBoxStream<'static, O> {
184 Box::pin(stream::iter(
185 self.store
186 .clone()
187 .into_iter()
188 .map(|val| self.on_insert_subs.wrap(val))
189 .collect::<Vec<_>>(),
190 ))
191 }
192}
193
194impl<T, S, O> HashSet<T, S, O>
195where
196 T: Clone + Hash + Eq + 'static,
197 S: SubscribersStore<T, O>,
198{
199 /// Adds the `value` to this [`HashSet`].
200 ///
201 /// If it didn't have such `value` present, `true` is returned.
202 ///
203 /// If it did have such `value` present, `false` is returned.
204 ///
205 /// This will produce [`HashSet::on_inser()t`] event.
206 pub fn insert(&mut self, value: T) -> bool {
207 if self.store.insert(value.clone()) {
208 self.on_insert_subs.send_update(value);
209 true
210 } else {
211 false
212 }
213 }
214
215 /// Removes the `value` from this [`HashSet`] and returns it, if any.
216 ///
217 /// This will produce [`HashSet::on_remove()`] event.
218 pub fn remove(&mut self, value: &T) -> Option<T> {
219 let value = self.store.take(value);
220
221 if let Some(val) = &value {
222 self.on_remove_subs.send_update(val.clone());
223 }
224
225 value
226 }
227
228 /// Makes this [`HashSet`] exactly the same as the `updated` one.
229 ///
230 /// It will calculate a diff between this [`HashSet`] and the `updated`, and
231 /// will spawn [`HashSet::on_insert()`] and [`HashSet::on_remove()`] if the
232 /// diff is not empty.
233 ///
234 /// For the usage example you can read [`HashSet`] docs.
235 pub fn update(&mut self, updated: StdHashSet<T>) {
236 let removed_elems = self.store.difference(&updated);
237 let inserted_elems = updated.difference(&self.store);
238
239 for removed_elem in removed_elems {
240 self.on_remove_subs.send_update(removed_elem.clone());
241 }
242
243 for inserted_elem in inserted_elems {
244 self.on_insert_subs.send_update(inserted_elem.clone());
245 }
246
247 self.store = updated;
248 }
249
250 /// Indicates whether this [`HashSet`] contains the `value`.
251 #[must_use]
252 pub fn contains(&self, value: &T) -> bool {
253 self.store.contains(value)
254 }
255}
256
257// Implemented manually to omit redundant `: Default` trait bounds, imposed by
258// `#[derive(Default)]`.
259impl<T, S, O> Default for HashSet<T, S, O>
260where
261 S: SubscribersStore<T, O>,
262{
263 fn default() -> Self {
264 Self {
265 store: StdHashSet::new(),
266 on_insert_subs: S::default(),
267 on_remove_subs: S::default(),
268 _output: PhantomData,
269 }
270 }
271}
272
273impl<'a, T, S: SubscribersStore<T, O>, O> IntoIterator
274 for &'a HashSet<T, S, O>
275{
276 type IntoIter = Iter<'a, T>;
277 type Item = &'a T;
278
279 fn into_iter(self) -> Self::IntoIter {
280 self.store.iter()
281 }
282}
283
284impl<T, S, O> Drop for HashSet<T, S, O>
285where
286 S: SubscribersStore<T, O>,
287{
288 /// Sends all values of a dropped [`HashSet`] to the
289 /// [`HashSet::on_remove()`] subscriptions.
290 fn drop(&mut self) {
291 #[expect(clippy::iter_over_hash_type, reason = "order doesn't matter")]
292 for val in self.store.drain() {
293 self.on_remove_subs.send_update(val);
294 }
295 }
296}
297
298impl<T, S, O> From<StdHashSet<T>> for HashSet<T, S, O>
299where
300 S: SubscribersStore<T, O>,
301{
302 fn from(from: StdHashSet<T>) -> Self {
303 Self {
304 store: from,
305 on_insert_subs: S::default(),
306 on_remove_subs: S::default(),
307 _output: PhantomData,
308 }
309 }
310}