Skip to main content

leptos_use/
use_broadcast_channel.rs

1use crate::core::OptionLocalRwSignal;
2use crate::{
3    UseEventListenerOptions, core::OptionLocalSignal, js, sendwrap_fn, use_event_listener,
4    use_event_listener_with_options, use_supported,
5};
6use codee::{CodecError, Decoder, Encoder};
7use leptos::ev::messageerror;
8use leptos::prelude::*;
9use thiserror::Error;
10use wasm_bindgen::JsValue;
11
12/// Reactive [BroadcastChannel API](https://developer.mozilla.org/en-US/docs/Web/API/BroadcastChannel).
13///
14/// Closes a broadcast channel automatically when the component is cleaned up.
15///
16/// ## Demo
17///
18/// [Link to Demo](https://github.com/Synphonyte/leptos-use/tree/main/examples/use_broadcast_channel)
19///
20/// ## Usage
21///
22/// The BroadcastChannel interface represents a named channel that any browsing context of a given origin can subscribe to. It allows communication between different documents (in different windows, tabs, frames, or iframes) of the same origin.
23///
24/// Messages are broadcasted via a message event fired at all BroadcastChannel objects listening to the channel.
25///
26/// ```
27/// # use leptos::prelude::*;
28/// # use leptos_use::{use_broadcast_channel, UseBroadcastChannelReturn};
29/// # use codee::string::FromToStringCodec;
30/// #
31/// # #[component]
32/// # fn Demo() -> impl IntoView {
33/// let UseBroadcastChannelReturn {
34///     is_supported,
35///     message,
36///     post,
37///     error,
38///     close,
39///     ..
40/// } = use_broadcast_channel::<bool, FromToStringCodec>("some-channel-name");
41///
42/// post(&true);
43///
44/// close();
45/// #
46/// # view! { }
47/// # }
48/// ```
49///
50/// Values are (en)decoded via the given codec. You can use any of the string codecs or a
51/// binary codec wrapped in `Base64`.
52///
53/// > Please check [the codec chapter](https://leptos-use.rs/codecs.html) to see what codecs are
54/// > available and what feature flags they require.
55///
56/// ```
57/// # use leptos::prelude::*;
58/// # use serde::{Deserialize, Serialize};
59/// # use leptos_use::use_broadcast_channel;
60/// # use codee::string::JsonSerdeCodec;
61/// #
62/// // Data sent in JSON must implement Serialize, Deserialize:
63/// #[derive(Serialize, Deserialize, Clone, PartialEq)]
64/// pub struct MyState {
65///     pub playing_lego: bool,
66///     pub everything_is_awesome: String,
67/// }
68///
69/// # #[component]
70/// # fn Demo() -> impl IntoView {
71/// use_broadcast_channel::<MyState, JsonSerdeCodec>("everyting-is-awesome");
72/// # view! { }
73/// # }
74/// ```
75///
76/// ## SendWrapped Return
77///
78/// The returned closures `post` and `close` are sendwrapped functions. They can
79/// only be called from the same thread that called `use_broadcast_channel`.
80pub fn use_broadcast_channel<T, C>(
81    name: &str,
82) -> UseBroadcastChannelReturn<
83    T,
84    impl Fn(&T) + Clone + Send + Sync + use<T, C>,
85    impl Fn() + Clone + Send + Sync,
86    C,
87>
88where
89    T: Send + Sync,
90    C: Encoder<T, Encoded = String> + Decoder<T, Encoded = str> + Send + Sync,
91    <C as Encoder<T>>::Error: Send + Sync,
92    <C as Decoder<T>>::Error: Send + Sync,
93{
94    let (is_closed, set_closed) = signal(false);
95    let (message, set_message) = signal(None::<T>);
96    let channel = OptionLocalRwSignal::<web_sys::BroadcastChannel>::new();
97    let error = OptionLocalRwSignal::<
98        UseBroadcastChannelError<<C as Encoder<T>>::Error, <C as Decoder<T>>::Error>,
99    >::new();
100
101    let is_supported = use_supported(|| js!("BroadcastChannel" in &window()));
102
103    let post = {
104        sendwrap_fn!(move |data: &T| {
105            if let Some(channel) = channel.get_untracked() {
106                match C::encode(data) {
107                    Ok(msg) => {
108                        if let Err(err) = channel.post_message(&msg.into()) {
109                            error.set(Some(UseBroadcastChannelError::PostMessage(err)))
110                        }
111                    }
112                    Err(err) => {
113                        error.set(Some(UseBroadcastChannelError::Codec(CodecError::Encode(
114                            err,
115                        ))));
116                    }
117                }
118            }
119        })
120    };
121
122    let close = {
123        sendwrap_fn!(move || {
124            if let Some(channel) = channel.get_untracked() {
125                channel.close();
126            }
127            set_closed.set(true);
128        })
129    };
130
131    // initialize the BroadcastChannel and event listeners, if supported by the browser
132    // we do this in an Effect to make sure it runs after mount
133    Effect::new({
134        let name = name.to_string();
135        move |_| {
136            if is_supported.get() {
137                let channel_val = web_sys::BroadcastChannel::new(&name).ok();
138                channel.set(channel_val.clone());
139
140                if let Some(channel) = channel_val {
141                    let _ = use_event_listener_with_options(
142                        channel.clone(),
143                        leptos::ev::message,
144                        move |event| {
145                            if let Some(data) = event.data().as_string() {
146                                match C::decode(&data) {
147                                    Ok(msg) => {
148                                        set_message.set(Some(msg));
149                                    }
150                                    Err(err) => error.set(Some(UseBroadcastChannelError::Codec(
151                                        CodecError::Decode(err),
152                                    ))),
153                                }
154                            } else {
155                                error.set(Some(UseBroadcastChannelError::ValueNotString));
156                            }
157                        },
158                        UseEventListenerOptions::default().passive(true),
159                    );
160
161                    let _ = use_event_listener_with_options(
162                        channel.clone(),
163                        messageerror,
164                        move |event| {
165                            error.set(Some(UseBroadcastChannelError::MessageEvent(event)));
166                        },
167                        UseEventListenerOptions::default().passive(true),
168                    );
169
170                    let _ = use_event_listener(channel, leptos::ev::close, move |_| {
171                        set_closed.set(true)
172                    });
173                }
174            }
175        }
176    });
177
178    on_cleanup({
179        let close = close.clone();
180
181        move || {
182            close();
183        }
184    });
185
186    UseBroadcastChannelReturn {
187        is_supported,
188        channel: channel.read_only(),
189        message: message.into(),
190        post,
191        close,
192        error: error.read_only(),
193        is_closed: is_closed.into(),
194    }
195}
196
197/// Return type of [`use_broadcast_channel`].
198pub struct UseBroadcastChannelReturn<T, PFn, CFn, C>
199where
200    T: Send + Sync + 'static,
201    PFn: Fn(&T) + Clone,
202    CFn: Fn() + Clone,
203    C: Encoder<T> + Decoder<T> + Send + Sync,
204{
205    /// `true` if this browser supports `BroadcastChannel`s.
206    pub is_supported: Signal<bool>,
207
208    /// The broadcast channel that is wrapped by this function
209    pub channel: OptionLocalSignal<web_sys::BroadcastChannel>,
210
211    /// Latest message received from the channel
212    pub message: Signal<Option<T>>,
213
214    /// Sends a message through the channel
215    pub post: PFn,
216
217    /// Closes the channel
218    pub close: CFn,
219
220    /// Latest error as reported by the `messageerror` event.
221    pub error: OptionLocalSignal<ErrorType<T, C>>,
222
223    /// Wether the channel is closed
224    pub is_closed: Signal<bool>,
225}
226
227type ErrorType<T, C> = UseBroadcastChannelError<<C as Encoder<T>>::Error, <C as Decoder<T>>::Error>;
228
229#[derive(Debug, Error)]
230pub enum UseBroadcastChannelError<E, D> {
231    #[error("failed to post message")]
232    PostMessage(JsValue),
233    #[error("channel message error")]
234    MessageEvent(web_sys::MessageEvent),
235    #[error("failed to (de)encode value")]
236    Codec(CodecError<E, D>),
237    #[error("received value is not a string")]
238    ValueNotString,
239}
240
241impl<E, D> Clone for UseBroadcastChannelError<E, D>
242where
243    E: Clone,
244    D: Clone,
245    CodecError<E, D>: Clone,
246{
247    fn clone(&self) -> Self {
248        match self {
249            UseBroadcastChannelError::PostMessage(v) => {
250                UseBroadcastChannelError::PostMessage(v.clone())
251            }
252            UseBroadcastChannelError::MessageEvent(v) => {
253                UseBroadcastChannelError::MessageEvent(v.clone())
254            }
255            UseBroadcastChannelError::Codec(v) => UseBroadcastChannelError::Codec(v.clone()),
256            UseBroadcastChannelError::ValueNotString => UseBroadcastChannelError::ValueNotString,
257        }
258    }
259}