Skip to main content

leptos_use_websocket/
use_websocket.rs

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