dioxus_radio/hooks/
use_radio.rs

1use std::{
2    collections::{HashMap, HashSet},
3    hash::Hash,
4    ops::{Deref, DerefMut},
5    sync::{Arc, Mutex},
6};
7
8use dioxus_lib::prelude::*;
9mod warnings {
10    pub use warnings::Warning;
11}
12pub use warnings::Warning;
13
14#[cfg(feature = "tracing")]
15pub trait RadioChannel<T>: 'static + PartialEq + Eq + Clone + Hash + std::fmt::Debug + Ord {
16    fn derive_channel(self, _radio: &T) -> Vec<Self> {
17        vec![self]
18    }
19}
20
21#[cfg(not(feature = "tracing"))]
22pub trait RadioChannel<T>: 'static + PartialEq + Eq + Clone + Hash {
23    fn derive_channel(self, _radio: &T) -> Vec<Self> {
24        vec![self]
25    }
26}
27
28/// Holds a global state and all its subscribers.
29pub struct RadioStation<Value, Channel>
30where
31    Channel: RadioChannel<Value>,
32    Value: 'static,
33{
34    value: Signal<Value>,
35    listeners: Signal<HashMap<Channel, Arc<Mutex<HashSet<ReactiveContext>>>>>,
36}
37
38impl<Value, Channel> Clone for RadioStation<Value, Channel>
39where
40    Channel: RadioChannel<Value>,
41{
42    fn clone(&self) -> Self {
43        *self
44    }
45}
46
47impl<Value, Channel> Copy for RadioStation<Value, Channel> where Channel: RadioChannel<Value> {}
48
49impl<Value, Channel> RadioStation<Value, Channel>
50where
51    Channel: RadioChannel<Value>,
52{
53    pub(crate) fn is_listening(
54        &self,
55        channel: &Channel,
56        reactive_context: &ReactiveContext,
57    ) -> bool {
58        let listeners = self.listeners.peek_unchecked();
59        listeners
60            .get(channel)
61            .map(|contexts| contexts.lock().unwrap().contains(reactive_context))
62            .unwrap_or_default()
63    }
64
65    pub(crate) fn listen(&self, channel: Channel, reactive_context: ReactiveContext) {
66        dioxus_lib::prelude::warnings::signal_write_in_component_body::allow(|| {
67            let mut listeners = self.listeners.write_unchecked();
68            let listeners = listeners.entry(channel).or_default();
69            reactive_context.subscribe(listeners.clone());
70        });
71    }
72
73    pub(crate) fn notify_listeners(&self, channel: &Channel) {
74        let listeners = self.listeners.write_unchecked();
75
76        #[cfg(feature = "tracing")]
77        tracing::info!("Notifying {channel:?}");
78
79        for (listener_channel, listeners) in listeners.iter() {
80            if listener_channel == channel {
81                for reactive_context in listeners.lock().unwrap().iter() {
82                    reactive_context.mark_dirty();
83                }
84            }
85        }
86    }
87
88    /// Read the current state value. This effectively subscribes to any change no matter the channel.
89    ///
90    /// Example:
91    ///
92    /// ```rs
93    /// let value = radio.read();
94    /// ```
95    pub fn read(&self) -> ReadableRef<Signal<Value>> {
96        self.value.read()
97    }
98
99    /// Read the current state value without subscribing.
100    ///
101    /// Example:
102    ///
103    /// ```rs
104    /// let value = radio.peek();
105    /// ```
106    pub fn peek(&self) -> ReadableRef<Signal<Value>> {
107        self.value.peek()
108    }
109
110    pub fn cleanup(&self) {
111        let mut listeners = self.listeners.write_unchecked();
112
113        // Clean up those channels with no reactive contexts
114        listeners.retain(|_, listeners| !listeners.lock().unwrap().is_empty());
115
116        #[cfg(feature = "tracing")]
117        {
118            use itertools::Itertools;
119            use tracing::{info, span, Level};
120
121            let mut channels_subscribers = HashMap::<&Channel, usize>::new();
122
123            for (channel, listeners) in listeners.iter() {
124                *channels_subscribers.entry(&channel).or_default() =
125                    listeners.lock().unwrap().len();
126            }
127
128            let span = span!(Level::DEBUG, "Radio Station Metrics");
129            let _enter = span.enter();
130
131            for (channel, count) in channels_subscribers.iter().sorted() {
132                info!(" {count} subscribers for {channel:?}")
133            }
134        }
135    }
136}
137
138pub struct RadioAntenna<Value, Channel>
139where
140    Channel: RadioChannel<Value>,
141    Value: 'static,
142{
143    pub(crate) channel: Channel,
144    station: RadioStation<Value, Channel>,
145}
146
147impl<Value, Channel> RadioAntenna<Value, Channel>
148where
149    Channel: RadioChannel<Value>,
150{
151    pub(crate) fn new(
152        channel: Channel,
153        station: RadioStation<Value, Channel>,
154    ) -> RadioAntenna<Value, Channel> {
155        RadioAntenna { channel, station }
156    }
157}
158
159pub struct RadioGuard<Value, Channel>
160where
161    Channel: RadioChannel<Value>,
162    Value: 'static,
163{
164    antenna: Signal<RadioAntenna<Value, Channel>>,
165    channels: Vec<Channel>,
166    value: WritableRef<'static, Signal<Value>>,
167}
168
169impl<Value, Channel> Drop for RadioGuard<Value, Channel>
170where
171    Channel: RadioChannel<Value>,
172{
173    fn drop(&mut self) {
174        for channel in &mut self.channels {
175            self.antenna.peek().station.notify_listeners(channel)
176        }
177        if !self.channels.is_empty() {
178            self.antenna.peek().station.cleanup();
179        }
180    }
181}
182
183impl<Value, Channel> Deref for RadioGuard<Value, Channel>
184where
185    Channel: RadioChannel<Value>,
186{
187    type Target = WritableRef<'static, Signal<Value>>;
188
189    fn deref(&self) -> &Self::Target {
190        &self.value
191    }
192}
193
194impl<Value, Channel> DerefMut for RadioGuard<Value, Channel>
195where
196    Channel: RadioChannel<Value>,
197{
198    fn deref_mut(&mut self) -> &mut WritableRef<'static, Signal<Value>> {
199        &mut self.value
200    }
201}
202
203/// `Radio` lets you access the state and is subscribed given it's `Channel`.
204pub struct Radio<Value, Channel>
205where
206    Channel: RadioChannel<Value>,
207    Value: 'static,
208{
209    antenna: Signal<RadioAntenna<Value, Channel>>,
210}
211
212impl<Value, Channel> Clone for Radio<Value, Channel>
213where
214    Channel: RadioChannel<Value>,
215{
216    fn clone(&self) -> Self {
217        *self
218    }
219}
220impl<Value, Channel> Copy for Radio<Value, Channel> where Channel: RadioChannel<Value> {}
221
222impl<Value, Channel> PartialEq for Radio<Value, Channel>
223where
224    Channel: RadioChannel<Value>,
225{
226    fn eq(&self, other: &Self) -> bool {
227        self.antenna == other.antenna
228    }
229}
230
231impl<Value, Channel> Radio<Value, Channel>
232where
233    Channel: RadioChannel<Value>,
234{
235    pub(crate) fn new(antenna: Signal<RadioAntenna<Value, Channel>>) -> Radio<Value, Channel> {
236        Radio { antenna }
237    }
238
239    pub(crate) fn subscribe_if_not(&self) {
240        dioxus_lib::prelude::warnings::signal_write_in_component_body::allow(|| {
241            if let Some(rc) = ReactiveContext::current() {
242                let antenna = &self.antenna.write_unchecked();
243                let channel = antenna.channel.clone();
244                let is_listening = antenna.station.is_listening(&channel, &rc);
245
246                // Subscribe the reader reactive context to the channel if it wasn't already
247                if !is_listening {
248                    antenna.station.listen(channel, rc);
249                }
250            }
251        });
252    }
253
254    /// Read the current state value.
255    ///
256    /// Example:
257    ///
258    /// ```rs
259    /// let value = radio.read();
260    /// ```
261    pub fn read(&self) -> ReadableRef<Signal<Value>> {
262        self.subscribe_if_not();
263        self.antenna.peek().station.value.peek_unchecked()
264    }
265
266    /// Read the current state value inside a callback.
267    ///
268    /// Example:
269    ///
270    /// ```rs
271    /// radio.with(|value| {
272    ///     // Do something with `value`
273    /// });
274    /// ```
275    pub fn with(&self, cb: impl FnOnce(ReadableRef<Signal<Value>>)) {
276        self.subscribe_if_not();
277        let value = self.antenna.peek().station.value;
278        let borrow = value.read();
279        cb(borrow);
280    }
281
282    /// Modify the state using the channel this radio was created with.
283    ///
284    /// Example:
285    ///
286    /// ```rs
287    /// radio.write().value = 1;
288    /// ```
289    pub fn write(&mut self) -> RadioGuard<Value, Channel> {
290        let value = self.antenna.peek().station.value.write_unchecked();
291        let channel = self.antenna.peek().channel.clone();
292        RadioGuard {
293            channels: channel.derive_channel(&*value),
294            antenna: self.antenna,
295            value,
296        }
297    }
298
299    /// Get a mutable reference to the current state value, inside a callback.
300    ///
301    /// Example:
302    ///
303    /// ```rs
304    /// radio.write_with(|value| {
305    ///     // Modify `value`
306    /// });
307    /// ```
308    pub fn write_with(&mut self, cb: impl FnOnce(RadioGuard<Value, Channel>)) {
309        let guard = self.write();
310        cb(guard);
311    }
312
313    /// Modify the state using a custom Channel.
314    ///
315    /// ## Example:
316    /// ```rs, no_run
317    /// radio.write(Channel::Whatever).value = 1;
318    /// ```
319    pub fn write_channel(&mut self, channel: Channel) -> RadioGuard<Value, Channel> {
320        let value = self.antenna.peek().station.value.write_unchecked();
321        RadioGuard {
322            channels: channel.derive_channel(&*value),
323            antenna: self.antenna,
324            value,
325        }
326    }
327
328    /// Get a mutable reference to the current state value, inside a callback.
329    ///
330    /// Example:
331    ///
332    /// ```rs
333    /// radio.write_channel_with(Channel::Whatever, |value| {
334    ///     // Modify `value`
335    /// });
336    /// ```
337    pub fn write_channel_with(
338        &mut self,
339        channel: Channel,
340        cb: impl FnOnce(RadioGuard<Value, Channel>),
341    ) {
342        let guard = self.write_channel(channel);
343        cb(guard);
344    }
345
346    /// Get a mutable reference to the current state value, inside a callback that returns the channel to be used.
347    ///
348    /// Example:
349    ///
350    /// ```rs
351    /// radio.write_with_channel_selection(|value| {
352    ///     // Modify `value`
353    ///     if value.cool {
354    ///         ChannelSelection::Select(Channel::Whatever)
355    ///     } else {
356    ///         ChannelSelection::Silence
357    ///     }
358    /// });
359    /// ```
360    pub fn write_with_channel_selection(
361        &mut self,
362        cb: impl FnOnce(&mut Value) -> ChannelSelection<Channel>,
363    ) -> ChannelSelection<Channel> {
364        let value = self.antenna.peek().station.value.write_unchecked();
365        let mut guard = RadioGuard {
366            channels: Vec::default(),
367            antenna: self.antenna,
368            value,
369        };
370        let channel_selection = cb(&mut guard.value);
371        let channel = match channel_selection.clone() {
372            ChannelSelection::Current => Some(self.antenna.peek().channel.clone()),
373            ChannelSelection::Silence => None,
374            ChannelSelection::Select(c) => Some(c),
375        };
376        if let Some(channel) = channel {
377            for channel in channel.derive_channel(&guard.value) {
378                self.antenna.peek().station.notify_listeners(&channel)
379            }
380            self.antenna.peek().station.cleanup();
381        }
382
383        channel_selection
384    }
385
386    /// Modify the state silently, no component will be notified.
387    ///
388    /// This is not recommended, the only intended usage for this is inside [RadioAsyncReducer].
389    ///
390    pub fn write_silently(&mut self) -> RadioGuard<Value, Channel> {
391        let value = self.antenna.peek().station.value.write_unchecked();
392        RadioGuard {
393            channels: Vec::default(),
394            antenna: self.antenna,
395            value,
396        }
397    }
398}
399
400impl<Channel> Copy for ChannelSelection<Channel> where Channel: Copy {}
401
402#[derive(Clone)]
403pub enum ChannelSelection<Channel> {
404    /// Notify the channel associated with the used [Radio].
405    Current,
406    /// Notify a given [Channel].
407    Select(Channel),
408    /// No subscriber will be notified.
409    Silence,
410}
411
412impl<Channel> ChannelSelection<Channel> {
413    /// Change to [ChannelSelection::Current]
414    pub fn current(&mut self) {
415        *self = Self::Current
416    }
417
418    /// Change to [ChannelSelection::Select]
419    pub fn select(&mut self, channel: Channel) {
420        *self = Self::Select(channel)
421    }
422
423    /// Change to [ChannelSelection::Silence]
424    pub fn silence(&mut self) {
425        *self = Self::Silence
426    }
427
428    /// Check if it is of type [ChannelSelection::Current]
429    pub fn is_current(&self) -> bool {
430        matches!(self, Self::Current)
431    }
432
433    /// Check if it is of type [ChannelSelection::Select] and return the channel.
434    pub fn is_select(&self) -> Option<&Channel> {
435        match self {
436            Self::Select(channel) => Some(channel),
437            _ => None,
438        }
439    }
440
441    /// Check if it is of type [ChannelSelection::Silence]
442    pub fn is_silence(&self) -> bool {
443        matches!(self, Self::Silence)
444    }
445}
446
447/// Consume the state and subscribe using the given `channel`
448/// Any mutation using this radio will notify other subscribers to the same `channel`,
449/// unless you explicitely pass a custom channel using other methods as [`Radio::write_channel()`]
450pub fn use_radio<Value, Channel>(channel: Channel) -> Radio<Value, Channel>
451where
452    Channel: RadioChannel<Value>,
453    Value: 'static,
454{
455    let station = use_context::<RadioStation<Value, Channel>>();
456
457    let mut radio = use_hook(|| {
458        let antenna = RadioAntenna::new(channel.clone(), station);
459        Radio::new(Signal::new(antenna))
460    });
461
462    if radio.antenna.peek().channel != channel {
463        radio.antenna.write().channel = channel;
464    }
465
466    radio
467}
468
469pub fn use_init_radio_station<Value, Channel>(
470    init_value: impl FnOnce() -> Value,
471) -> RadioStation<Value, Channel>
472where
473    Channel: RadioChannel<Value>,
474    Value: 'static,
475{
476    use_context_provider(|| RadioStation {
477        value: Signal::new(init_value()),
478        listeners: Signal::default(),
479    })
480}
481
482pub fn use_radio_station<Value, Channel>() -> RadioStation<Value, Channel>
483where
484    Channel: RadioChannel<Value>,
485    Value: 'static,
486{
487    use_context::<RadioStation<Value, Channel>>()
488}
489
490pub trait DataReducer {
491    type Channel;
492    type Action;
493
494    fn reduce(&mut self, action: Self::Action) -> ChannelSelection<Self::Channel>;
495}
496
497pub trait RadioReducer {
498    type Action;
499    type Channel;
500
501    fn apply(&mut self, action: Self::Action) -> ChannelSelection<Self::Channel>;
502}
503
504impl<
505        Data: DataReducer<Channel = Channel, Action = Action>,
506        Channel: RadioChannel<Data>,
507        Action,
508    > RadioReducer for Radio<Data, Channel>
509{
510    type Action = Action;
511    type Channel = Channel;
512
513    fn apply(&mut self, action: Action) -> ChannelSelection<Channel> {
514        self.write_with_channel_selection(|data| data.reduce(action))
515    }
516}
517
518pub trait DataAsyncReducer {
519    type Channel;
520    type Action;
521
522    #[allow(async_fn_in_trait)]
523    async fn async_reduce(
524        _radio: &mut Radio<Self, Self::Channel>,
525        _action: Self::Action,
526    ) -> ChannelSelection<Self::Channel>
527    where
528        Self::Channel: RadioChannel<Self>,
529        Self: Sized;
530}
531
532pub trait RadioAsyncReducer {
533    type Action;
534
535    fn async_apply(&mut self, _action: Self::Action)
536    where
537        Self::Action: 'static;
538}
539
540impl<
541        Data: DataAsyncReducer<Channel = Channel, Action = Action>,
542        Channel: RadioChannel<Data>,
543        Action,
544    > RadioAsyncReducer for Radio<Data, Channel>
545{
546    type Action = Action;
547
548    fn async_apply(&mut self, action: Self::Action)
549    where
550        Self::Action: 'static,
551    {
552        let mut radio = *self;
553        spawn(async move {
554            let channel = Data::async_reduce(&mut radio, action).await;
555            radio.write_with_channel_selection(|_| channel);
556        });
557    }
558}