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}