use std::future::Future;
use std::sync::atomic::Ordering;
use std::sync::Arc;
use std::time::Duration;
use futures_util::future::{select, Either};
use gloo_timers::future::TimeoutFuture;
use spotproto::Packet;
use crate::api;
use crate::client::Inner;
use crate::error::{Error, Result};
use crate::identity;
use crate::transport_wasm::{self, Incoming};
const HANDSHAKE_TIMEOUT: Duration = Duration::from_secs(300);
const STEADY_READ_TIMEOUT: Duration = Duration::from_secs(120);
const CONN_RETRY_DELAY: Duration = Duration::from_secs(2);
const CONN_STAGGER: Duration = Duration::from_secs(2);
const CONN_HEALTHY_AFTER: i64 = 30;
const CONN_MAX_FAIL: u32 = 10;
const MAINTENANCE_TICK: Duration = Duration::from_secs(30);
pub async fn sleep(dur: Duration) {
TimeoutFuture::new(millis(dur)).await;
}
pub fn now_ms() -> f64 {
js_sys::Date::now()
}
pub async fn with_timeout<F: Future>(fut: F, dur: Duration) -> Option<F::Output> {
let timeout = TimeoutFuture::new(millis(dur));
futures_util::pin_mut!(fut);
futures_util::pin_mut!(timeout);
match select(fut, timeout).await {
Either::Left((out, _)) => Some(out),
Either::Right(((), _)) => None,
}
}
fn millis(dur: Duration) -> u32 {
dur.as_millis().min(u32::MAX as u128) as u32
}
pub async fn main_loop(inner: Arc<Inner>) {
inner.logf(format_args!("client entering main loop"));
run_connect(&inner).await;
while !inner.is_closed() {
sleep(MAINTENANCE_TICK).await;
if inner.is_closed() {
break;
}
let cnt = inner.conn_cnt.load(Ordering::Relaxed);
if cnt < inner.min_conn.load(Ordering::Relaxed) {
run_connect(&inner).await;
}
}
}
async fn run_connect(inner: &Arc<Inner>) {
let (mut hosts, mut min_conn) = match api::get_hosts().await {
Ok(v) => v,
Err(e) => {
inner.logf(format_args!("failed to fetch host list: {e}"));
return;
}
};
hosts.truncate(10);
if min_conn == 0 || min_conn > hosts.len() as u32 {
min_conn = hosts.len() as u32;
}
inner.min_conn.store(min_conn, Ordering::Relaxed);
inner.wake_online_waiters();
for host in hosts {
if inner.is_closed() {
break;
}
let registered = inner.hosts.lock().unwrap().insert(host.clone());
if !registered {
continue;
}
inner.logf(format_args!("connecting to host: {host}"));
let inner = inner.clone();
wasm_bindgen_futures::spawn_local(async move { conn_task(inner, host).await });
sleep(CONN_STAGGER).await;
}
}
async fn conn_task(inner: Arc<Inner>, host: String) {
inner.conn_cnt.fetch_add(1, Ordering::Relaxed);
let mut fails: u32 = 0;
while !inner.is_closed() {
match transport_wasm::connect(&host, "/_websocket").await {
Err(e) => {
inner.logf(format_args!("failed to connect to server: {e}"));
fails += 1;
if fails > CONN_MAX_FAIL {
break;
}
sleep(CONN_RETRY_DELAY).await;
}
Ok((sink, stream)) => {
let start = identity::now_unix();
let (online, res) = handle(&inner, sink, stream).await;
if let Err(e) = res {
inner.logf(format_args!("error during communications with server: {e}"));
}
if online && identity::now_unix() - start >= CONN_HEALTHY_AFTER {
fails = 0;
sleep(CONN_RETRY_DELAY).await;
continue;
}
fails += 1;
if fails > CONN_MAX_FAIL {
break;
}
sleep(CONN_RETRY_DELAY).await;
}
}
}
inner.hosts.lock().unwrap().remove(&host);
inner.conn_cnt.fetch_sub(1, Ordering::Relaxed);
}
async fn handle(
inner: &Arc<Inner>,
mut sink: rsurl::aio::WsSink,
mut stream: rsurl::aio::WsStream,
) -> (bool, Result<()>) {
if let Err(e) = handshake(inner, &mut stream, &mut sink).await {
return (false, Err(e));
}
inner.online_incr();
inner.set_sink(sink);
let res = read_loop(inner, &mut stream).await;
inner.drop_sink();
inner.online_decr();
(true, res)
}
async fn handshake(
inner: &Arc<Inner>,
stream: &mut rsurl::aio::WsStream,
sink: &mut rsurl::aio::WsSink,
) -> Result<()> {
loop {
let data = match with_timeout(transport_wasm::recv_packet(stream), HANDSHAKE_TIMEOUT).await
{
Some(Ok(Incoming::Packet(data))) => data,
Some(Ok(Incoming::Closed)) => {
return Err(Error::Ws("connection closed during handshake".into()))
}
Some(Err(e)) => return Err(e),
None => return Err(Error::Ws("handshake timed out".into())),
};
match spotproto::parse(&data, true)? {
Packet::HandshakeRequest(req) => {
if req.ready {
inner.logf(format_args!(
"authentication done, connected as c.{}",
req.client_id
));
return Ok(());
}
let buf = build_handshake_response(inner, &req)?;
sink.send_binary(&buf)
.map_err(|e| Error::Ws(e.to_string()))?;
}
other => {
inner.logf(format_args!("unsupported handshake packet type {other:?}"));
}
}
}
}
fn build_handshake_response(
inner: &Arc<Inner>,
req: &spotproto::HandshakeRequest,
) -> Result<Vec<u8>> {
if let Some(groups) = &req.groups {
if let Err(e) = inner.handle_groups(groups) {
inner.logf(format_args!("failed to update groups: {e}"));
}
}
let mut res = req.respond(inner.signer())?;
res.id = inner.id_bin();
Ok(Packet::HandshakeResponse(res).encode()?)
}
async fn read_loop(inner: &Arc<Inner>, stream: &mut rsurl::aio::WsStream) -> Result<()> {
loop {
let data =
match with_timeout(transport_wasm::recv_packet(stream), STEADY_READ_TIMEOUT).await {
Some(Ok(Incoming::Packet(data))) => data,
Some(Ok(Incoming::Closed)) => return Ok(()), Some(Err(e)) => return Err(e),
None => {
if inner.is_closed() {
return Ok(());
}
continue;
}
};
match spotproto::parse(&data, true)? {
Packet::HandshakeRequest(req) => {
if req.ready {
continue;
}
let buf = build_handshake_response(inner, &req)?;
inner.send_raw(&buf)?;
}
Packet::Message(msg) => inner.route_message(msg),
other => {
inner.logf(format_args!("unsupported packet type {other:?}"));
}
}
if inner.is_closed() {
return Ok(());
}
}
}