Skip to main content

musli_web/
tungstenite029.rs

1//! Client side implementation for [`tokio-tungstenite`] `0.29.x`.
2//!
3//! This allows non-browser clients to talk to the same websocket API which is
4//! served by [`ws::Server`], such as through the [`axum08`] integration.
5//!
6//! [`axum08`]: <https://docs.rs/musli-web/latest/musli_web/axum08/>
7//! [`tokio-tungstenite`]: <https://docs.rs/tokio-tungstenite/0.29>
8//! [`ws::Server`]: <https://docs.rs/musli-web/latest/musli_web/ws/struct.Server.html>
9//!
10//! # Examples
11//!
12//! ```no_run
13//! use musli_web::tungstenite029::prelude::*;
14//!
15//! mod api {
16//!     use musli::{Decode, Encode};
17//!     use musli_web::api;
18//!
19//!     #[derive(Encode, Decode)]
20//!     pub struct HelloRequest<'de> {
21//!         pub message: &'de str,
22//!     }
23//!
24//!     #[derive(Encode, Decode)]
25//!     pub struct HelloResponse<'de> {
26//!         pub message: &'de str,
27//!     }
28//!
29//!     api::define! {
30//!         pub type Hello;
31//!
32//!         impl Endpoint for Hello {
33//!             impl<'de> Request for HelloRequest<'de>;
34//!             type Response<'de> = HelloResponse<'de>;
35//!         }
36//!     }
37//! }
38//!
39//! # async fn example() -> Result<(), Box<dyn core::error::Error>> {
40//! let mut service = ws::connect("ws://localhost:3000/ws")
41//!     .on_error(|error| {
42//!         tracing::error!("WebSocket error: {error}");
43//!     })
44//!     .build();
45//!
46//! let handle = service.handle().clone();
47//!
48//! tokio::spawn(async move {
49//!     if let Err(error) = service.run().await {
50//!         tracing::error!("WebSocket service error: {error}");
51//!     }
52//! });
53//!
54//! handle.wait_until_open().await?;
55//!
56//! let packet = handle
57//!     .request()
58//!     .body(api::HelloRequest { message: "Hello!" })
59//!     .send()
60//!     .await?;
61//!
62//! let response = packet.decode()?;
63//! println!("Response: {}", response.message);
64//! # Ok(())
65//! # }
66//! ```
67
68use core::future::{Future, poll_fn};
69use core::pin::Pin;
70
71use bytes::Bytes;
72use futures_core03::Stream;
73use futures_sink03::Sink;
74use tokio::net::TcpStream;
75use tokio_tungstenite029::tungstenite::Error;
76use tokio_tungstenite029::tungstenite::protocol::Message as WsMessage;
77use tokio_tungstenite029::{MaybeTlsStream, WebSocketStream, connect_async};
78
79use crate::client::{ClientImpl, EmptyCallback, Message, ServiceBuilder, SocketImpl};
80
81/// The socket type used by this implementation.
82#[doc(hidden)]
83pub type Socket = WebSocketStream<MaybeTlsStream<TcpStream>>;
84
85pub mod prelude {
86    //! The public facing API for use with `tokio-tungstenite` `0.29.x`.
87
88    pub mod ws {
89        //! Organization module prefixing all exported items with `ws` for
90        //! convenient namespacing.
91
92        pub use crate::api::ChannelId;
93        pub use crate::client::{
94            Channel, EmptyCallback, Error, Handle, Listener, Packet, RawPacket, RequestBuilder,
95            State, StateListener,
96        };
97
98        use crate::tungstenite029::Tungstenite029Impl;
99
100        /// Implementation alias for [`connect`].
101        ///
102        /// [`connect`]: crate::tungstenite029::connect
103        #[inline]
104        pub fn connect(url: impl AsRef<str>) -> ServiceBuilder<EmptyCallback> {
105            crate::tungstenite029::connect(url)
106        }
107
108        /// Implementation alias for [`Service`].
109        ///
110        /// [`Service`]: crate::client::Service
111        pub type Service = crate::client::Service<Tungstenite029Impl>;
112
113        /// Implementation alias for [`ServiceBuilder`].
114        ///
115        /// [`ServiceBuilder`]: crate::client::ServiceBuilder
116        pub type ServiceBuilder<C> = crate::client::ServiceBuilder<Tungstenite029Impl, C>;
117    }
118}
119
120/// Client implementation for `tokio-tungstenite` `0.29.x`.
121///
122/// See [`connect()`].
123#[derive(Clone, Copy)]
124pub enum Tungstenite029Impl {}
125
126/// Construct a new [`ServiceBuilder`] which will connect to `url`.
127///
128/// Note that no connection is established until [`Service::run`] is called.
129///
130/// [`Service::run`]: crate::client::Service::run
131#[inline]
132pub fn connect(url: impl AsRef<str>) -> ServiceBuilder<Tungstenite029Impl, EmptyCallback> {
133    crate::client::connect(url)
134}
135
136impl crate::client::sealed_client::Sealed for Tungstenite029Impl {}
137
138impl ClientImpl for Tungstenite029Impl {
139    type Error = Error;
140    type Socket = Socket;
141
142    #[inline]
143    async fn connect(url: &str) -> Result<Self::Socket, Self::Error> {
144        let (socket, _) = connect_async(url).await?;
145        Ok(socket)
146    }
147}
148
149impl crate::client::sealed_socket::Sealed for Socket {}
150
151impl SocketImpl for Socket {
152    type Error = Error;
153
154    #[inline]
155    fn recv(&mut self) -> impl Future<Output = Option<Result<Message, Self::Error>>> + Send + '_ {
156        poll_fn(move |cx| {
157            Pin::new(&mut *self)
158                .poll_next(cx)
159                .map(|message| message.map(|message| message.map(convert)))
160        })
161    }
162
163    #[inline]
164    fn send(&mut self, data: &[u8]) -> impl Future<Output = Result<(), Self::Error>> + Send + '_ {
165        let message = WsMessage::Binary(Bytes::copy_from_slice(data));
166
167        async move {
168            poll_fn(|cx| Pin::new(&mut *self).poll_ready(cx)).await?;
169            Pin::new(&mut *self).start_send(message)?;
170            poll_fn(|cx| Pin::new(&mut *self).poll_flush(cx)).await
171        }
172    }
173
174    #[inline]
175    fn close(&mut self) -> impl Future<Output = Result<(), Self::Error>> + Send + '_ {
176        poll_fn(move |cx| Pin::new(&mut *self).poll_close(cx))
177    }
178}
179
180/// Convert a tungstenite message into a message understood by the client.
181#[inline]
182fn convert(message: WsMessage) -> Message {
183    match message {
184        WsMessage::Binary(data) => Message::Binary(data),
185        WsMessage::Ping(..) => Message::Ping,
186        WsMessage::Pong(..) => Message::Pong,
187        WsMessage::Close(..) => Message::Close,
188        // NB: Raw frames are never produced while reading, and text messages
189        // are not part of the protocol. Both are treated as a protocol
190        // violation which tears the connection down.
191        WsMessage::Text(..) | WsMessage::Frame(..) => Message::Text,
192    }
193}