use std::collections::HashMap;
use std::sync::Arc;
use std::time::Duration;
use anyhow::{anyhow, Result};
use futures_util::{SinkExt, StreamExt};
use tokio::sync::{broadcast, mpsc, oneshot, Mutex};
use tokio_tungstenite::tungstenite::Message;
pub const REQUEST_TIMEOUT: Duration = Duration::from_secs(30);
pub const EVENT_CHANNEL_CAPACITY: usize = 2048;
pub const CONNECT_TIMEOUT: Duration = Duration::from_secs(5);
pub trait Protocol: Send + Sync + 'static {
type ProtoError: Send + 'static;
type Event: Clone + Send + 'static;
fn encode_request(
id: u64,
method: &str,
params: serde_json::Value,
session_id: Option<&str>,
) -> Result<String>;
fn decode_frame(text: &str) -> Decoded<Self::ProtoError, Self::Event>;
fn closed_error() -> Self::ProtoError;
}
pub enum Decoded<E, Ev> {
Reply {
id: u64,
result: Result<serde_json::Value, E>,
},
Event(Ev),
Ignore,
}
type PendingMap<E> = HashMap<u64, oneshot::Sender<Result<serde_json::Value, E>>>;
pub struct WsRpc<P: Protocol> {
next_id: Mutex<u64>,
pending: Arc<Mutex<PendingMap<P::ProtoError>>>,
events_tx: broadcast::Sender<P::Event>,
write_tx: mpsc::UnboundedSender<String>,
reader_handle: Option<tokio::task::JoinHandle<()>>,
writer_handle: Option<tokio::task::JoinHandle<()>>,
}
impl<P: Protocol> WsRpc<P> {
pub async fn connect(ws_url: &str, label: &str) -> Result<Self> {
let (ws_stream, _) =
tokio::time::timeout(CONNECT_TIMEOUT, tokio_tungstenite::connect_async(ws_url))
.await
.map_err(|_| {
anyhow!(
"{label} WebSocket connect to {ws_url} timed out after {:?}",
CONNECT_TIMEOUT
)
})??;
let (mut ws_sink, mut ws_stream) = ws_stream.split();
let pending: Arc<Mutex<PendingMap<P::ProtoError>>> = Arc::new(Mutex::new(HashMap::new()));
let (events_tx, _) = broadcast::channel(EVENT_CHANNEL_CAPACITY);
let (write_tx, mut write_rx) = mpsc::unbounded_channel::<String>();
let writer_handle = tokio::spawn(async move {
while let Some(text) = write_rx.recv().await {
if ws_sink.send(Message::Text(text)).await.is_err() {
break;
}
}
let _ = ws_sink.close().await;
});
let pending_r = pending.clone();
let events_r = events_tx.clone();
let reader_handle = tokio::spawn(async move {
while let Some(msg) = ws_stream.next().await {
let text = match msg {
Ok(Message::Text(t)) => t,
Ok(Message::Binary(b)) => match String::from_utf8(b) {
Ok(s) => s,
Err(e) => {
tracing::debug!(
bytes = e.as_bytes().len(),
"dropping non-UTF-8 binary frame"
);
continue;
}
},
Ok(Message::Close(_)) | Err(_) => break,
Ok(_) => continue,
};
match P::decode_frame(&text) {
Decoded::Reply { id, result } => {
if let Some(tx) = pending_r.lock().await.remove(&id) {
let _ = tx.send(result);
}
}
Decoded::Event(ev) => {
let _ = events_r.send(ev);
}
Decoded::Ignore => {
tracing::debug!(frame = %truncate_frame(&text), "dropping undecodable/idless frame");
}
}
}
let mut p = pending_r.lock().await;
for (_, tx) in p.drain() {
let _ = tx.send(Err(P::closed_error()));
}
});
Ok(Self {
next_id: Mutex::new(1),
pending,
events_tx,
write_tx,
reader_handle: Some(reader_handle),
writer_handle: Some(writer_handle),
})
}
async fn next_id(&self) -> u64 {
let mut n = self.next_id.lock().await;
let id = *n;
*n += 1;
id
}
#[allow(clippy::result_large_err)]
pub async fn request(
&self,
method: &str,
params: serde_json::Value,
session_id: Option<&str>,
) -> std::result::Result<serde_json::Value, RequestError<P::ProtoError>> {
let id = self.next_id().await;
let text =
P::encode_request(id, method, params, session_id).map_err(RequestError::Transport)?;
let (tx, rx) = oneshot::channel();
self.pending.lock().await.insert(id, tx);
if self.write_tx.send(text).is_err() {
self.pending.lock().await.remove(&id);
return Err(RequestError::Transport(anyhow!("writer task closed")));
}
match tokio::time::timeout(REQUEST_TIMEOUT, rx).await {
Ok(Ok(Ok(v))) => Ok(v),
Ok(Ok(Err(e))) => Err(RequestError::Protocol(e)),
Ok(Err(_)) => Err(RequestError::Transport(anyhow!("response channel dropped"))),
Err(_) => {
self.pending.lock().await.remove(&id);
Err(RequestError::Timeout)
}
}
}
pub fn subscribe(&self) -> broadcast::Receiver<P::Event> {
self.events_tx.subscribe()
}
pub async fn close(mut self) {
let (write_tx, _) = mpsc::unbounded_channel::<String>();
let dead = std::mem::replace(&mut self.write_tx, write_tx);
drop(dead);
if let Some(h) = self.writer_handle.take() {
let _ = h.await;
}
if let Some(h) = self.reader_handle.take() {
h.abort();
let _ = h.await;
}
}
}
impl<P: Protocol> Drop for WsRpc<P> {
fn drop(&mut self) {
if let Some(h) = self.reader_handle.take() {
h.abort();
}
if let Some(h) = self.writer_handle.take() {
h.abort();
}
}
}
fn truncate_frame(text: &str) -> std::borrow::Cow<'_, str> {
const MAX: usize = 200;
if text.len() <= MAX {
std::borrow::Cow::Borrowed(text)
} else {
let end = text
.char_indices()
.take_while(|(i, _)| *i < MAX)
.last()
.map(|(i, c)| i + c.len_utf8())
.unwrap_or(0);
std::borrow::Cow::Owned(format!("{}… ({} bytes total)", &text[..end], text.len()))
}
}
pub enum RequestError<E> {
Protocol(E),
Timeout,
Transport(anyhow::Error),
}