Skip to main content

leptos_use/
use_websocket.rs

1use crate::{ReconnectLimit, core::ConnectionReadyState};
2use cfg_if::cfg_if;
3use codee::{CodecError, Decoder, Encoder, HybridCoderError, HybridDecoder, HybridEncoder};
4use default_struct_builder::DefaultBuilder;
5use leptos::prelude::*;
6use std::marker::PhantomData;
7use std::sync::Arc;
8use thiserror::Error;
9use web_sys::{CloseEvent, Event};
10
11#[allow(rustdoc::bare_urls)]
12/// Creating and managing a [Websocket](https://developer.mozilla.org/en-US/docs/Web/API/WebSocket) connection.
13///
14/// ## Demo
15///
16/// [Link to Demo](https://github.com/Synphonyte/leptos-use/tree/main/examples/use_websocket)
17///
18/// ## Usage
19///
20/// Values are (en)decoded via the given codec. You can use any of the codecs, string or binary.
21///
22/// > Please check [the codec chapter](https://leptos-use.rs/codecs.html) to see what codecs are
23/// > available and what feature flags they require.
24///
25/// ```
26/// # use leptos::prelude::*;
27/// # use codee::string::FromToStringCodec;
28/// # use leptos_use::{use_websocket, UseWebSocketReturn};
29/// # use leptos_use::core::ConnectionReadyState;
30/// #
31/// # #[component]
32/// # fn Demo() -> impl IntoView {
33/// let UseWebSocketReturn {
34///     ready_state,
35///     message,
36///     send,
37///     open,
38///     close,
39///     ..
40/// } = use_websocket::<String, String, FromToStringCodec>("wss://echo.websocket.events/");
41///
42/// let send_message = move |_| {
43///     send(&"Hello, world!".to_string());
44/// };
45///
46/// let status = move || ready_state.get().to_string();
47///
48/// let connected = move || ready_state.get() == ConnectionReadyState::Open;
49///
50/// let open_connection = move |_| {
51///     open();
52/// };
53///
54/// let close_connection = move |_| {
55///     close();
56/// };
57///
58/// view! {
59///     <div>
60///         <p>"status: " {status}</p>
61///
62///         <button on:click=send_message disabled=move || !connected()>"Send"</button>
63///         <button on:click=open_connection disabled=connected>"Open"</button>
64///         <button on:click=close_connection disabled=move || !connected()>"Close"</button>
65///
66///         <p>"Receive message: " {move || format!("{:?}", message.get())}</p>
67///     </div>
68/// }
69/// # }
70/// ```
71///
72/// Here is another example using `msgpack` for encoding and decoding. This means that only binary
73/// messages can be sent or received. For this to work you have to enable the **`msgpack_serde` feature** flag.
74///
75/// ```
76/// # use leptos::*;
77/// # use codee::binary::MsgpackSerdeCodec;
78/// # use leptos_use::{use_websocket, UseWebSocketReturn};
79/// # use serde::{Deserialize, Serialize};
80/// #
81/// # #[component]
82/// # fn Demo() -> impl IntoView {
83/// #[derive(Serialize, Deserialize)]
84/// struct SomeData {
85///     name: String,
86///     count: i32,
87/// }
88///
89/// let UseWebSocketReturn {
90///     message,
91///     send,
92///     ..
93/// } = use_websocket::<SomeData, SomeData, MsgpackSerdeCodec>("wss://some.websocket.server/");
94///
95/// let send_data = move || {
96///     send(&SomeData {
97///         name: "John Doe".to_string(),
98///         count: 42,
99///     });
100/// };
101/// #
102/// # view! {}
103/// }
104/// ```
105///
106/// ### Heartbeats
107///
108/// Heartbeats can be configured by the `heartbeat` option. You have to provide a heartbeat
109/// type, that implements the `Default` trait and an `Encoder` for it. This encoder doesn't have
110/// to be the same as the one used for the other websocket messages.
111///
112/// ```
113/// # use leptos::*;
114/// # use codee::string::FromToStringCodec;
115/// # use leptos_use::{use_websocket_with_options, UseWebSocketOptions, UseWebSocketReturn};
116/// # use serde::{Deserialize, Serialize};
117/// #
118/// # #[component]
119/// # fn Demo() -> impl IntoView {
120/// #[derive(Default)]
121/// struct Heartbeat;
122///
123/// // Simple example for usage with `FromToStringCodec`
124/// impl std::fmt::Display for Heartbeat {
125///     fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
126///         write!(f, "<Heartbeat>")
127///     }
128/// }
129///
130/// let UseWebSocketReturn {
131///     send,
132///     message,
133///     ..
134/// } = use_websocket_with_options::<String, String, FromToStringCodec, _, _>(
135///     "wss://echo.websocket.events/",
136///     UseWebSocketOptions::default()
137///         // Enable heartbeats every 10 seconds. In this case we use the same codec as for the
138///         // other messages. But this is not necessary.
139///         .heartbeat::<Heartbeat, FromToStringCodec>(10_000),
140/// );
141/// #
142/// # view! {}
143/// }
144/// ```
145///
146/// ## Relative Paths
147///
148/// If the provided `url` is relative, it will be resolved relative to the current page.
149/// Urls will be resolved like this the following. Please note that the protocol (http vs https) will
150/// be taken into account as well.
151///
152/// | Current Page                   | Relative Url             | Resolved Url                        |
153/// |--------------------------------|--------------------------|-------------------------------------|
154/// | http://example.com/some/where  | /api/ws                  | ws://example.com/api/ws             |
155/// | https://example.com/some/where | /api/ws                  | wss://example.com/api/ws            |
156/// | https://example.com/some/where | api/ws                   | wss://example.com/some/where/api/ws |
157/// | https://example.com/some/where | //otherdomain.com/api/ws | wss://otherdomain.com/api/ws        |
158///
159///
160/// ## Usage with `provide_context`
161///
162/// The return value of `use_websocket` utilizes several type parameters which can make it
163/// cumbersome to use with `provide_context` + `expect_context`.
164/// The following example shows how to avoid type parameters with dynamic dispatch.
165/// This sacrifices a little bit of performance for the sake of ergonomics. However,
166/// compared to network transmission speeds this loss of performance is negligible.
167///
168/// First we define the `struct` that is going to be passed around as context.
169///
170/// ```
171/// # use leptos::prelude::*;
172/// use std::sync::Arc;
173///
174/// #[derive(Clone)]
175/// pub struct WebsocketContext {
176///     pub message: Signal<Option<String>>,
177///     send: Arc<dyn Fn(&String)>,  // use Arc to make it easily cloneable
178/// }
179///
180/// impl WebsocketContext {
181///     pub fn new(message: Signal<Option<String>>, send: Arc<dyn Fn(&String)>) -> Self {
182///         Self {
183///             message,
184///             send,
185///         }
186///     }
187///
188///     // create a method to avoid having to use parantheses around the field
189///     #[inline(always)]
190///     pub fn send(&self, message: &str) {
191///         (self.send)(&message.to_string())
192///     }
193/// }
194/// ```
195///
196/// Now you can provide the context like the following.
197///
198/// ```
199/// # use leptos::prelude::*;
200/// # use codee::string::FromToStringCodec;
201/// # use leptos_use::{use_websocket, UseWebSocketReturn};
202/// # use std::sync::Arc;
203/// # #[derive(Clone)]
204/// # pub struct WebsocketContext {
205/// #     pub message: Signal<Option<String>>,
206/// #     send: Arc<dyn Fn(&String) + Send + Sync>,
207/// # }
208/// #
209/// # impl WebsocketContext {
210/// #     pub fn new(message: Signal<Option<String>>, send: Arc<dyn Fn(&String) + Send + Sync>) -> Self {
211/// #         Self {
212/// #             message,
213/// #             send,
214/// #         }
215/// #     }
216/// # }
217///
218/// # #[component]
219/// # fn Demo() -> impl IntoView {
220/// let UseWebSocketReturn {
221///     message,
222///     send,
223///     ..
224/// } = use_websocket::<String, String, FromToStringCodec>("ws:://some.websocket.io");
225///
226/// provide_context(WebsocketContext::new(message, Arc::new(send.clone())));
227/// #
228/// # view! {}
229/// # }
230/// ```
231///
232/// Finally let's use the context:
233///
234/// ```
235/// # use leptos::prelude::*;
236/// # use leptos_use::{use_websocket, UseWebSocketReturn};
237/// # use std::sync::Arc;
238/// # #[derive(Clone)]
239/// # pub struct WebsocketContext {
240/// #     pub message: Signal<Option<String>>,
241/// #     send: Arc<dyn Fn(&String)>,
242/// # }
243/// #
244/// # impl WebsocketContext {
245/// #     #[inline(always)]
246/// #     pub fn send(&self, message: &str) {
247/// #         (self.send)(&message.to_string())
248/// #     }
249/// # }
250///
251/// # #[component]
252/// # fn Demo() -> impl IntoView {
253/// let websocket = expect_context::<WebsocketContext>();
254///
255/// websocket.send("Hello World!");
256/// #
257/// # view! {}
258/// # }
259/// ```
260///
261/// ## SendWrapped Return
262///
263/// The returned closures `open`, `close`, and `send` are sendwrapped functions. They can
264/// only be called from the same thread that called `use_websocket`.
265///
266/// ## Server-Side Rendering
267///
268/// > Make sure you follow the [instructions in Server-Side Rendering](https://leptos-use.rs/server_side_rendering.html).
269///
270/// On the server the returned functions amount to no-ops.
271pub fn use_websocket<Tx, Rx, C>(
272    url: &str,
273) -> UseWebSocketReturn<
274    Tx,
275    Rx,
276    impl Fn() + Clone + Send + Sync + 'static,
277    impl Fn() + Clone + Send + Sync + 'static,
278    impl Fn(&Tx) + Clone + Send + Sync + 'static,
279>
280where
281    Tx: Send + Sync + 'static,
282    Rx: Send + Sync + 'static,
283    C: Encoder<Tx> + Decoder<Rx>,
284    C: HybridEncoder<Tx, <C as Encoder<Tx>>::Encoded, Error = <C as Encoder<Tx>>::Error>,
285    C: HybridDecoder<Rx, <C as Decoder<Rx>>::Encoded, Error = <C as Decoder<Rx>>::Error>,
286{
287    use_websocket_with_options::<Tx, Rx, C, (), DummyEncoder>(url, UseWebSocketOptions::default())
288}
289
290/// Version of [`use_websocket`] that takes `UseWebSocketOptions`. See [`use_websocket`] for how to use.
291#[allow(clippy::type_complexity)]
292pub fn use_websocket_with_options<Tx, Rx, C, Hb, HbCodec>(
293    url: &str,
294    options: UseWebSocketOptions<
295        Rx,
296        HybridCoderError<<C as Encoder<Tx>>::Error>,
297        HybridCoderError<<C as Decoder<Rx>>::Error>,
298        Hb,
299        HbCodec,
300    >,
301) -> UseWebSocketReturn<
302    Tx,
303    Rx,
304    impl Fn() + Clone + Send + Sync + 'static,
305    impl Fn() + Clone + Send + Sync + 'static,
306    impl Fn(&Tx) + Clone + Send + Sync + 'static,
307>
308where
309    Tx: Send + Sync + 'static,
310    Rx: Send + Sync + 'static,
311    C: Encoder<Tx> + Decoder<Rx>,
312    C: HybridEncoder<Tx, <C as Encoder<Tx>>::Encoded, Error = <C as Encoder<Tx>>::Error>,
313    C: HybridDecoder<Rx, <C as Decoder<Rx>>::Encoded, Error = <C as Decoder<Rx>>::Error>,
314    Hb: Default + Send + Sync + 'static,
315    HbCodec: Encoder<Hb> + Send + Sync,
316    HbCodec: HybridEncoder<
317            Hb,
318            <HbCodec as Encoder<Hb>>::Encoded,
319            Error = <HbCodec as Encoder<Hb>>::Error,
320        >,
321    <HbCodec as Encoder<Hb>>::Error: std::fmt::Debug,
322{
323    let url = normalize_url(url);
324
325    let UseWebSocketOptions {
326        on_open,
327        on_message,
328        on_message_raw,
329        on_message_raw_bytes,
330        on_error,
331        on_close,
332        reconnect_limit,
333        reconnect_interval,
334        immediate,
335        protocols,
336        heartbeat,
337    } = options;
338
339    let (ready_state, set_ready_state) = signal(ConnectionReadyState::Closed);
340    let (message, set_message) = signal(None);
341
342    let open;
343    let close;
344    let send;
345
346    #[cfg(not(feature = "ssr"))]
347    {
348        use crate::{sendwrap_fn, use_interval_fn, utils::Pausable};
349        use js_sys::Array;
350        use leptos::leptos_dom::helpers::TimeoutHandle;
351        use std::sync::atomic::AtomicBool;
352        use std::time::Duration;
353        use wasm_bindgen::prelude::*;
354        use web_sys::{BinaryType, MessageEvent, WebSocket};
355
356        let ws = StoredValue::new_local(None::<WebSocket>);
357
358        let reconnect_timer_ref: StoredValue<Option<TimeoutHandle>> = StoredValue::new(None);
359
360        let reconnect_times_ref: StoredValue<u64> = StoredValue::new(0);
361        let manually_closed_ref: StoredValue<bool> = StoredValue::new(false);
362
363        // Identifies the socket the handlers below belong to. Handlers of a socket that has
364        // since been superseded must not touch the shared state anymore.
365        let connection_id_ref: StoredValue<u64> = StoredValue::new(0);
366
367        let unmounted = Arc::new(AtomicBool::new(false));
368
369        let connect_ref: StoredValue<Option<Arc<dyn Fn() + Send + Sync>>> = StoredValue::new(None);
370
371        let send_str = move |data: &str| {
372            if ready_state.get_untracked() == ConnectionReadyState::Open
373                && let Some(web_socket) = ws.get_value()
374            {
375                let _ = web_socket.send_with_str(data);
376            }
377        };
378
379        let send_bytes = move |data: &[u8]| {
380            if ready_state.get_untracked() == ConnectionReadyState::Open
381                && let Some(web_socket) = ws.get_value()
382            {
383                let _ = web_socket.send_with_u8_array(data);
384            }
385        };
386
387        send = {
388            let on_error = Arc::clone(&on_error);
389
390            sendwrap_fn!(move |value: &Tx| {
391                let on_error = Arc::clone(&on_error);
392
393                send_with_codec::<Tx, C>(value, send_str, send_bytes, move |err| {
394                    on_error(UseWebSocketError::Codec(CodecError::Encode(err)));
395                });
396            })
397        };
398
399        let heartbeat_interval_ref = StoredValue::new_local(None::<(Arc<dyn Fn()>, Arc<dyn Fn()>)>);
400
401        let stop_heartbeat = move || {
402            if let Some((pause, _)) = heartbeat_interval_ref.get_value() {
403                pause();
404            }
405        };
406
407        let start_heartbeat = {
408            let on_error = Arc::clone(&on_error);
409
410            move || {
411                if let Some(heartbeat) = &heartbeat {
412                    if let Some((pause, resume)) = heartbeat_interval_ref.get_value() {
413                        pause();
414                        resume();
415                    } else {
416                        let on_error = Arc::clone(&on_error);
417
418                        let Pausable { pause, resume, .. } = use_interval_fn(
419                            move || {
420                                send_with_codec::<Hb, HbCodec>(
421                                    &Hb::default(),
422                                    send_str,
423                                    send_bytes,
424                                    {
425                                        let on_error = Arc::clone(&on_error);
426
427                                        move |err| {
428                                            on_error(UseWebSocketError::HeartbeatCodec(format!(
429                                                "Failed to encode heartbeat data: {err:?}"
430                                            )))
431                                        }
432                                    },
433                                )
434                            },
435                            heartbeat.interval,
436                        );
437
438                        heartbeat_interval_ref.set_value(Some((Arc::new(pause), Arc::new(resume))));
439                    }
440                }
441            }
442        };
443
444        let reconnect_ref: StoredValue<Option<Arc<dyn Fn() + Send + Sync>>> =
445            StoredValue::new(None);
446        reconnect_ref.set_value({
447            let unmounted = Arc::clone(&unmounted);
448
449            Some(Arc::new(move || {
450                let unmounted = Arc::clone(&unmounted);
451
452                if !manually_closed_ref.get_value()
453                    && !reconnect_limit.is_exceeded_by(reconnect_times_ref.get_value())
454                    && ws
455                        .get_value()
456                        .is_some_and(|ws: WebSocket| ws.ready_state() != WebSocket::OPEN)
457                    && reconnect_timer_ref.get_value().is_none()
458                {
459                    reconnect_timer_ref.set_value(
460                        set_timeout_with_handle(
461                            move || {
462                                if unmounted.load(std::sync::atomic::Ordering::Relaxed) {
463                                    return;
464                                }
465                                if let Some(connect) = connect_ref.get_value() {
466                                    connect();
467                                    reconnect_times_ref.update_value(|current| *current += 1);
468                                }
469                            },
470                            Duration::from_millis(reconnect_interval),
471                        )
472                        .ok(),
473                    );
474                }
475            }))
476        });
477
478        connect_ref.set_value({
479            let unmounted = Arc::clone(&unmounted);
480            let on_error = Arc::clone(&on_error);
481
482            Some(Arc::new(move || {
483                if let Some(reconnect_timer) = reconnect_timer_ref.get_value() {
484                    reconnect_timer.clear();
485                    reconnect_timer_ref.set_value(None);
486                }
487
488                if let Some(web_socket) = ws.get_value() {
489                    let _ = web_socket.close();
490                }
491
492                connection_id_ref.update_value(|id| *id += 1);
493                let connection_id = connection_id_ref.get_value();
494
495                let web_socket = {
496                    protocols.with_untracked(|protocols| {
497                        protocols.as_ref().map_or_else(
498                            || WebSocket::new(&url).unwrap_throw(),
499                            |protocols| {
500                                let array = protocols
501                                    .iter()
502                                    .map(|p| JsValue::from(p.clone()))
503                                    .collect::<Array>();
504                                WebSocket::new_with_str_sequence(&url, &JsValue::from(&array))
505                                    .unwrap_throw()
506                            },
507                        )
508                    })
509                };
510                web_socket.set_binary_type(BinaryType::Arraybuffer);
511                set_ready_state.set(ConnectionReadyState::Connecting);
512
513                // onopen handler
514                {
515                    let unmounted = Arc::clone(&unmounted);
516                    let on_open = Arc::clone(&on_open);
517
518                    let onopen_closure = Closure::wrap(Box::new({
519                        let start_heartbeat = start_heartbeat.clone();
520
521                        move |e: Event| {
522                            if unmounted.load(std::sync::atomic::Ordering::Relaxed)
523                                || connection_id_ref.get_value() != connection_id
524                            {
525                                return;
526                            }
527
528                            #[cfg(debug_assertions)]
529                            let zone = leptos::reactive::diagnostics::SpecialNonReactiveZone::enter();
530
531                            on_open(e);
532
533                            #[cfg(debug_assertions)]
534                            drop(zone);
535
536                            set_ready_state.set(ConnectionReadyState::Open);
537
538                            start_heartbeat();
539                        }
540                    })
541                        as Box<dyn FnMut(Event)>);
542                    web_socket.set_onopen(Some(onopen_closure.as_ref().unchecked_ref()));
543                    // Forget the closure to keep it alive
544                    onopen_closure.forget();
545                }
546
547                // onmessage handler
548                {
549                    let unmounted = Arc::clone(&unmounted);
550                    let on_message = Arc::clone(&on_message);
551                    let on_message_raw = Arc::clone(&on_message_raw);
552                    let on_message_raw_bytes = Arc::clone(&on_message_raw_bytes);
553                    let on_error = Arc::clone(&on_error);
554
555                    let onmessage_closure = Closure::wrap(Box::new(move |e: MessageEvent| {
556                        if unmounted.load(std::sync::atomic::Ordering::Relaxed)
557                            || connection_id_ref.get_value() != connection_id
558                        {
559                            return;
560                        }
561
562                        e.data().dyn_into::<js_sys::ArrayBuffer>().map_or_else(
563                            |_| {
564                                e.data().dyn_into::<js_sys::JsString>().map_or_else(
565                                    |_| {
566                                        unreachable!(
567                                            "message event, received Unknown: {:?}",
568                                            e.data()
569                                        );
570                                    },
571                                    |txt| {
572                                        let txt = String::from(&txt);
573
574                                        #[cfg(debug_assertions)]
575                                        let zone = leptos::reactive::diagnostics::SpecialNonReactiveZone::enter();
576
577                                        on_message_raw(&txt);
578
579                                        #[cfg(debug_assertions)]
580                                        drop(zone);
581
582                                        match C::decode_str(&txt) {
583                                            Ok(val) => {
584                                                #[cfg(debug_assertions)]
585                                                let prev = leptos::reactive::diagnostics::SpecialNonReactiveZone::enter();
586
587                                                on_message(&val);
588
589                                                #[cfg(debug_assertions)]
590                                                drop(prev);
591
592                                                set_message.set(Some(val));
593                                            }
594                                            Err(err) => {
595                                                on_error(CodecError::Decode(err).into());
596                                            }
597                                        }
598                                    },
599                                );
600                            },
601                            |array_buffer| {
602                                let array = js_sys::Uint8Array::new(&array_buffer);
603                                let array = array.to_vec();
604
605                                #[cfg(debug_assertions)]
606                                let zone = leptos::reactive::diagnostics::SpecialNonReactiveZone::enter();
607
608                                on_message_raw_bytes(&array);
609
610                                #[cfg(debug_assertions)]
611                                drop(zone);
612
613                                match C::decode_bin(array.as_slice()) {
614                                    Ok(val) => {
615                                        #[cfg(debug_assertions)]
616                                        let prev = leptos::reactive::diagnostics::SpecialNonReactiveZone::enter();
617
618                                        on_message(&val);
619
620                                        #[cfg(debug_assertions)]
621                                        drop(prev);
622
623                                        set_message.set(Some(val));
624                                    }
625                                    Err(err) => {
626                                        on_error(CodecError::Decode(err).into());
627                                    }
628                                }
629                            },
630                        );
631                    })
632                        as Box<dyn FnMut(MessageEvent)>);
633                    web_socket.set_onmessage(Some(onmessage_closure.as_ref().unchecked_ref()));
634                    onmessage_closure.forget();
635                }
636
637                // onerror handler
638                {
639                    let unmounted = Arc::clone(&unmounted);
640                    let on_error = Arc::clone(&on_error);
641
642                    let onerror_closure = Closure::wrap(Box::new(move |e: Event| {
643                        if unmounted.load(std::sync::atomic::Ordering::Relaxed)
644                            || connection_id_ref.get_value() != connection_id
645                        {
646                            return;
647                        }
648
649                        stop_heartbeat();
650
651                        #[cfg(debug_assertions)]
652                        let zone = leptos::reactive::diagnostics::SpecialNonReactiveZone::enter();
653
654                        on_error(UseWebSocketError::Event(e));
655
656                        #[cfg(debug_assertions)]
657                        drop(zone);
658
659                        set_ready_state.set(ConnectionReadyState::Closed);
660
661                        // try to reconnect
662                        if let Some(reconnect) = &reconnect_ref.get_value() {
663                            reconnect();
664                        }
665                    })
666                        as Box<dyn FnMut(Event)>);
667                    web_socket.set_onerror(Some(onerror_closure.as_ref().unchecked_ref()));
668                    onerror_closure.forget();
669                }
670
671                // onclose handler
672                {
673                    let unmounted = Arc::clone(&unmounted);
674                    let on_close = Arc::clone(&on_close);
675
676                    let onclose_closure = Closure::wrap(Box::new(move |e: CloseEvent| {
677                        if unmounted.load(std::sync::atomic::Ordering::Relaxed)
678                            || connection_id_ref.get_value() != connection_id
679                        {
680                            return;
681                        }
682
683                        stop_heartbeat();
684
685                        #[cfg(debug_assertions)]
686                        let zone = leptos::reactive::diagnostics::SpecialNonReactiveZone::enter();
687
688                        on_close(e);
689
690                        #[cfg(debug_assertions)]
691                        drop(zone);
692
693                        set_ready_state.set(ConnectionReadyState::Closed);
694
695                        // if closing was not intentional, try to reconnect
696                        if let Some(reconnect) = &reconnect_ref.get_value() {
697                            reconnect();
698                        }
699                    })
700                        as Box<dyn FnMut(CloseEvent)>);
701                    web_socket.set_onclose(Some(onclose_closure.as_ref().unchecked_ref()));
702                    onclose_closure.forget();
703                }
704
705                ws.set_value(Some(web_socket));
706            }))
707        });
708
709        // Open connection
710        open = sendwrap_fn!(move || {
711            reconnect_times_ref.set_value(0);
712            // Reopening on purpose also re-arms automatic reconnects, which
713            // `close()` had switched off.
714            manually_closed_ref.set_value(false);
715            if let Some(connect) = connect_ref.get_value() {
716                connect();
717            }
718        });
719
720        // Close connection
721        close = sendwrap_fn!(move || {
722            stop_heartbeat();
723            manually_closed_ref.set_value(true);
724            // A reconnect scheduled by an earlier failure would otherwise still
725            // fire and reopen the socket behind the caller's back. Dropping the
726            // handle is not enough, it has to be cleared.
727            reconnect_timer_ref.update_value(|timer| {
728                if let Some(timer) = timer.take() {
729                    timer.clear();
730                }
731            });
732            if let Some(web_socket) = ws.get_value() {
733                let _ = web_socket.close();
734            }
735        });
736
737        // Open connection (not called if option `manual` is true)
738        Effect::new({
739            let open = open.clone();
740            move |_| {
741                if immediate {
742                    open();
743                }
744            }
745        });
746
747        // clean up (unmount)
748        on_cleanup({
749            let close = close.clone();
750            move || {
751                unmounted.store(true, std::sync::atomic::Ordering::Relaxed);
752                close();
753            }
754        });
755    }
756
757    #[cfg(feature = "ssr")]
758    {
759        open = move || {};
760        close = move || {};
761        send = move |_: &Tx| {};
762
763        let _ = url;
764        let _ = on_open;
765        let _ = on_message;
766        let _ = on_message_raw;
767        let _ = on_message_raw_bytes;
768        let _ = on_error;
769        let _ = on_close;
770        let _ = reconnect_limit;
771        let _ = reconnect_interval;
772        let _ = immediate;
773        let _ = protocols;
774        let _ = heartbeat;
775        let _ = set_ready_state;
776        let _ = set_message;
777    }
778
779    UseWebSocketReturn {
780        ready_state: ready_state.into(),
781        message: message.into(),
782        open,
783        close,
784        send,
785        _marker: PhantomData,
786    }
787}
788
789#[cfg(not(feature = "ssr"))]
790fn send_with_codec<T, Codec>(
791    value: &T,
792    send_str: impl Fn(&str),
793    send_bytes: impl Fn(&[u8]),
794    on_error: impl Fn(HybridCoderError<<Codec as Encoder<T>>::Error>),
795) where
796    Codec: Encoder<T>,
797    Codec: HybridEncoder<T, <Codec as Encoder<T>>::Encoded, Error = <Codec as Encoder<T>>::Error>,
798{
799    if Codec::is_binary_encoder() {
800        match Codec::encode_bin(value) {
801            Ok(val) => send_bytes(&val),
802            Err(err) => on_error(err),
803        }
804    } else {
805        match Codec::encode_str(value) {
806            Ok(val) => send_str(&val),
807            Err(err) => on_error(err),
808        }
809    }
810}
811
812type ArcFnBytes = Arc<dyn Fn(&[u8]) + Send + Sync>;
813
814/// Options for [`use_websocket_with_options`].
815#[derive(DefaultBuilder)]
816pub struct UseWebSocketOptions<Rx, E, D, Hb, HbCodec>
817where
818    Rx: ?Sized,
819    Hb: Default + Send + Sync + 'static,
820    HbCodec: Encoder<Hb>,
821    HbCodec: HybridEncoder<
822            Hb,
823            <HbCodec as Encoder<Hb>>::Encoded,
824            Error = <HbCodec as Encoder<Hb>>::Error,
825        >,
826{
827    /// Heartbeat options
828    #[builder(skip)]
829    heartbeat: Option<HeartbeatOptions<Hb, HbCodec>>,
830    /// `WebSocket` connect callback.
831    on_open: Arc<dyn Fn(Event) + Send + Sync>,
832    /// `WebSocket` message callback for typed message decoded by codec.
833    #[builder(skip)]
834    on_message: Arc<dyn Fn(&Rx) + Send + Sync>,
835    /// `WebSocket` message callback for text.
836    on_message_raw: Arc<dyn Fn(&str) + Send + Sync>,
837    /// `WebSocket` message callback for binary.
838    on_message_raw_bytes: ArcFnBytes,
839    /// `WebSocket` error callback.
840    #[builder(skip)]
841    on_error: Arc<dyn Fn(UseWebSocketError<E, D>) + Send + Sync>,
842    /// `WebSocket` close callback.
843    on_close: Arc<dyn Fn(CloseEvent) + Send + Sync>,
844    /// Retry times. Defaults to `ReconnectLimit::Limited(3)`. Use `ReconnectLimit::Infinite` for
845    /// infinite retries.
846    reconnect_limit: ReconnectLimit,
847    /// Retry interval in ms. Defaults to 3000.
848    reconnect_interval: u64,
849    /// If `true` the `WebSocket` connection will immediately be opened when calling this function.
850    /// If `false` you have to manually call the `open` function.
851    /// Defaults to `true`.
852    immediate: bool,
853    /// Sub protocols. See [MDN Docs](https://developer.mozilla.org/en-US/docs/Web/API/WebSocket/WebSocket#protocols).
854    ///
855    /// Can be set as a signal to support protocols only available after the initial render.
856    ///
857    /// Note that protocols are only updated on the next websocket open() call, not whenever the signal is updated.
858    /// Therefore "lazy" protocols should use the `immediate(false)` option and manually call `open()`.
859    #[builder(into)]
860    protocols: Signal<Option<Vec<String>>>,
861}
862
863impl<Rx: ?Sized, E, D, Hb, HbCodec> UseWebSocketOptions<Rx, E, D, Hb, HbCodec>
864where
865    Hb: Default + Send + Sync + 'static,
866    HbCodec: Encoder<Hb>,
867    HbCodec: HybridEncoder<
868            Hb,
869            <HbCodec as Encoder<Hb>>::Encoded,
870            Error = <HbCodec as Encoder<Hb>>::Error,
871        >,
872{
873    /// `WebSocket` error callback.
874    pub fn on_error<F>(self, handler: F) -> Self
875    where
876        F: Fn(UseWebSocketError<E, D>) + Send + Sync + 'static,
877    {
878        Self {
879            on_error: Arc::new(handler),
880            ..self
881        }
882    }
883
884    /// `WebSocket` message callback for typed message decoded by codec.
885    pub fn on_message<F>(self, handler: F) -> Self
886    where
887        F: Fn(&Rx) + Send + Sync + 'static,
888    {
889        Self {
890            on_message: Arc::new(handler),
891            ..self
892        }
893    }
894
895    /// Set the data, codec and interval at which the heartbeat is sent. The heartbeat
896    /// is the default value of the `NewHb` type.
897    pub fn heartbeat<NewHb, NewHbCodec>(
898        self,
899        interval: u64,
900    ) -> UseWebSocketOptions<Rx, E, D, NewHb, NewHbCodec>
901    where
902        NewHb: Default + Send + Sync + 'static,
903        NewHbCodec: Encoder<NewHb>,
904        NewHbCodec: HybridEncoder<
905                NewHb,
906                <NewHbCodec as Encoder<NewHb>>::Encoded,
907                Error = <NewHbCodec as Encoder<NewHb>>::Error,
908            >,
909    {
910        UseWebSocketOptions {
911            heartbeat: Some(HeartbeatOptions {
912                data: PhantomData::<NewHb>,
913                interval,
914                codec: PhantomData::<NewHbCodec>,
915            }),
916            on_open: self.on_open,
917            on_message: self.on_message,
918            on_message_raw: self.on_message_raw,
919            on_message_raw_bytes: self.on_message_raw_bytes,
920            on_close: self.on_close,
921            on_error: self.on_error,
922            reconnect_limit: self.reconnect_limit,
923            reconnect_interval: self.reconnect_interval,
924            immediate: self.immediate,
925            protocols: self.protocols,
926        }
927    }
928}
929
930impl<Rx: ?Sized, E, D> Default for UseWebSocketOptions<Rx, E, D, (), DummyEncoder> {
931    fn default() -> Self {
932        Self {
933            heartbeat: None,
934            on_open: Arc::new(|_| {}),
935            on_message: Arc::new(|_| {}),
936            on_message_raw: Arc::new(|_| {}),
937            on_message_raw_bytes: Arc::new(|_| {}),
938            on_error: Arc::new(|_| {}),
939            on_close: Arc::new(|_| {}),
940            reconnect_limit: ReconnectLimit::default(),
941            reconnect_interval: 3000,
942            immediate: true,
943            protocols: Default::default(),
944        }
945    }
946}
947
948pub struct DummyEncoder;
949
950impl Encoder<()> for DummyEncoder {
951    type Encoded = String;
952    type Error = ();
953
954    fn encode(_: &()) -> Result<Self::Encoded, Self::Error> {
955        Ok("".to_string())
956    }
957}
958
959/// Options for heartbeats
960#[cfg_attr(feature = "ssr", allow(dead_code))]
961pub struct HeartbeatOptions<Hb, HbCodec>
962where
963    Hb: Default + Send + Sync + 'static,
964    HbCodec: Encoder<Hb>,
965    HbCodec: HybridEncoder<
966            Hb,
967            <HbCodec as Encoder<Hb>>::Encoded,
968            Error = <HbCodec as Encoder<Hb>>::Error,
969        >,
970{
971    /// Heartbeat data that will be sent to the server
972    data: PhantomData<Hb>,
973    /// Heartbeat interval in ms. A heartbeat will be sent every `interval` ms.
974    interval: u64,
975    /// Codec used to encode the heartbeat data
976    codec: PhantomData<HbCodec>,
977}
978
979impl<Hb, HbCodec> Clone for HeartbeatOptions<Hb, HbCodec>
980where
981    Hb: Default + Send + Sync + 'static,
982    HbCodec: Encoder<Hb>,
983    HbCodec: HybridEncoder<
984            Hb,
985            <HbCodec as Encoder<Hb>>::Encoded,
986            Error = <HbCodec as Encoder<Hb>>::Error,
987        >,
988{
989    fn clone(&self) -> Self {
990        *self
991    }
992}
993
994impl<Hb, HbCodec> Copy for HeartbeatOptions<Hb, HbCodec>
995where
996    Hb: Default + Send + Sync + 'static,
997    HbCodec: Encoder<Hb>,
998    HbCodec: HybridEncoder<
999            Hb,
1000            <HbCodec as Encoder<Hb>>::Encoded,
1001            Error = <HbCodec as Encoder<Hb>>::Error,
1002        >,
1003{
1004}
1005
1006/// Return type of [`use_websocket`].
1007#[derive(Clone)]
1008pub struct UseWebSocketReturn<Tx, Rx, OpenFn, CloseFn, SendFn>
1009where
1010    Tx: Send + Sync + 'static,
1011    Rx: Send + Sync + 'static,
1012    OpenFn: Fn() + Clone + Send + Sync + 'static,
1013    CloseFn: Fn() + Clone + Send + Sync + 'static,
1014    SendFn: Fn(&Tx) + Clone + Send + Sync + 'static,
1015{
1016    /// The current state of the `WebSocket` connection.
1017    pub ready_state: Signal<ConnectionReadyState>,
1018    /// Latest message received from `WebSocket`.
1019    pub message: Signal<Option<Rx>>,
1020    /// Opens the `WebSocket` connection
1021    pub open: OpenFn,
1022    /// Closes the `WebSocket` connection
1023    pub close: CloseFn,
1024    /// Sends data through the socket
1025    pub send: SendFn,
1026
1027    _marker: PhantomData<Tx>,
1028}
1029
1030#[derive(Error, Debug)]
1031pub enum UseWebSocketError<E, D> {
1032    #[error("WebSocket error event")]
1033    Event(Event),
1034    #[error("WebSocket codec error: {0}")]
1035    Codec(#[from] CodecError<E, D>),
1036    #[error("WebSocket heartbeat codec error: {0}")]
1037    HeartbeatCodec(String),
1038}
1039
1040fn normalize_url(url: &str) -> String {
1041    cfg_if! { if #[cfg(feature = "ssr")] {
1042        url.to_string()
1043    } else {
1044        if url.starts_with("ws://") || url.starts_with("wss://") {
1045            url.to_string()
1046        } else if url.starts_with("//") {
1047            format!("{}{}", detect_protocol(), url)
1048        } else if url.starts_with('/') {
1049            format!(
1050                "{}//{}{}",
1051                detect_protocol(),
1052                window().location().host().expect("Host not found"),
1053                url
1054            )
1055        } else {
1056            let mut path = window().location().pathname().expect("Pathname not found");
1057            if !path.ends_with('/') {
1058                path.push('/')
1059            }
1060            format!(
1061                "{}//{}{}{}",
1062                detect_protocol(),
1063                window().location().host().expect("Host not found"),
1064                path,
1065                url
1066            )
1067        }
1068    }}
1069}
1070
1071#[cfg_attr(feature = "ssr", allow(dead_code))]
1072fn detect_protocol() -> String {
1073    cfg_if! { if #[cfg(feature = "ssr")] {
1074        "ws".to_string()
1075    } else {
1076        window().location().protocol().expect("Protocol not found").replace("http", "ws")
1077    }}
1078}