use std::{
pin::Pin,
task::{Context, Poll},
time::Duration,
};
use futures_util::{Stream, StreamExt};
use polyoxide_ws::{connect, tungstenite::Message, Connection, Correlated, Mode, WsError};
use super::{
request::{no_answer, request, Answer},
SocketError, StreamSet, MAX_STREAMS_PER_CONNECTION,
};
pub(crate) struct Bare<S: StreamSet> {
connection: Connection,
path: S::Path,
streams: Vec<S::Name>,
client_pings: bool,
}
impl<S: StreamSet> Unpin for Bare<S> {}
impl<S: StreamSet> Bare<S> {
pub(crate) async fn open(
base_url: &str,
path: S::Path,
connect_timeout: Duration,
answer_timeout: Duration,
) -> Result<Self, S::Error> {
let socket = connect(S::url(base_url, path), connect_timeout).await?;
let connection = Connection::new(socket, Mode::Data)
.min_request_interval(S::MIN_REQUEST_INTERVAL)
.answer_timeout(answer_timeout);
Ok(Self {
connection,
path,
streams: Vec::new(),
client_pings: polyoxide_ws::default_socket_transport()
.capabilities()
.client_pings,
})
}
pub(crate) async fn subscribe(&mut self, streams: &[S::Name]) -> Result<(), S::Error> {
let mut new: Vec<S::Name> = Vec::new();
for stream in streams {
if !self.streams.contains(stream) && !new.contains(stream) {
new.push(stream.clone());
}
}
if self.streams.len() + new.len() > MAX_STREAMS_PER_CONNECTION {
return Err(<S::Error as SocketError>::too_many_streams(
self.path,
MAX_STREAMS_PER_CONNECTION,
));
}
for batch in new.chunks(S::MAX_NAMES_PER_REQUEST) {
request::<S::Name, S::Error>(&mut self.connection, "SUBSCRIBE", Some(batch)).await?;
self.streams.extend(batch.iter().cloned());
}
Ok(())
}
pub(crate) async fn unsubscribe(&mut self, streams: &[S::Name]) -> Result<(), S::Error> {
let mut gone: Vec<S::Name> = Vec::new();
for stream in streams {
if self.streams.contains(stream) && !gone.contains(stream) {
gone.push(stream.clone());
}
}
for batch in gone.chunks(S::MAX_NAMES_PER_REQUEST) {
request::<S::Name, S::Error>(&mut self.connection, "UNSUBSCRIBE", Some(batch)).await?;
self.streams.retain(|s| !batch.contains(s));
}
Ok(())
}
pub(crate) async fn list_subscriptions(&mut self) -> Result<Vec<String>, S::Error> {
let id = self.connection.next_id();
let result =
request::<S::Name, S::Error>(&mut self.connection, "LIST_SUBSCRIPTIONS", None).await?;
serde_json::from_value(result.clone())
.map_err(|_| <S::Error as SocketError>::response(id, result.to_string()))
}
pub(crate) async fn ping(&mut self) -> Result<Duration, S::Error> {
if !self.client_pings {
return Err(<S::Error>::from(WsError::from(
polyoxide_ws::tungstenite::Error::Io(std::io::Error::new(
std::io::ErrorKind::Unsupported,
polyoxide_ws::PING_UNSUPPORTED,
)),
)));
}
let runtime = std::sync::Arc::clone(self.connection.runtime());
let start = runtime.now();
self.send_ping().await?;
self.connection
.await_answer(|message| match message {
Message::Pong(_) => Correlated::Answer(()),
_ => Correlated::Push,
})
.await
.map_err(|err| no_answer::<S::Error>(err, 0))?;
Ok(runtime.now().saturating_duration_since(start))
}
#[cfg(test)]
#[cfg_attr(not(feature = "usdm-ws"), allow(dead_code))]
pub(crate) fn set_client_pings(&mut self, client_pings: bool) {
self.client_pings = client_pings;
}
pub(crate) async fn close(&mut self) -> Result<(), S::Error> {
self.connection.close().await?;
Ok(())
}
pub(crate) fn streams(&self) -> &[S::Name] {
&self.streams
}
#[cfg_attr(not(feature = "usdm-ws"), allow(dead_code))]
pub(crate) fn path(&self) -> S::Path {
self.path
}
pub(crate) fn connection(&self) -> &Connection {
&self.connection
}
async fn send_ping(&mut self) -> Result<(), S::Error> {
self.connection
.send(Message::Ping(Vec::new().into()))
.await?;
Ok(())
}
#[cfg(test)]
pub(crate) fn close_frame(&self) -> Option<(Option<u16>, String)> {
let closed = self.connection.closed()?;
Some(match closed.kind() {
polyoxide_ws::Kind::Closed { code, reason } => (code, reason.to_owned()),
_ => (None, String::new()),
})
}
}
impl<S: StreamSet> Stream for Bare<S> {
type Item = Result<S::Update, S::Error>;
fn poll_next(mut self: Pin<&mut Self>, cx: &mut Context<'_>) -> Poll<Option<Self::Item>> {
loop {
let message = match self.connection.poll_next_unpin(cx) {
Poll::Ready(Some(Ok(message))) => message,
Poll::Ready(Some(Err(err))) => return Poll::Ready(Some(Err(err.into()))),
Poll::Ready(None) => return Poll::Ready(None),
Poll::Pending => return Poll::Pending,
};
let Message::Text(text) = message else {
continue;
};
match S::decode(&text) {
Ok(update) => return Poll::Ready(Some(Ok(update))),
Err(_) if Answer::parse(&text).is_some() => continue,
Err(err) => return Poll::Ready(Some(Err(err))),
}
}
}
}