use core::{
error::Error,
fmt::{Display, Formatter},
pin::Pin,
task::{Context, Poll, ready},
time::Duration,
};
use crate::event_stream::EventStream;
use bytes_utils::Str;
use futures_core::future::BoxFuture;
use futures_timer::Delay;
use http_body_util::BodyDataStream;
use pin_project_lite::pin_project;
use reqwest::{
Body, Error as ReqwestError, RequestBuilder, Response, StatusCode,
header::{ACCEPT, CONTENT_TYPE, HeaderName, HeaderValue},
};
use crate::{
constants::EMPTY_STR,
errors::{CantCloneError, EventStreamError},
event::Event,
response_to_stream,
retry::{DEFAULT_RETRY, ExponentialBackoff, RetryPolicy},
};
#[derive(Debug, Clone)]
pub enum StreamEvent {
Open,
Event(Event),
}
impl From<Event> for StreamEvent {
fn from(event: Event) -> Self {
StreamEvent::Event(event)
}
}
pin_project! {
#[project = EventSourceProjection]
#[derive(Debug)]
pub struct EventSource<R> {
builder: RequestBuilder,
#[pin]
connection_state: ConnectionState,
last_event_id: Str,
retry_policy: R
}
}
pin_project! {
#[project = ConnectionStateProjection]
enum ConnectionState {
Connecting {
#[pin]
future: BoxFuture<'static, Result<Response, ReqwestError>>,
retry_state: Option<(usize, Duration)>,
},
Retrying {
#[pin]
delay: Delay,
attempt_number: usize,
delay_duration: Duration,
},
Open {
#[pin]
stream: EventStream<BodyDataStream<Body>>,
retry_state: Option<(usize, Duration)>,
},
Closed,
}
}
impl std::fmt::Debug for ConnectionState {
fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result {
match self {
Self::Connecting { retry_state, .. } => f
.debug_struct("Connecting")
.field("future", &"future")
.field("retry_state", retry_state)
.finish(),
Self::Retrying {
delay,
attempt_number,
delay_duration,
} => f
.debug_struct("Retrying")
.field("delay", delay)
.field("attempt_number", attempt_number)
.field("delay_duration", delay_duration)
.finish(),
Self::Open {
stream,
retry_state,
} => f
.debug_struct("Open")
.field("stream", stream)
.field("retry_state", retry_state)
.finish(),
Self::Closed => write!(f, "Closed"),
}
}
}
impl<'pin, R> EventSourceProjection<'pin, R> {
fn initiate_connection(
&mut self,
retry_state: Option<(usize, Duration)>,
) -> Result<(), EventSourceErrorKind> {
let req = self.builder.try_clone().unwrap().header(
HeaderName::from_static("last-event-id"),
HeaderValue::from_str(self.last_event_id).map_err(|_| {
EventSourceErrorKind::InvalidLastEventId(self.last_event_id.clone())
})?,
);
let res_future = Box::pin(req.send());
*self.connection_state = ConnectionState::Connecting {
future: res_future,
retry_state,
};
Ok(())
}
fn handle_successful_response(
&mut self,
response: Response,
retry_state: Option<(usize, Duration)>,
) -> Result<(), EventSourceErrorKind> {
let status = response.status();
if !status.is_success() {
return Err(EventSourceErrorKind::InvalidStatusCode {
status,
response: Box::new(response),
});
}
if let Some(content_type) = response.headers().get(CONTENT_TYPE)
&& !content_type.as_bytes().starts_with(b"text/event-stream")
{
return Err(EventSourceErrorKind::InvalidContentType {
status,
content_type: content_type.clone(),
response: Box::new(response),
});
}
let stream = response_to_stream(response);
*self.connection_state = ConnectionState::Open {
stream,
retry_state,
};
Ok(())
}
fn start_retry(&mut self, attempt_number: usize, delay_duration: Duration) {
self.connection_state.set(ConnectionState::Retrying {
delay: Delay::new(delay_duration),
attempt_number,
delay_duration,
})
}
fn handle_error(&mut self, err: &EventSourceErrorKind, last_retry: Option<(usize, Duration)>)
where
R: RetryPolicy<EventSourceErrorKind>,
{
if let Some(retry_delay) = self.retry_policy.retry(err, last_retry) {
let retry_num = last_retry.map(|retry| retry.0).unwrap_or(1);
self.start_retry(retry_num, retry_delay);
} else {
self.connection_state.set(ConnectionState::Closed);
}
}
fn handle_event(&mut self, event: &Event)
where
R: RetryPolicy<EventSourceErrorKind>,
{
*self.last_event_id = event.id.clone();
if let Some(duration) = event.retry {
self.retry_policy.set_reconnection_time(duration)
}
}
}
impl<R> EventSource<R> {
pub fn new_with_retry(
request: RequestBuilder,
retry_policy: R,
) -> Result<Self, CantCloneError> {
let request = request.header(ACCEPT, HeaderValue::from_static("text/event-stream"));
let req_fut = Box::pin(request.try_clone().ok_or(CantCloneError)?.send());
Ok(EventSource {
builder: request,
connection_state: ConnectionState::Connecting {
future: req_fut,
retry_state: None,
},
last_event_id: EMPTY_STR,
retry_policy,
})
}
}
impl EventSource<ExponentialBackoff> {
pub fn new(request: RequestBuilder) -> Result<EventSource<ExponentialBackoff>, CantCloneError> {
let request = request.header(ACCEPT, HeaderValue::from_static("text/event-stream"));
let req_fut = Box::pin(request.try_clone().ok_or(CantCloneError)?.send());
Ok(EventSource {
builder: request,
connection_state: ConnectionState::Connecting {
future: req_fut,
retry_state: None,
},
last_event_id: EMPTY_STR,
retry_policy: DEFAULT_RETRY,
})
}
}
#[derive(Debug)]
enum EventSourceErrorKind {
InvalidLastEventId(Str),
Transport(ReqwestError),
Stream(EventStreamError<ReqwestError>),
InvalidStatusCode {
status: StatusCode,
response: Box<Response>, },
InvalidContentType {
status: StatusCode,
content_type: HeaderValue,
response: Box<Response>,
},
StreamEnded, }
impl Display for EventSourceErrorKind {
fn fmt(&self, f: &mut Formatter<'_>) -> core::fmt::Result {
match self {
EventSourceErrorKind::InvalidLastEventId(s) => s.fmt(f),
EventSourceErrorKind::Transport(err) => err.fmt(f),
EventSourceErrorKind::Stream(err) => err.fmt(f),
EventSourceErrorKind::InvalidStatusCode { status, .. } => write!(
f,
"got non 2XX status code {status}: '{canonical}'",
canonical = status.canonical_reason().unwrap_or("no canonical reason")
),
EventSourceErrorKind::InvalidContentType {
status,
content_type,
..
} => write!(
f,
"got invalid content-type '{content_type}' on status '{status}' request",
content_type = content_type
.to_str()
.unwrap_or("unable to read content-type as str")
),
EventSourceErrorKind::StreamEnded => "stream ended".fmt(f),
}
}
}
impl Error for EventSourceErrorKind {}
#[derive(Debug)]
pub struct EventSourceError {
retry_state: Option<(usize, Duration)>,
kind: EventSourceErrorKind,
}
impl Display for EventSourceError {
fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result {
let retry_state = self
.retry_state
.as_ref()
.map(|(attempts, duration)| format!("attempts: '{attempts}' duration: {duration:?}"))
.unwrap_or(String::from("no last retry"));
write!(
f,
"Error '{kind}' with last retry: {retry_state}",
kind = self.kind
)
}
}
impl std::error::Error for EventSourceError {}
impl EventSourceError {
fn new(
kind: impl Into<EventSourceErrorKind>,
retry_state: impl Into<Option<(usize, Duration)>>,
) -> Self {
Self {
retry_state: retry_state.into(),
kind: kind.into(),
}
}
pub fn is_status_code(&self) -> bool {
matches!(self.kind, EventSourceErrorKind::InvalidStatusCode { .. })
}
pub fn is_content_type(&self) -> bool {
matches!(self.kind, EventSourceErrorKind::InvalidContentType { .. })
}
pub fn is_response_err(&self) -> bool {
matches!(
self.kind,
EventSourceErrorKind::InvalidContentType { .. }
| EventSourceErrorKind::InvalidStatusCode { .. }
)
}
pub fn status_code(&self) -> Option<StatusCode> {
match &self.kind {
EventSourceErrorKind::InvalidStatusCode { status, .. }
| EventSourceErrorKind::InvalidContentType { status, .. } => Some(*status),
_ => None,
}
}
pub fn response(&self) -> Option<&Response> {
match &self.kind {
EventSourceErrorKind::InvalidStatusCode { response, .. }
| EventSourceErrorKind::InvalidContentType { response, .. } => Some(response),
_ => None,
}
}
pub fn into_response_err(self) -> Option<(Response, StatusCode, Option<HeaderValue>)> {
match self.kind {
EventSourceErrorKind::InvalidStatusCode { response, status } => {
Some((*response, status, None))
}
EventSourceErrorKind::InvalidContentType {
status,
content_type,
response,
} => Some((*response, status, Some(content_type))),
_ => None,
}
}
pub fn is_stream_ended(&self) -> bool {
matches!(self.kind, EventSourceErrorKind::StreamEnded)
}
}
impl From<ReqwestError> for EventSourceErrorKind {
fn from(value: ReqwestError) -> Self {
Self::Transport(value)
}
}
impl From<EventStreamError<ReqwestError>> for EventSourceErrorKind {
fn from(value: EventStreamError<ReqwestError>) -> Self {
Self::Stream(value)
}
}
impl<R> futures_core::Stream for EventSource<R>
where
R: RetryPolicy<EventSourceErrorKind>,
{
type Item = Result<StreamEvent, EventSourceError>;
fn poll_next(self: Pin<&mut Self>, cx: &mut Context<'_>) -> Poll<Option<Self::Item>> {
let mut this = self.project();
loop {
match this.connection_state.as_mut().project() {
ConnectionStateProjection::Connecting {
future,
retry_state,
} => {
let retry_state = *retry_state;
match ready!(future.poll(cx)) {
Ok(response) => {
if let Err(kind) =
this.handle_successful_response(response, retry_state)
{
return Poll::Ready(Some(Err(EventSourceError::new(
kind,
retry_state,
))));
};
return Poll::Ready(Some(Ok(StreamEvent::Open)));
}
Err(err) => {
this.connection_state.set(ConnectionState::Closed);
return Poll::Ready(Some(Err(EventSourceError::new(err, retry_state))));
}
}
}
ConnectionStateProjection::Retrying {
delay,
attempt_number,
delay_duration,
} => {
ready!(delay.poll(cx));
let retry_state = Some((*attempt_number + 1, *delay_duration));
if let Err(err) = this.initiate_connection(retry_state) {
this.connection_state.set(ConnectionState::Closed);
return Poll::Ready(Some(Err(EventSourceError::new(err, retry_state))));
}
}
ConnectionStateProjection::Open {
stream,
retry_state,
} => {
let retry_state = *retry_state;
match ready!(stream.poll_next(cx)) {
Some(Ok(event)) => {
this.handle_event(&event);
return Poll::Ready(Some(Ok(event.into())));
}
Some(Err(err)) => {
let err_kind = err.into();
this.handle_error(&err_kind, retry_state);
return Poll::Ready(Some(Err(EventSourceError::new(
err_kind,
retry_state,
))));
}
None => {
let err_kind = EventSourceErrorKind::StreamEnded;
this.handle_error(&err_kind, retry_state);
return Poll::Ready(Some(Err(EventSourceError::new(
err_kind,
retry_state,
))));
}
}
}
ConnectionStateProjection::Closed => return Poll::Ready(None),
}
}
}
}