use axum::{
http::{HeaderMap, HeaderValue, header},
response::{
IntoResponse, Response,
sse::{Event, KeepAlive, Sse},
},
};
use bytes::Bytes;
use futures::{Stream, StreamExt as _, stream};
use serde_json::Value;
const BOUNDARY: &str = "-";
pub(super) struct Chunk {
pub payload: Value,
pub resume_id: Option<u64>,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub(super) enum Wire {
Sse,
Multipart,
}
pub(in super::super) fn negotiate(headers: &HeaderMap) -> Option<Wire> {
let accept = headers.get(header::ACCEPT).and_then(|v| v.to_str().ok())?;
let mut multipart = false;
for part in accept.split(',') {
let Some(media) = part.trim().split(';').next().map(str::trim) else {
continue;
};
if media.eq_ignore_ascii_case("text/event-stream") {
return Some(Wire::Sse);
}
if media.eq_ignore_ascii_case("multipart/mixed") {
multipart = true;
}
}
multipart.then_some(Wire::Multipart)
}
pub(super) fn respond<S>(wire: Wire, chunks: S) -> Response
where
S: Stream<Item = Chunk> + Send + 'static,
{
match wire {
Wire::Sse => sse_response(chunks),
Wire::Multipart => multipart_response(chunks),
}
}
fn sse_response<S>(chunks: S) -> Response
where
S: Stream<Item = Chunk> + Send + 'static,
{
let events = chunks
.map(|chunk| {
let event = Event::default().event("next").data(chunk.payload.to_string());
Ok::<_, std::convert::Infallible>(match chunk.resume_id {
Some(id) => event.id(id.to_string()),
None => event,
})
})
.chain(stream::once(async { Ok(Event::default().event("complete").data("")) }));
Sse::new(events)
.keep_alive(KeepAlive::new().interval(std::time::Duration::from_secs(15)).text(""))
.into_response()
}
fn multipart_response<S>(chunks: S) -> Response
where
S: Stream<Item = Chunk> + Send + 'static,
{
let body = chunks
.map(|chunk| {
let json = chunk.payload.to_string();
let part = format!(
"\r\n--{BOUNDARY}\r\ncontent-type: application/json; charset=utf-8\r\n\
content-length: {}\r\n\r\n{json}\r\n",
json.len()
);
Ok::<_, std::convert::Infallible>(Bytes::from(part))
})
.chain(stream::once(async { Ok(Bytes::from(format!("--{BOUNDARY}--\r\n"))) }));
(
[(
header::CONTENT_TYPE,
HeaderValue::from_static("multipart/mixed; boundary=\"-\"; deferSpec=20220824"),
)],
axum::body::Body::from_stream(body),
)
.into_response()
}