leptos_use/
use_broadcast_channel.rs

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