leptos_use/
use_broadcast_channel.rs

1use crate::{
2    UseEventListenerOptions, core::OptionLocalSignal, js, sendwrap_fn, use_event_listener,
3    use_event_listener_with_options, use_supported,
4};
5use codee::{CodecError, Decoder, Encoder};
6use leptos::ev::messageerror;
7use leptos::prelude::*;
8use send_wrapper::SendWrapper;
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, set_channel) = signal(None::<SendWrapper<web_sys::BroadcastChannel>>);
97    let (error, set_error) = signal(
98        None::<
99            SendWrapper<
100                UseBroadcastChannelError<<C as Encoder<T>>::Error, <C as Decoder<T>>::Error>,
101            >,
102        >,
103    );
104
105    let is_supported = use_supported(|| js!("BroadcastChannel" in &window()));
106
107    let post = {
108        sendwrap_fn!(move |data: &T| {
109            if let Some(channel) = channel.get_untracked() {
110                match C::encode(data) {
111                    Ok(msg) => {
112                        if let Err(err) = channel.post_message(&msg.into()) {
113                            set_error.set(Some(SendWrapper::new(
114                                UseBroadcastChannelError::PostMessage(err),
115                            )))
116                        }
117                    }
118                    Err(err) => {
119                        set_error.set(Some(SendWrapper::new(UseBroadcastChannelError::Codec(
120                            CodecError::Encode(err),
121                        ))));
122                    }
123                }
124            }
125        })
126    };
127
128    let close = {
129        sendwrap_fn!(move || {
130            if let Some(channel) = channel.get_untracked() {
131                channel.close();
132            }
133            set_closed.set(true);
134        })
135    };
136
137    // initialize the BroadcastChannel and event listeners, if supported by the browser
138    // we do this in an Effect to make sure it runs after mount
139    Effect::new({
140        let name = name.to_string();
141        move |_| {
142            if is_supported.get() {
143                let channel_val = web_sys::BroadcastChannel::new(&name).ok();
144                set_channel.set(channel_val.clone().map(SendWrapper::new));
145
146                if let Some(channel) = channel_val {
147                    let _ = use_event_listener_with_options(
148                        channel.clone(),
149                        leptos::ev::message,
150                        move |event| {
151                            if let Some(data) = event.data().as_string() {
152                                match C::decode(&data) {
153                                    Ok(msg) => {
154                                        set_message.set(Some(msg));
155                                    }
156                                    Err(err) => set_error.set(Some(SendWrapper::new(
157                                        UseBroadcastChannelError::Codec(CodecError::Decode(err)),
158                                    ))),
159                                }
160                            } else {
161                                set_error.set(Some(SendWrapper::new(
162                                    UseBroadcastChannelError::ValueNotString,
163                                )));
164                            }
165                        },
166                        UseEventListenerOptions::default().passive(true),
167                    );
168
169                    let _ = use_event_listener_with_options(
170                        channel.clone(),
171                        messageerror,
172                        move |event| {
173                            set_error.set(Some(SendWrapper::new(
174                                UseBroadcastChannelError::MessageEvent(event),
175                            )));
176                        },
177                        UseEventListenerOptions::default().passive(true),
178                    );
179
180                    let _ = use_event_listener(channel, leptos::ev::close, move |_| {
181                        set_closed.set(true)
182                    });
183                }
184            }
185        }
186    });
187
188    on_cleanup({
189        let close = close.clone();
190
191        move || {
192            close();
193        }
194    });
195
196    UseBroadcastChannelReturn {
197        is_supported,
198        channel: channel.into(),
199        message: message.into(),
200        post,
201        close,
202        error: error.into(),
203        is_closed: is_closed.into(),
204    }
205}
206
207/// Return type of [`use_broadcast_channel`].
208pub struct UseBroadcastChannelReturn<T, PFn, CFn, C>
209where
210    T: Send + Sync + 'static,
211    PFn: Fn(&T) + Clone,
212    CFn: Fn() + Clone,
213    C: Encoder<T> + Decoder<T> + Send + Sync,
214{
215    /// `true` if this browser supports `BroadcastChannel`s.
216    pub is_supported: Signal<bool>,
217
218    /// The broadcast channel that is wrapped by this function
219    pub channel: OptionLocalSignal<web_sys::BroadcastChannel>,
220
221    /// Latest message received from the channel
222    pub message: Signal<Option<T>>,
223
224    /// Sends a message through the channel
225    pub post: PFn,
226
227    /// Closes the channel
228    pub close: CFn,
229
230    /// Latest error as reported by the `messageerror` event.
231    pub error: OptionLocalSignal<ErrorType<T, C>>,
232
233    /// Wether the channel is closed
234    pub is_closed: Signal<bool>,
235}
236
237type ErrorType<T, C> = UseBroadcastChannelError<<C as Encoder<T>>::Error, <C as Decoder<T>>::Error>;
238
239#[derive(Debug, Error)]
240pub enum UseBroadcastChannelError<E, D> {
241    #[error("failed to post message")]
242    PostMessage(JsValue),
243    #[error("channel message error")]
244    MessageEvent(web_sys::MessageEvent),
245    #[error("failed to (de)encode value")]
246    Codec(CodecError<E, D>),
247    #[error("received value is not a string")]
248    ValueNotString,
249}
250
251impl<E, D> Clone for UseBroadcastChannelError<E, D>
252where
253    E: Clone,
254    D: Clone,
255    CodecError<E, D>: Clone,
256{
257    fn clone(&self) -> Self {
258        match self {
259            UseBroadcastChannelError::PostMessage(v) => {
260                UseBroadcastChannelError::PostMessage(v.clone())
261            }
262            UseBroadcastChannelError::MessageEvent(v) => {
263                UseBroadcastChannelError::MessageEvent(v.clone())
264            }
265            UseBroadcastChannelError::Codec(v) => UseBroadcastChannelError::Codec(v.clone()),
266            UseBroadcastChannelError::ValueNotString => UseBroadcastChannelError::ValueNotString,
267        }
268    }
269}