Skip to main content

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}