use crate::core::{Error, ErrorKind, MessageId, Result};
use crate::engine::{Engine, EngineConfig, EnginePoll, ReceiveReport, SendFailedEvent};
const HELLO_MESSAGE: &[u8] = &[0];
const DEFAULT_PING_INTERVAL_MS: u64 = 5_000;
#[derive(Clone, Copy, Debug, Default, Eq, PartialEq)]
pub enum PeerState {
#[default]
Disconnected,
Connecting,
Connected,
}
#[derive(Clone, Copy, Debug, Eq, PartialEq)]
#[allow(clippy::large_enum_variant)]
pub enum EndpointPoll<'a> {
Transmit {
bytes: &'a [u8],
attempts: u8,
},
Message(crate::engine::MessageEvent),
SendFailed(crate::engine::SendFailedEvent),
Idle,
}
#[derive(Clone, Debug, Eq, PartialEq)]
pub struct PeerSlot {
config: EngineConfig,
engine: Option<Engine>,
state: PeerState,
last_seen_ms: u64,
last_ping_ms: u64,
pending_ping: bool,
}
impl PeerSlot {
#[must_use]
pub const fn new(config: EngineConfig) -> Self {
Self {
config,
engine: None,
state: PeerState::Disconnected,
last_seen_ms: 0,
last_ping_ms: 0,
pending_ping: false,
}
}
#[must_use]
pub const fn state(&self) -> PeerState {
self.state
}
#[must_use]
pub const fn is_connected(&self) -> bool {
matches!(self.state, PeerState::Connected)
}
#[must_use]
pub const fn has_session(&self) -> bool {
self.engine.is_some()
}
#[must_use]
pub const fn last_seen_ms(&self) -> u64 {
self.last_seen_ms
}
pub fn connect(&mut self, now_ms: u64) -> Result<MessageId> {
self.engine = Some(Engine::new(self.config));
self.state = PeerState::Connecting;
self.last_seen_ms = now_ms;
self.last_ping_ms = now_ms;
self.pending_ping = false;
let Some(engine) = self.engine.as_mut() else {
return Err(Error::new(ErrorKind::Engine));
};
engine.send(HELLO_MESSAGE)
}
pub fn accept_passive(&mut self, now_ms: u64) {
self.engine = Some(Engine::new(self.config));
self.state = PeerState::Connecting;
self.last_seen_ms = now_ms;
self.last_ping_ms = now_ms;
self.pending_ping = false;
}
pub fn disconnect(&mut self) {
self.engine = None;
self.state = PeerState::Disconnected;
self.last_seen_ms = 0;
self.last_ping_ms = 0;
self.pending_ping = false;
}
pub fn engine_or_connect(&mut self, now_ms: u64) -> Result<&mut Engine> {
if self.engine.is_none() {
self.connect(now_ms)?;
}
self.last_seen_ms = now_ms;
let Some(engine) = self.engine.as_mut() else {
return Err(Error::new(ErrorKind::Engine));
};
Ok(engine)
}
pub fn engine_or_accept_passive(&mut self, now_ms: u64) -> Result<&mut Engine> {
if self.engine.is_none() {
self.accept_passive(now_ms);
}
self.last_seen_ms = now_ms;
let Some(engine) = self.engine.as_mut() else {
return Err(Error::new(ErrorKind::Engine));
};
Ok(engine)
}
pub fn engine_mut(&mut self) -> Option<&mut Engine> {
self.engine.as_mut()
}
pub fn send(&mut self, message: &[u8]) -> Result<Option<MessageId>> {
let Some(engine) = self.engine.as_mut() else {
return Ok(None);
};
engine.send(message).map(Some)
}
pub fn receive(&mut self, now_ms: u64, bytes: &[u8]) -> ReceiveReport {
let Some(engine) = self.engine.as_mut() else {
return ReceiveReport::Incomplete { needed: None };
};
let report = engine.receive(bytes);
if matches!(
report,
ReceiveReport::Packet { .. }
| ReceiveReport::Duplicate { .. }
| ReceiveReport::Ack { .. }
| ReceiveReport::Ping
| ReceiveReport::Pong
) {
self.state = PeerState::Connected;
self.last_seen_ms = now_ms;
}
if matches!(report, ReceiveReport::Pong) {
self.pending_ping = false;
}
report
}
pub fn poll<'a>(&mut self, now_ms: u64, tx_buf: &'a mut [u8]) -> Result<EndpointPoll<'a>> {
self.queue_ping_if_idle(now_ms)?;
let Some(engine) = self.engine.as_mut() else {
return Ok(EndpointPoll::Idle);
};
match engine.poll(now_ms, tx_buf)? {
EnginePoll::Transmit { bytes, attempts } => {
Ok(EndpointPoll::Transmit { bytes, attempts })
}
EnginePoll::Message(message) => {
self.state = PeerState::Connected;
self.last_seen_ms = now_ms;
Ok(EndpointPoll::Message(message))
}
EnginePoll::SendFailed(event) => {
self.disconnect_after_send_failed(event);
Ok(EndpointPoll::SendFailed(event))
}
EnginePoll::Idle => Ok(EndpointPoll::Idle),
}
}
pub fn disconnect_if_idle(&mut self, now_ms: u64, timeout_ms: u64) -> bool {
if self.engine.is_some() && now_ms.saturating_sub(self.last_seen_ms) >= timeout_ms {
self.disconnect();
return true;
}
false
}
fn queue_ping_if_idle(&mut self, now_ms: u64) -> Result<()> {
if !self.is_connected()
|| self.pending_ping
|| now_ms.saturating_sub(self.last_seen_ms) < DEFAULT_PING_INTERVAL_MS
|| now_ms.saturating_sub(self.last_ping_ms) < DEFAULT_PING_INTERVAL_MS
{
return Ok(());
}
let Some(engine) = self.engine.as_mut() else {
return Ok(());
};
engine.state.send_ping(&engine.config)?;
self.last_ping_ms = now_ms;
self.pending_ping = true;
Ok(())
}
fn disconnect_after_send_failed(&mut self, _event: SendFailedEvent) {
self.disconnect();
}
}
impl Default for PeerSlot {
fn default() -> Self {
Self::new(EngineConfig::default())
}
}
#[cfg(test)]
mod tests {
use super::PeerSlot;
#[test]
fn peer_slot_creates_and_drops_engine() {
let mut peer = PeerSlot::default();
assert!(!peer.has_session());
assert!(!peer.is_connected());
peer.engine_or_connect(10).unwrap();
assert!(peer.has_session());
assert_eq!(peer.state(), super::PeerState::Connecting);
assert_eq!(peer.last_seen_ms(), 10);
assert!(!peer.disconnect_if_idle(19, 10));
assert!(peer.has_session());
assert!(peer.disconnect_if_idle(20, 10));
assert!(!peer.has_session());
}
#[test]
fn reconnect_replaces_engine_state() {
let mut peer = PeerSlot::default();
peer.engine_or_connect(1).unwrap().send(b"hello").unwrap();
peer.disconnect();
let engine = peer.engine_or_connect(2).unwrap();
assert_eq!(engine.send(b"hello").unwrap().get(), 1);
}
}