use std::{
io,
pin::Pin,
sync::{
atomic::{AtomicBool, AtomicUsize, Ordering},
Arc,
},
task::{Context, Poll},
time::{Duration, Instant},
};
use futures_util::{SinkExt, StreamExt};
use polyoxide_ws::tungstenite::{http::Uri, Message};
use tokio::{
io::{AsyncRead, AsyncWrite, ReadBuf},
net::TcpStream,
};
const PERPS_WS_URL: &str = "wss://ws.perpetuals.polymarket.com/v1/ws";
const GIVE_UP_AFTER: Duration = Duration::from_secs(180);
const LINGER: Duration = Duration::from_secs(2);
struct Counting {
inner: TcpStream,
withhold: Arc<AtomicBool>,
withheld: Arc<AtomicUsize>,
after_close: Arc<AtomicBool>,
written: Arc<AtomicUsize>,
}
impl AsyncRead for Counting {
fn poll_read(
mut self: Pin<&mut Self>,
cx: &mut Context<'_>,
buf: &mut ReadBuf<'_>,
) -> Poll<io::Result<()>> {
Pin::new(&mut self.inner).poll_read(cx, buf)
}
}
impl AsyncWrite for Counting {
fn poll_write(
mut self: Pin<&mut Self>,
cx: &mut Context<'_>,
buf: &[u8],
) -> Poll<io::Result<usize>> {
if self.withhold.load(Ordering::SeqCst) && !self.after_close.load(Ordering::SeqCst) {
self.withheld.fetch_add(buf.len(), Ordering::SeqCst);
return Poll::Ready(Ok(buf.len()));
}
let written = Pin::new(&mut self.inner).poll_write(cx, buf);
if let Poll::Ready(Ok(n)) = written {
if self.after_close.load(Ordering::SeqCst) {
self.written.fetch_add(n, Ordering::SeqCst);
}
}
written
}
fn poll_flush(mut self: Pin<&mut Self>, cx: &mut Context<'_>) -> Poll<io::Result<()>> {
Pin::new(&mut self.inner).poll_flush(cx)
}
fn poll_shutdown(mut self: Pin<&mut Self>, cx: &mut Context<'_>) -> Poll<io::Result<()>> {
Pin::new(&mut self.inner).poll_shutdown(cx)
}
}
#[tokio::main]
async fn main() -> Result<(), Box<dyn std::error::Error>> {
let args: Vec<String> = std::env::args().skip(1).collect();
let flush = args.iter().any(|arg| arg == "--flush");
let url = args
.iter()
.find(|arg| arg.starts_with("ws"))
.map_or(PERPS_WS_URL, String::as_str);
polyoxide_ws::ensure_crypto_provider();
let uri: Uri = url.parse()?;
let host = uri.host().ok_or("the URL names no host")?;
let tls = uri.scheme_str() == Some("wss");
let port = uri.port_u16().unwrap_or(if tls { 443 } else { 80 });
let withhold = Arc::new(AtomicBool::new(false));
let withheld = Arc::new(AtomicUsize::new(0));
let after_close = Arc::new(AtomicBool::new(false));
let written = Arc::new(AtomicUsize::new(0));
let stream = Counting {
inner: TcpStream::connect((host, port)).await?,
withhold: Arc::clone(&withhold),
withheld: Arc::clone(&withheld),
after_close: Arc::clone(&after_close),
written: Arc::clone(&written),
};
let (mut ws, _response) = tokio_tungstenite::client_async_tls(url, stream).await?;
withhold.store(true, Ordering::SeqCst);
let opened = Instant::now();
let pattern = if flush {
"flush once after the close"
} else {
"return on the close, no flush"
};
println!("connected to {url}; pattern: {pattern}");
let ended = tokio::time::timeout(GIVE_UP_AFTER, async {
loop {
match ws.next().await {
Some(Ok(Message::Close(frame))) => {
after_close.store(true, Ordering::SeqCst);
return format!("a Close frame {frame:?}");
}
Some(Ok(other)) => println!("{:?}: {other:?}", opened.elapsed()),
Some(Err(err)) => return format!("a read error, no Close frame: {err}"),
None => return "the end of the stream, no Close frame".to_owned(),
}
}
})
.await;
let Ok(ended) = ended else {
println!("no close within {GIVE_UP_AFTER:?}");
return Ok(());
};
println!("after {:?} the server sent {ended}", opened.elapsed());
if flush {
let flushed = ws.flush().await;
println!("flush: {flushed:?}");
}
tokio::time::sleep(LINGER).await;
println!(
"bytes withheld before the close (pongs): {}",
withheld.load(Ordering::SeqCst)
);
println!(
"bytes written to the TCP socket after the close was read: {}",
written.load(Ordering::SeqCst)
);
Ok(())
}