use crate::config::TapConfig;
use crate::connection::TapConnection;
use crate::errors::TapError;
use crate::events::{TapEvent, extract_event_id};
use futures::Stream;
use std::pin::Pin;
use std::sync::Arc;
use std::task::{Context, Poll};
use std::time::Duration;
use tokio::sync::mpsc;
pub struct TapStream {
receiver: mpsc::Receiver<Result<Arc<TapEvent>, TapError>>,
close_sender: Option<mpsc::Sender<()>>,
closed: bool,
}
impl TapStream {
pub fn new(config: TapConfig) -> Self {
let (event_tx, event_rx) = mpsc::channel(config.channel_buffer_size);
let (close_tx, close_rx) = mpsc::channel(1);
tokio::spawn(connection_task(config, event_tx, close_rx));
Self {
receiver: event_rx,
close_sender: Some(close_tx),
closed: false,
}
}
pub async fn close(&mut self) {
if let Some(sender) = self.close_sender.take() {
let _ = sender.send(()).await;
}
self.closed = true;
}
pub fn is_closed(&self) -> bool {
self.closed
}
}
impl Stream for TapStream {
type Item = Result<Arc<TapEvent>, TapError>;
fn poll_next(mut self: Pin<&mut Self>, cx: &mut Context<'_>) -> Poll<Option<Self::Item>> {
if self.closed {
return Poll::Ready(None);
}
self.receiver.poll_recv(cx)
}
}
impl Drop for TapStream {
fn drop(&mut self) {
self.close_sender.take();
tracing::debug!("TapStream dropped");
}
}
async fn connection_task(
config: TapConfig,
event_tx: mpsc::Sender<Result<Arc<TapEvent>, TapError>>,
mut close_rx: mpsc::Receiver<()>,
) {
let mut current_reconnect_delay = config.initial_reconnect_delay;
let mut attempt: u32 = 0;
loop {
if close_rx.try_recv().is_ok() {
tracing::debug!("Connection task received close signal");
break;
}
tracing::debug!(attempt, hostname = %config.hostname, "Connecting to TAP service");
let conn_result = TapConnection::connect(&config).await;
match conn_result {
Ok(mut conn) => {
tracing::info!(hostname = %config.hostname, "TAP stream connected");
current_reconnect_delay = config.initial_reconnect_delay;
attempt = 0;
loop {
tokio::select! {
biased;
_ = close_rx.recv() => {
tracing::debug!("Connection task received close signal during receive");
let _ = conn.close().await;
return;
}
recv_result = conn.recv() => {
match recv_result {
Ok(Some(msg)) => {
match serde_json::from_str::<TapEvent>(&msg) {
Ok(event) => {
let event_id = event.id();
if config.send_acks
&& let Err(err) = conn.send_ack(event_id).await
{
tracing::warn!(error = %err, "Failed to send ack");
}
let event = Arc::new(event);
if event_tx.send(Ok(event)).await.is_err() {
tracing::debug!("Event receiver dropped, closing connection");
let _ = conn.close().await;
return;
}
}
Err(err) => {
tracing::warn!(error = %err, "Failed to parse TAP message");
if config.send_acks {
if let Some(event_id) = extract_event_id(&msg) {
tracing::debug!(event_id, "Extracted event ID via fallback parser");
if let Err(ack_err) = conn.send_ack(event_id).await {
tracing::warn!(error = %ack_err, "Failed to send ack for unparseable message");
}
} else {
tracing::warn!("Could not extract event ID from unparseable message");
}
}
if event_tx.send(Err(TapError::ParseError(err.to_string()))).await.is_err() {
tracing::debug!("Event receiver dropped, closing connection");
let _ = conn.close().await;
return;
}
}
}
}
Ok(None) => {
tracing::debug!("TAP connection closed by server");
break;
}
Err(err) => {
tracing::warn!(error = %err, "TAP connection error");
break;
}
}
}
}
}
}
Err(err) => {
tracing::warn!(error = %err, attempt, "Failed to connect to TAP service");
}
}
attempt += 1;
if let Some(max) = config.max_reconnect_attempts
&& attempt >= max
{
tracing::error!(attempts = attempt, "Max reconnection attempts exceeded");
let _ = event_tx
.send(Err(TapError::MaxReconnectAttemptsExceeded(attempt)))
.await;
break;
}
tracing::debug!(
delay_ms = current_reconnect_delay.as_millis(),
attempt,
"Waiting before reconnection"
);
tokio::select! {
_ = close_rx.recv() => {
tracing::debug!("Connection task received close signal during backoff");
return;
}
_ = tokio::time::sleep(current_reconnect_delay) => {
current_reconnect_delay = Duration::from_secs_f64(
(current_reconnect_delay.as_secs_f64() * config.reconnect_backoff_multiplier)
.min(config.max_reconnect_delay.as_secs_f64()),
);
}
}
}
tracing::debug!("Connection task exiting");
}
pub fn connect(config: TapConfig) -> TapStream {
TapStream::new(config)
}
pub fn connect_to(hostname: &str) -> TapStream {
TapStream::new(TapConfig::new(hostname))
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_stream_initial_state() {
}
#[tokio::test]
async fn test_stream_close() {
let mut stream = TapStream::new(TapConfig::new("localhost:9999"));
assert!(!stream.is_closed());
stream.close().await;
assert!(stream.is_closed());
}
#[test]
fn test_connect_functions() {
let _ = TapConfig::new("localhost:2480");
}
#[test]
fn test_reconnect_delay_calculation() {
let initial = Duration::from_secs(1);
let max = Duration::from_secs(10);
let multiplier = 2.0;
let mut delay = initial;
assert_eq!(delay, Duration::from_secs(1));
delay = Duration::from_secs_f64((delay.as_secs_f64() * multiplier).min(max.as_secs_f64()));
assert_eq!(delay, Duration::from_secs(2));
delay = Duration::from_secs_f64((delay.as_secs_f64() * multiplier).min(max.as_secs_f64()));
assert_eq!(delay, Duration::from_secs(4));
delay = Duration::from_secs_f64((delay.as_secs_f64() * multiplier).min(max.as_secs_f64()));
assert_eq!(delay, Duration::from_secs(8));
delay = Duration::from_secs_f64((delay.as_secs_f64() * multiplier).min(max.as_secs_f64()));
assert_eq!(delay, Duration::from_secs(10)); }
}