1#![cfg_attr(docsrs, feature(doc_cfg))]
2#![cfg_attr(
3 test,
4 allow(
5 clippy::expect_used,
6 clippy::indexing_slicing,
7 clippy::panic,
8 clippy::unwrap_used
9 )
10)]
11#[cfg(target_family = "wasm")]
25compile_error!(
26 "rig-tungstenite is a native websocket backend (tokio-tungstenite). On wasm, implement \
27 `rig_http::ws_client::WebSocketClientExt` over `web_sys::WebSocket` and open sessions with \
28 `connect_with(..)`."
29);
30
31#[cfg(not(target_family = "wasm"))]
32pub use tokio_tungstenite;
33
34#[cfg(not(target_family = "wasm"))]
35mod connection;
36
37#[cfg(not(target_family = "wasm"))]
38use connection::Connection;
39#[cfg(not(target_family = "wasm"))]
40use rig_http::http_client::{Error, NoBody, Request, Result};
41#[cfg(not(target_family = "wasm"))]
42use rig_http::ws_client::{BoxedWebSocketConnection, ConnectOptions, WebSocketClientExt};
43#[cfg(not(target_family = "wasm"))]
44use std::time::Duration;
45#[cfg(not(target_family = "wasm"))]
46use tokio_tungstenite::tungstenite::{self, client::IntoClientRequest};
47
48#[cfg(not(target_family = "wasm"))]
49#[derive(Clone, Copy, Debug, Default)]
52pub struct TungsteniteClient;
53
54#[cfg(not(target_family = "wasm"))]
55impl TungsteniteClient {
56 #[must_use]
58 pub fn new() -> Self {
59 Self
60 }
61}
62
63#[cfg(not(target_family = "wasm"))]
64impl WebSocketClientExt for TungsteniteClient {
65 async fn connect(
66 &self,
67 request: Request<NoBody>,
68 options: ConnectOptions,
69 ) -> Result<BoxedWebSocketConnection> {
70 let request = client_request(request)?;
71 let socket =
72 rig_http::runtime::bind(Box::pin(handshake(request, options.timeout)))?.await?;
73 Ok(Box::new(Connection(socket)))
74 }
75}
76
77#[cfg(not(target_family = "wasm"))]
78fn client_request(request: Request<NoBody>) -> Result<tungstenite::handshake::client::Request> {
81 let (parts, _) = request.into_parts();
82 let mut request = parts
83 .uri
84 .to_string()
85 .into_client_request()
86 .map_err(from_tungstenite)?;
87 for (name, value) in &parts.headers {
88 request.headers_mut().insert(name, value.clone());
89 }
90 Ok(request)
91}
92
93#[cfg(not(target_family = "wasm"))]
94#[derive(Debug, thiserror::Error)]
96#[error("timed out connecting the websocket after {0:?}")]
97struct ConnectTimeout(Duration);
98
99#[cfg(not(target_family = "wasm"))]
100async fn handshake(
101 request: tungstenite::handshake::client::Request,
102 timeout: Option<Duration>,
103) -> Result<connection::Socket> {
104 let connect = async {
105 tokio_tungstenite::connect_async(request)
106 .await
107 .map(|(socket, _)| socket)
108 .map_err(from_tungstenite)
109 };
110
111 let Some(timeout) = timeout else {
112 return connect.await;
113 };
114
115 match rig_http::wasm_compat::timeout(timeout, connect).await {
116 Ok(result) => result,
117 Err(_) => Err(Error::instance(ConnectTimeout(timeout))),
118 }
119}
120
121#[cfg(not(target_family = "wasm"))]
122fn from_tungstenite(error: tungstenite::Error) -> Error {
126 let tungstenite::Error::Http(response) = error else {
127 return Error::instance(error);
128 };
129
130 let (parts, body) = (*response).into_parts();
131 let body = body
132 .map(|body| String::from_utf8_lossy(&body).into_owned())
133 .unwrap_or_default();
134
135 Error::non_success_with_details(parts.status, parts.headers, body)
136}
137
138#[cfg(all(test, not(target_family = "wasm")))]
139mod tests;