use futures::{SinkExt, StreamExt};
use rig_http::http_client::{Error, Result};
use rig_http::wasm_compat::WasmBoxedFuture;
use rig_http::ws_client::{CloseFrame, Frame, WebSocketConnection};
use std::collections::VecDeque;
use tokio::net::TcpStream;
use tokio_tungstenite::tungstenite::protocol::CloseFrame as TungsteniteCloseFrame;
use tokio_tungstenite::{
MaybeTlsStream, WebSocketStream,
tungstenite::{self, Message},
};
pub(crate) type Socket = WebSocketStream<MaybeTlsStream<TcpStream>>;
pub(crate) struct DirectConnection(Socket);
impl DirectConnection {
pub(crate) fn new(socket: Socket) -> Self {
Self(socket)
}
}
impl WebSocketConnection for DirectConnection {
fn send(&mut self, frame: Frame) -> WasmBoxedFuture<'_, Result<()>> {
Box::pin(async move {
self.0
.send(into_message(frame))
.await
.map_err(crate::from_tungstenite)
})
}
fn recv(&mut self) -> WasmBoxedFuture<'_, Result<Option<Frame>>> {
Box::pin(async move {
loop {
match self.0.next().await {
Some(Ok(message)) => match from_message(message) {
Some(frame) => return Ok(Some(frame)),
None => continue,
},
Some(Err(error)) => return Err(crate::from_tungstenite(error)),
None => return Ok(None),
}
}
})
}
fn close(&mut self, frame: Option<CloseFrame>) -> WasmBoxedFuture<'_, Result<()>> {
Box::pin(async move {
self.0
.close(frame.map(into_close_frame))
.await
.map_err(crate::from_tungstenite)
})
}
}
enum Command {
Send(Frame, futures::channel::oneshot::Sender<Result<()>>),
Recv(futures::channel::oneshot::Sender<Result<Option<Frame>>>),
Close(
Option<CloseFrame>,
futures::channel::oneshot::Sender<Result<()>>,
),
}
pub(crate) struct ForwardedConnection {
commands: futures::channel::mpsc::Sender<Command>,
_actor: crate::runtime::OwnedTask<()>,
}
#[derive(Debug, thiserror::Error)]
#[error("the websocket connection task has stopped")]
struct ConnectionTaskGone;
const READ_AHEAD: usize = 256;
async fn run_actor(socket: Socket, mut requests: futures::channel::mpsc::Receiver<Command>) {
use futures::{FutureExt, select};
let (mut sink, mut stream) = socket.split();
let mut inbound: VecDeque<Result<Frame>> = VecDeque::new();
let mut pending_read: Option<futures::channel::oneshot::Sender<Result<Option<Frame>>>> = None;
let mut stream_ended = false;
loop {
match pending_read.take() {
Some(reply) if !inbound.is_empty() || stream_ended => {
let answer = match inbound.pop_front() {
Some(Ok(frame)) => Ok(Some(frame)),
Some(Err(error)) => Err(error),
None => Ok(None),
};
if let Err(answer) = reply.send(answer) {
match answer {
Ok(Some(frame)) => inbound.push_front(Ok(frame)),
Err(error) => inbound.push_front(Err(error)),
Ok(None) => {}
}
}
continue;
}
still_pending => pending_read = still_pending,
}
let command = if stream_ended || inbound.len() >= READ_AHEAD {
requests.next().await
} else {
select! {
command = requests.next().fuse() => command,
message = stream.next().fuse() => {
match message {
Some(Ok(message)) => {
if let Some(frame) = from_message(message) {
inbound.push_back(Ok(frame));
}
}
Some(Err(error)) => inbound.push_back(Err(crate::from_tungstenite(error))),
None => stream_ended = true,
}
continue;
}
}
};
let Some(command) = command else {
return;
};
match command {
Command::Send(frame, reply) => {
let result = sink
.send(into_message(frame))
.await
.map_err(crate::from_tungstenite);
let _ = reply.send(result);
}
Command::Recv(reply) => pending_read = Some(reply),
Command::Close(frame, reply) => {
let mut result = sink
.send(Message::Close(frame.map(into_close_frame)))
.await
.map_err(crate::from_tungstenite);
if let Err(error) = SinkExt::close(&mut sink).await {
result = result.and(Err(crate::from_tungstenite(error)));
}
let _ = reply.send(result);
return;
}
}
}
}
impl ForwardedConnection {
#[cfg(not(target_family = "wasm"))]
pub(crate) fn spawn(socket: Socket) -> Result<rig_http::ws_client::BoxedWebSocketConnection> {
let (commands, requests) = futures::channel::mpsc::channel::<Command>(1);
let actor = crate::runtime::spawn_off_runtime(run_actor(socket, requests))?;
Ok(Box::new(Self {
commands,
_actor: actor,
}))
}
async fn request<T, F>(&mut self, command: F) -> Result<T>
where
F: FnOnce(futures::channel::oneshot::Sender<Result<T>>) -> Command,
{
let (reply, answer) = futures::channel::oneshot::channel();
self.commands
.send(command(reply))
.await
.map_err(|_| Error::instance(ConnectionTaskGone))?;
answer
.await
.map_err(|_| Error::instance(ConnectionTaskGone))?
}
}
impl WebSocketConnection for ForwardedConnection {
fn send(&mut self, frame: Frame) -> WasmBoxedFuture<'_, Result<()>> {
Box::pin(async move { self.request(|reply| Command::Send(frame, reply)).await })
}
fn recv(&mut self) -> WasmBoxedFuture<'_, Result<Option<Frame>>> {
Box::pin(async move { self.request(Command::Recv).await })
}
fn close(&mut self, frame: Option<CloseFrame>) -> WasmBoxedFuture<'_, Result<()>> {
Box::pin(async move { self.request(|reply| Command::Close(frame, reply)).await })
}
}
fn into_message(frame: Frame) -> Message {
match frame {
Frame::Text(text) => Message::text(text),
Frame::Binary(bytes) => Message::binary(bytes),
Frame::Ping(bytes) => Message::Ping(bytes),
Frame::Pong(bytes) => Message::Pong(bytes),
Frame::Close(frame) => Message::Close(frame.map(into_close_frame)),
}
}
fn from_message(message: Message) -> Option<Frame> {
Some(match message {
Message::Text(text) => Frame::Text(text.to_string()),
Message::Binary(bytes) => Frame::Binary(bytes),
Message::Ping(bytes) => Frame::Ping(bytes),
Message::Pong(bytes) => Frame::Pong(bytes),
Message::Close(frame) => Frame::Close(frame.map(|frame| CloseFrame {
code: frame.code.into(),
reason: frame.reason.to_string(),
})),
Message::Frame(_) => return None,
})
}
fn into_close_frame(frame: CloseFrame) -> TungsteniteCloseFrame {
TungsteniteCloseFrame {
code: tungstenite::protocol::frame::coding::CloseCode::from(frame.code),
reason: frame.reason.into(),
}
}
#[cfg(test)]
mod tests;