use crate::events::Event;
use crate::transport::{WsConnection, WsTransport};
const TARGET: &str = "wickra_exchange_core::wsutil";
pub(crate) fn reconnect_if_dropped(
ws: Option<&dyn WsTransport>,
url: &str,
connection: &mut Option<Box<dyn WsConnection>>,
subscribe_messages: &[String],
events: &mut Vec<Event>,
) {
let dropped = connection.as_ref().is_some_and(|c| !c.is_connected());
if !dropped || subscribe_messages.is_empty() {
return;
}
let subscriptions = subscribe_messages.len();
tracing::info!(target: TARGET, subscriptions, "stream closed by the peer; reconnecting");
events.push(Event::Disconnected);
*connection = None;
let Some(ws) = ws else {
tracing::warn!(target: TARGET, "no WebSocket transport is configured; the stream stays closed");
return;
};
let Ok(mut fresh) = ws.connect(url) else {
tracing::warn!(target: TARGET, "reconnect could not open a connection; the next poll retries");
return;
};
for (replayed, message) in subscribe_messages.iter().enumerate() {
if fresh.send(message).is_err() {
tracing::warn!(target: TARGET, replayed, subscriptions, "reconnected, but replaying the subscriptions failed; staying closed");
return; }
}
tracing::info!(target: TARGET, subscriptions, "reconnected and replayed every subscription");
*connection = Some(fresh);
events.push(Event::Reconnected);
}
#[cfg(test)]
mod tests {
use super::*;
use crate::transport::MockWsTransport;
#[test]
fn reopens_and_replays_subscribes_on_close() {
let ws = MockWsTransport::new();
ws.push_connection(vec![Ok(None)]); ws.push_connection(vec![]);
let mut connection = Some(ws.connect("wss://x").unwrap());
connection.as_mut().unwrap().recv().ok(); assert!(!connection.as_ref().unwrap().is_connected());
let mut events = Vec::new();
reconnect_if_dropped(
Some(&ws),
"wss://x",
&mut connection,
&["sub".to_string()],
&mut events,
);
assert_eq!(events, vec![Event::Disconnected, Event::Reconnected]);
assert!(connection.is_some());
assert_eq!(ws.connected_urls().len(), 2); assert_eq!(ws.sent(), vec!["sub".to_string()]); }
#[test]
fn soak_survives_many_reconnect_cycles() {
const CYCLES: usize = 200;
let ws = MockWsTransport::new();
for _ in 0..=CYCLES {
ws.push_connection(vec![Ok(None)]);
}
let mut connection = Some(ws.connect("wss://x").unwrap());
let subs = vec!["sub".to_string()];
for _ in 0..CYCLES {
connection.as_mut().unwrap().recv().ok(); let mut events = Vec::new();
reconnect_if_dropped(Some(&ws), "wss://x", &mut connection, &subs, &mut events);
assert_eq!(events, vec![Event::Disconnected, Event::Reconnected]);
assert!(connection.is_some());
}
assert_eq!(ws.connected_urls().len(), CYCLES + 1);
assert_eq!(ws.sent().len(), CYCLES);
}
#[test]
fn a_client_without_a_transport_reports_the_drop_and_stops() {
let ws = MockWsTransport::new();
ws.push_connection(vec![Ok(None)]);
let mut connection = Some(ws.connect("wss://x").unwrap());
connection.as_mut().unwrap().recv().ok();
let mut events = Vec::new();
reconnect_if_dropped(
None,
"wss://x",
&mut connection,
&["sub".into()],
&mut events,
);
assert_eq!(events, vec![Event::Disconnected]);
assert!(connection.is_none(), "no transport, so nothing to reopen");
}
#[test]
fn a_refused_reconnect_leaves_the_connection_closed() {
let ws = MockWsTransport::new();
ws.push_connection(vec![Ok(None)]);
ws.push_refused_connection();
let mut connection = Some(ws.connect("wss://x").unwrap());
connection.as_mut().unwrap().recv().ok();
let mut events = Vec::new();
reconnect_if_dropped(
Some(&ws),
"wss://x",
&mut connection,
&["sub".into()],
&mut events,
);
assert_eq!(events, vec![Event::Disconnected]);
assert!(connection.is_none(), "a failed reopen must not be kept");
assert_eq!(ws.connected_urls().len(), 2, "it did try");
}
#[test]
fn a_reconnect_that_cannot_resubscribe_is_not_reported_as_recovered() {
let ws = MockWsTransport::new();
ws.push_connection(vec![Ok(None)]);
ws.push_unsendable_connection();
let mut connection = Some(ws.connect("wss://x").unwrap());
connection.as_mut().unwrap().recv().ok();
let mut events = Vec::new();
reconnect_if_dropped(
Some(&ws),
"wss://x",
&mut connection,
&["sub-a".into(), "sub-b".into()],
&mut events,
);
assert_eq!(
events,
vec![Event::Disconnected],
"a socket subscribed to nothing is not a recovery"
);
assert!(connection.is_none());
assert!(ws.sent().is_empty(), "not one subscribe frame landed");
}
#[test]
fn live_connection_is_left_untouched() {
let ws = MockWsTransport::new();
let mut connection = Some(ws.connect("wss://x").unwrap()); let mut events = Vec::new();
reconnect_if_dropped(
Some(&ws),
"wss://x",
&mut connection,
&["sub".to_string()],
&mut events,
);
assert!(events.is_empty());
assert!(connection.is_some());
}
}