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}