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}