use std::convert::Infallible;
use std::fmt::{Display, Formatter};
use std::pin::Pin;
use std::task::{Context, Poll};
use axum::body::{Body, Bytes, HttpBody};
use axum::http::{header, HeaderValue, Response};
use axum::response::IntoResponse;
use futures::Stream;
use http_body::Frame;
use serde::Serialize;
#[allow(clippy::struct_field_names, dead_code)]
pub struct Event<T> {
pub event_type: Option<String>,
pub id: Option<String>,
pub data: T,
}
impl<T> Event<T> {
pub fn from_data(data: T) -> Event<T> {
Self {
event_type: None,
id: None,
data,
}
}
}
impl<T: Serialize> Display for Event<T> {
fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result {
if let Some(event_type) = self.event_type.as_deref() {
writeln!(f, "event: {event_type}")?;
}
if let Some(id) = self.event_type.as_deref() {
writeln!(f, "id: {id}")?;
}
let data = serde_json::to_string(&self.data).map_err(|_| std::fmt::Error)?;
writeln!(f, "data: {data}\n")
}
}
pub struct ServerSentEventStream<S>(S);
impl<S, T> ServerSentEventStream<S>
where
S: Send + Stream<Item = Event<T>>,
T: Serialize + Send + Unpin,
{
pub fn new(stream: S) -> ServerSentEventStream<S> {
ServerSentEventStream(stream)
}
}
impl<S, T> HttpBody for ServerSentEventStream<S>
where
S: Send + Stream<Item = Event<T>> + Unpin,
T: Serialize + Send + Unpin,
{
type Data = Bytes;
type Error = Infallible;
fn poll_frame(self: Pin<&mut Self>, cx: &mut Context<'_>) -> Poll<Option<Result<Frame<Self::Data>, Self::Error>>> {
let inner = Pin::new(&mut self.get_mut().0);
match inner.poll_next(cx) {
Poll::Ready(Some(event)) => {
let data = event.to_string().into_bytes();
Poll::Ready(Some(Ok(Frame::data(Bytes::from(data)))))
}
Poll::Ready(None) => Poll::Ready(None),
Poll::Pending => Poll::Pending,
}
}
}
impl<S, T> IntoResponse for ServerSentEventStream<S>
where
S: Send + Stream<Item = Event<T>> + Unpin + 'static,
T: Serialize + Send + Unpin,
{
fn into_response(self) -> axum::response::Response {
let mut response = Response::new(Body::new(self));
response
.headers_mut()
.append(header::CONTENT_TYPE, HeaderValue::from_static("text/event-stream"));
response
.headers_mut()
.append(header::CACHE_CONTROL, HeaderValue::from_static("no-store"));
response
}
}