use crate::error::{IFlowError, Result};
use futures::{SinkExt, StreamExt};
use serde_json::Value;
use std::time::Duration;
use tokio_tungstenite::{WebSocketStream, connect_async, tungstenite::protocol::Message};
use tracing::debug;
use url::Url;
pub struct WebSocketTransport {
url: String,
websocket: Option<WebSocketStream<tokio_tungstenite::MaybeTlsStream<tokio::net::TcpStream>>>,
connected: bool,
timeout: f64,
}
impl WebSocketTransport {
pub fn new(url: String, timeout: f64) -> Self {
Self {
url,
websocket: None,
connected: false,
timeout,
}
}
pub async fn connect(&mut self) -> Result<()> {
if self.connected {
tracing::warn!("Already connected to {}", self.url);
return Ok(());
}
debug!("Connecting to {}", self.url);
let _url = Url::parse(&self.url)
.map_err(|e| IFlowError::Connection(format!("Invalid URL: {}", e)))?;
let (ws_stream, _) =
tokio::time::timeout(Duration::from_secs_f64(self.timeout), connect_async(&self.url))
.await
.map_err(|_| IFlowError::Timeout("Connection timeout".to_string()))?
.map_err(|e| {
IFlowError::Connection(format!("WebSocket connection failed: {}", e))
})?;
self.websocket = Some(ws_stream);
self.connected = true;
debug!("Connected to {}", self.url);
Ok(())
}
pub async fn send(&mut self, message: &Value) -> Result<()> {
if !self.connected {
return Err(IFlowError::NotConnected);
}
let ws_stream = self.websocket.as_mut().ok_or(IFlowError::NotConnected)?;
let data = serde_json::to_string(message).map_err(|e| IFlowError::JsonParse(e))?;
ws_stream
.send(Message::Text(data.clone().into()))
.await
.map_err(|e| IFlowError::Transport(format!("Failed to send message: {}", e)))?;
tracing::debug!(
"Sent message: {}", data
);
Ok(())
}
pub async fn send_raw(&mut self, message: &str) -> Result<()> {
if !self.connected {
return Err(IFlowError::NotConnected);
}
let ws_stream = self.websocket.as_mut().ok_or(IFlowError::NotConnected)?;
ws_stream
.send(Message::Text(message.to_string().into()))
.await
.map_err(|e| IFlowError::Transport(format!("Failed to send message: {}", e)))?;
tracing::debug!(
"Sent raw message: {}",
message.to_string()
);
Ok(())
}
pub async fn receive(&mut self) -> Result<String> {
if !self.connected {
return Err(IFlowError::NotConnected);
}
let ws_stream = self.websocket.as_mut().ok_or(IFlowError::NotConnected)?;
loop {
let msg = match ws_stream.next().await {
Some(Ok(msg)) => msg,
Some(Err(e)) => {
tracing::error!("WebSocket error: {}", e);
self.connected = false;
return Err(IFlowError::Transport(format!(
"Failed to receive message: {}",
e
)));
}
None => {
tracing::debug!("WebSocket connection closed");
self.connected = false;
return Err(IFlowError::Connection("Connection closed".to_string()));
}
};
match msg {
Message::Text(text) => {
let cleaned_text = text.trim_start_matches(|c: char| {
!c.is_ascii() || c.is_control() && c != '\n' && c != '\r' && c != '\t'
});
tracing::debug!(
"Received message: {}", cleaned_text.to_string()
);
return Ok(cleaned_text.to_string());
}
Message::Binary(data) => {
match String::from_utf8(data.to_vec()) {
Ok(text) => return Ok(text),
Err(_) => {
tracing::debug!("Received binary message, ignoring");
continue;
}
}
}
Message::Ping(data) => {
tracing::debug!("Received ping, sending pong");
if let Err(e) = ws_stream.send(Message::Pong(data)).await {
tracing::error!("Failed to send pong: {}", e);
self.connected = false;
return Err(IFlowError::Transport(format!("Failed to send pong: {}", e)));
}
continue;
}
Message::Pong(_) => {
tracing::debug!("Received pong");
continue;
}
Message::Close(close_frame) => {
tracing::debug!("Received close frame: {:?}", close_frame);
self.connected = false;
return Err(IFlowError::Connection(
"Connection closed by server".to_string(),
));
}
Message::Frame(_) => {
tracing::debug!("Received raw frame, ignoring");
continue;
}
}
}
}
pub async fn close(&mut self) -> Result<()> {
if let Some(mut ws_stream) = self.websocket.take() {
ws_stream
.close(None)
.await
.map_err(|e| IFlowError::Transport(format!("Error closing WebSocket: {}", e)))?;
debug!("WebSocket connection closed");
}
self.connected = false;
Ok(())
}
pub fn is_connected(&self) -> bool {
self.connected
}
pub fn url(&self) -> &str {
&self.url
}
}