pub(crate) const CONNECTING: u16 = 0;
pub(crate) const OPEN: u16 = 1;
pub(crate) const CLOSING: u16 = 2;
pub(crate) const CLOSED: u16 = 3;
pub(crate) trait ProxySocket {
fn ready_state(&self) -> u16;
}
pub(crate) fn is_usable(ready_state: u16) -> bool {
ready_state == CONNECTING || ready_state == OPEN
}
pub(crate) fn needs_connect<S: ProxySocket>(socket: &mut Option<S>) -> bool {
if let Some(s) = socket.as_ref()
&& !is_usable(s.ready_state())
{
*socket = None;
}
socket.is_none()
}
pub(crate) fn store_attempt<S: ProxySocket, E>(
socket: &mut Option<S>,
attempt: Result<S, E>,
) -> Result<(), E> {
match attempt {
Ok(s) => {
*socket = Some(s);
Ok(())
}
Err(e) => {
*socket = None;
Err(e)
}
}
}
#[cfg(test)]
mod tests {
use super::{
CLOSED, CLOSING, CONNECTING, OPEN, ProxySocket, is_usable, needs_connect, store_attempt,
};
struct FakeSocket {
ready_state: u16,
}
impl ProxySocket for FakeSocket {
fn ready_state(&self) -> u16 {
self.ready_state
}
}
fn socket(ready_state: u16) -> Option<FakeSocket> {
Some(FakeSocket { ready_state })
}
#[wasm_lite::wasm_lite_test]
fn connecting_and_open_are_usable() {
assert!(
is_usable(CONNECTING),
"a pending handshake may still succeed"
);
assert!(is_usable(OPEN));
assert!(!is_usable(CLOSING));
assert!(!is_usable(CLOSED));
}
#[wasm_lite::wasm_lite_test]
fn an_empty_slot_needs_a_connection() {
let mut s: Option<FakeSocket> = None;
assert!(needs_connect(&mut s));
}
#[wasm_lite::wasm_lite_test]
fn a_live_socket_is_left_alone() {
for state in [CONNECTING, OPEN] {
let mut s = socket(state);
assert!(!needs_connect(&mut s), "state {state} should be kept");
assert!(s.is_some(), "state {state} should not be discarded");
}
}
#[wasm_lite::wasm_lite_test]
fn a_dead_socket_is_discarded_and_triggers_a_connect() {
for state in [CLOSING, CLOSED] {
let mut s = socket(state);
assert!(
needs_connect(&mut s),
"state {state} should force a connect"
);
assert!(s.is_none(), "state {state} should be cleared");
}
}
#[wasm_lite::wasm_lite_test]
fn a_failed_attempt_is_not_cached() {
let mut s: Option<FakeSocket> = None;
let outcome = store_attempt(&mut s, Err::<FakeSocket, _>("connect refused"));
assert_eq!(outcome, Err("connect refused"), "the error reaches the log");
assert!(s.is_none(), "a failure must not occupy the slot");
assert!(
needs_connect(&mut s),
"the next tick must retry, not conclude it is connected"
);
}
#[wasm_lite::wasm_lite_test]
fn a_failed_attempt_clears_whatever_was_there() {
let mut s = socket(OPEN);
let _ = store_attempt(&mut s, Err::<FakeSocket, _>("refused"));
assert!(
s.is_none(),
"a failed attempt must empty the slot, not leave a stale socket"
);
}
#[wasm_lite::wasm_lite_test]
fn a_successful_attempt_is_stored() {
let mut s: Option<FakeSocket> = None;
let outcome =
store_attempt::<FakeSocket, &str>(&mut s, Ok(FakeSocket { ready_state: OPEN }));
assert!(outcome.is_ok());
assert!(!needs_connect(&mut s), "a live socket needs no reconnect");
}
#[wasm_lite::wasm_lite_test]
fn the_loop_recovers_from_a_failure_then_a_drop() {
let mut s: Option<FakeSocket> = None;
let mut attempts = 0;
for tick in 0..3 {
if needs_connect(&mut s) {
attempts += 1;
let attempt = if tick < 2 {
Err("refused")
} else {
Ok(FakeSocket { ready_state: OPEN })
};
let _ = store_attempt(&mut s, attempt);
}
}
assert_eq!(attempts, 3, "each failure must be retried");
assert!(s.is_some(), "the third attempt connected");
s = socket(CLOSED);
assert!(needs_connect(&mut s), "a dropped connection is retried");
let _ = store_attempt(&mut s, Ok::<_, &str>(FakeSocket { ready_state: OPEN }));
assert!(
!needs_connect(&mut s),
"and the loop settles once reconnected"
);
}
}