use axum::body::{Body, Bytes};
use axum::http::{header, StatusCode};
use axum::response::{IntoResponse, Response};
use serde::Serialize;
use std::time::Duration;
use tokio::sync::broadcast;
const HEARTBEAT: Duration = Duration::from_secs(15);
#[derive(Clone, Copy)]
pub(crate) struct Framing {
pub prologue: &'static str,
pub heartbeat: &'static str,
pub content_type: &'static str,
pub no_buffering: bool,
}
pub(crate) const RETRY_AND_PING: Framing = Framing {
prologue: "retry: 2000\n\n",
heartbeat: ": ping\n\n",
content_type: "text/event-stream",
no_buffering: false,
};
pub(crate) const CHAT_TURN: Framing = Framing {
prologue: "retry: 2000\n\n",
heartbeat: "",
content_type: "text/event-stream",
no_buffering: false,
};
pub(crate) const CONNECTED_AND_KEEPALIVE: Framing = Framing {
prologue: ": connected\n\n",
heartbeat: ": keepalive\n\n",
content_type: "text/event-stream; charset=utf-8",
no_buffering: true,
};
pub(crate) struct Frame<S> {
pub event: Option<String>,
pub payload: S,
}
pub(crate) fn named<S>(event: &str, payload: S) -> Frame<S> {
Frame {
event: Some(event.to_string()),
payload,
}
}
pub(crate) fn unnamed<S>(payload: S) -> Frame<S> {
Frame {
event: None,
payload,
}
}
fn render<S: Serialize>(frame: &Frame<S>) -> Option<Bytes> {
let data = serde_json::to_string(&frame.payload).ok()?;
Some(Bytes::from(match &frame.event {
Some(event) => format!("event: {event}\ndata: {data}\n\n"),
None => format!("data: {data}\n\n"),
}))
}
pub(crate) struct Sink {
tx: tokio::sync::mpsc::Sender<Bytes>,
}
impl Sink {
pub(crate) async fn send<S: Serialize>(&self, frame: Frame<S>) -> bool {
match render(&frame) {
Some(chunk) => self.tx.send(chunk).await.is_ok(),
None => true,
}
}
}
pub(crate) fn driven<F, Fut>(framing: Framing, run: F) -> Response
where
F: FnOnce(Sink) -> Fut + Send + 'static,
Fut: std::future::Future<Output = ()> + Send,
{
let (tx, rx) = tokio::sync::mpsc::channel::<Bytes>(64);
tokio::spawn(async move {
if tx.send(Bytes::from(framing.prologue)).await.is_err() {
return;
}
let work = run(Sink { tx: tx.clone() });
let mut work = std::pin::pin!(work);
if framing.heartbeat.is_empty() {
work.await;
return;
}
let mut heartbeat = tokio::time::interval(HEARTBEAT);
heartbeat.tick().await;
loop {
tokio::select! {
_ = &mut work => return,
_ = heartbeat.tick() => {
if tx.send(Bytes::from(framing.heartbeat)).await.is_err() {
return;
}
}
}
}
});
respond(framing, rx)
}
pub(crate) fn stream<T, S, W>(
framing: Framing,
replay: Vec<Frame<S>>,
live: broadcast::Sender<T>,
wire: W,
) -> Response
where
T: Clone + Send + 'static,
W: Fn(T) -> Option<Frame<S>> + Send + 'static,
S: Serialize + Send + 'static,
{
let (tx, rx) = tokio::sync::mpsc::channel::<Bytes>(64);
tokio::spawn(async move {
let mut received = live.subscribe();
let mut prologue = vec![Bytes::from(framing.prologue)];
prologue.extend(replay.iter().filter_map(render));
for chunk in prologue {
if tx.send(chunk).await.is_err() {
return;
}
}
let mut heartbeat = tokio::time::interval(HEARTBEAT);
heartbeat.tick().await;
loop {
let chunk = tokio::select! {
value = received.recv() => match value {
Ok(value) => match wire(value).as_ref().and_then(render) {
Some(chunk) => chunk,
None => continue,
},
Err(_) => continue,
},
_ = heartbeat.tick() => Bytes::from(framing.heartbeat),
};
if tx.send(chunk).await.is_err() {
return;
}
}
});
respond(framing, rx)
}
fn respond(framing: Framing, rx: tokio::sync::mpsc::Receiver<Bytes>) -> Response {
let body = Body::from_stream(futures_util::stream::unfold(rx, |mut rx| async move {
rx.recv()
.await
.map(|chunk| (Ok::<Bytes, std::convert::Infallible>(chunk), rx))
}));
let mut response = Response::builder()
.status(StatusCode::OK)
.header(header::CONTENT_TYPE, framing.content_type)
.header(header::CACHE_CONTROL, "no-cache, no-transform")
.header(header::CONNECTION, "keep-alive");
if framing.no_buffering {
response = response.header("x-accel-buffering", "no");
}
response
.body(body)
.unwrap_or_else(|_| StatusCode::INTERNAL_SERVER_ERROR.into_response())
}