pub mod ui_message;
use std::pin::Pin;
use std::sync::Arc;
use std::task::{Context, Poll};
use std::time::Duration;
use axum::response::{IntoResponse, Response};
use bytes::Bytes;
use futures_core::Stream;
use futures_core::future::BoxFuture;
use futures_util::StreamExt;
use http::{HeaderName, HeaderValue};
use crate::ports::{ByteStream, Clock, HttpError, timeout};
use crate::stream::{BoxStream, ResponseStream};
pub const MAX_SSE_LINE_BYTES: usize = 1024 * 1024;
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct SseEvent {
pub event: Option<String>,
pub data: String,
pub id: Option<String>,
}
impl SseEvent {
#[must_use]
pub fn new(data: impl Into<String>) -> Self {
Self {
event: None,
data: data.into(),
id: None,
}
}
#[must_use]
pub fn encode(&self) -> Bytes {
let mut wire = String::new();
for (field, value) in [("event", &self.event), ("id", &self.id)] {
if let Some(value) = value {
wire.push_str(field);
wire.push_str(": ");
wire.extend(
value
.chars()
.filter(|character| !matches!(character, '\r' | '\n')),
);
wire.push('\n');
}
}
for line in split_data_lines(&self.data) {
wire.push_str("data: ");
wire.push_str(line);
wire.push('\n');
}
wire.push('\n');
Bytes::from(wire)
}
}
fn split_data_lines(data: &str) -> Vec<&str> {
let mut lines = Vec::new();
let mut rest = data;
while let Some(index) = rest.find(['\r', '\n']) {
lines.push(&rest[..index]);
let tail = &rest[index..];
rest = tail.strip_prefix("\r\n").unwrap_or(&tail[1..]);
}
lines.push(rest);
lines
}
#[derive(Default)]
pub struct SseDecoder {
buffer: Vec<u8>,
event: Option<String>,
data: Vec<String>,
id: Option<String>,
bom_checked: bool,
split_cr: bool,
overflowed: bool,
}
impl SseDecoder {
pub fn overflowed(&self) -> bool {
self.overflowed
}
pub fn push(&mut self, chunk: &[u8]) -> Vec<SseEvent> {
const BOM: &[u8] = b"\xEF\xBB\xBF";
if self.overflowed {
return Vec::new();
}
let mut chunk = chunk;
if self.split_cr {
self.split_cr = false;
if let Some(rest) = chunk.strip_prefix(b"\n".as_slice()) {
chunk = rest;
}
}
self.buffer.extend_from_slice(chunk);
if !self.bom_checked {
if self.buffer.starts_with(BOM) {
self.buffer.drain(..BOM.len());
self.bom_checked = true;
} else if !BOM.starts_with(&self.buffer) {
self.bom_checked = true;
}
}
let mut events = Vec::new();
while let Some(line) = self.next_line() {
if let Some(event) = self.field_line(&line) {
events.push(event);
}
}
if self.buffer.len() > MAX_SSE_LINE_BYTES {
self.overflowed = true;
self.buffer.clear();
}
events
}
pub fn finish(&mut self) -> Option<SseEvent> {
self.buffer.clear();
self.event = None;
self.data.clear();
self.id = None;
self.bom_checked = false;
self.split_cr = false;
self.overflowed = false;
None
}
fn next_line(&mut self) -> Option<String> {
let end = self
.buffer
.iter()
.position(|byte| matches!(byte, b'\n' | b'\r'))?;
let crlf = self.buffer[end] == b'\r' && self.buffer.get(end + 1) == Some(&b'\n');
let line = String::from_utf8_lossy(&self.buffer[..end]).into_owned();
self.split_cr = self.buffer[end] == b'\r' && end + 1 == self.buffer.len();
self.buffer.drain(..end + 1 + usize::from(crlf));
Some(line)
}
fn field_line(&mut self, line: &str) -> Option<SseEvent> {
if line.is_empty() {
return self.dispatch();
}
if line.starts_with(':') {
return None; }
let (name, value) = match line.split_once(':') {
Some((name, value)) => (name, value.strip_prefix(' ').unwrap_or(value)),
None => (line, ""),
};
match name {
"event" => self.event = Some(value.to_owned()),
"data" => self.data.push(value.to_owned()),
"id" if !value.contains('\0') => {
self.id = (!value.is_empty()).then(|| value.to_owned());
}
_ => {}
}
None
}
fn dispatch(&mut self) -> Option<SseEvent> {
if self.data.is_empty() {
self.event = None;
return None;
}
let event = SseEvent {
event: self.event.take(),
data: self.data.join("\n"),
id: self.id.clone(),
};
self.data.clear();
Some(event)
}
}
pub fn sse_events(body: ByteStream) -> BoxStream<'static, Result<SseEvent, HttpError>> {
Box::pin(futures_util::stream::unfold(
Pipeline {
body: Some(body),
decoder: SseDecoder::default(),
pending: Vec::new().into_iter(),
done: false,
},
|mut pipe| async move {
loop {
if let Some(event) = pipe.pending.next() {
return Some((Ok(event), pipe));
}
if pipe.done {
return None;
}
let Some(mut body) = pipe.body.take() else {
return None;
};
match body.next().await {
Some(Ok(chunk)) => {
pipe.body = Some(body);
pipe.pending = pipe.decoder.push(&chunk).into_iter();
if pipe.decoder.overflowed() {
pipe.done = true;
return Some((
Err(HttpError::ResponseTooLarge {
limit: MAX_SSE_LINE_BYTES,
}),
pipe,
));
}
}
Some(Err(err)) => {
pipe.done = true;
return Some((Err(err), pipe));
}
None => {
pipe.done = true;
return pipe.decoder.finish().map(|event| (Ok(event), pipe));
}
}
}
},
))
}
struct Pipeline {
body: Option<ByteStream>,
decoder: SseDecoder,
pending: std::vec::IntoIter<SseEvent>,
done: bool,
}
pub const HEARTBEAT_INTERVAL: Duration = Duration::from_secs(15);
pub struct SseResponse {
events: BoxStream<'static, SseEvent>,
clock: Arc<dyn Clock>,
every: Duration,
extra_headers: Vec<(HeaderName, HeaderValue)>,
}
impl SseResponse {
#[must_use]
pub fn new<S>(events: S, clock: Arc<dyn Clock>) -> Self
where
S: Stream<Item = SseEvent> + Send + 'static,
{
Self {
events: Box::pin(events),
clock,
every: HEARTBEAT_INTERVAL,
extra_headers: Vec::new(),
}
}
#[must_use]
pub fn heartbeat_every(mut self, every: Duration) -> Self {
self.every = every;
self
}
#[must_use]
pub fn header(mut self, name: HeaderName, value: HeaderValue) -> Self {
self.extra_headers.push((name, value));
self
}
}
impl IntoResponse for SseResponse {
fn into_response(self) -> Response {
let body = ResponseStream::new(
SseBody::new(self.events, self.clock, self.every)
.map(Ok::<Bytes, std::convert::Infallible>),
);
let mut response = body.into_response();
let headers = response.headers_mut();
headers.insert(
http::header::CONTENT_TYPE,
HeaderValue::from_static("text/event-stream"),
);
headers.insert(
http::header::CACHE_CONTROL,
HeaderValue::from_static("no-cache"),
);
headers.insert(
HeaderName::from_static("x-accel-buffering"),
HeaderValue::from_static("no"),
);
for (name, value) in self.extra_headers {
headers.insert(name, value);
}
response
}
}
struct SseBody {
source: BoxStream<'static, SseEvent>,
clock: Arc<dyn Clock>,
every: Duration,
sleep: Option<BoxFuture<'static, Option<()>>>,
}
impl SseBody {
fn new(source: BoxStream<'static, SseEvent>, clock: Arc<dyn Clock>, every: Duration) -> Self {
let mut body = Self {
source,
clock,
every,
sleep: None,
};
body.rearm();
body
}
fn rearm(&mut self) {
if self.every.is_zero() {
self.sleep = None;
return;
}
let clock = Arc::clone(&self.clock);
let every = self.every;
self.sleep = Some(Box::pin(async move {
timeout::<()>(clock.as_ref(), futures_util::future::pending(), every).await
}));
}
}
impl Stream for SseBody {
type Item = Bytes;
fn poll_next(self: Pin<&mut Self>, cx: &mut Context<'_>) -> Poll<Option<Bytes>> {
let this = self.get_mut();
match this.source.as_mut().poll_next(cx) {
Poll::Ready(Some(event)) => {
this.rearm();
return Poll::Ready(Some(event.encode()));
}
Poll::Ready(None) => return Poll::Ready(None),
Poll::Pending => {}
}
let Some(sleep) = this.sleep.as_mut() else {
return Poll::Pending;
};
if sleep.as_mut().poll(cx).is_ready() {
this.rearm();
Poll::Ready(Some(Bytes::from_static(b": heartbeat\n\n")))
} else {
Poll::Pending
}
}
}
#[cfg(test)]
mod tests {
use bytes::Bytes;
use super::*;
fn decode(chunks: &[&[u8]]) -> Vec<SseEvent> {
let mut decoder = SseDecoder::default();
let mut events = Vec::new();
for chunk in chunks {
events.extend(decoder.push(chunk));
}
events.extend(decoder.finish());
events
}
const SAMPLE: &[u8] = b": comment, ignored\r\n\
event: token\n\
id: 42\r\n\
data: first\n\
\n\
data:second\n\
data: padded\r\n\
\n\
: another comment\n\
data: \xF0\x9F\x8C\xB0\n\
\n";
fn sample_events() -> Vec<SseEvent> {
vec![
SseEvent {
event: Some("token".to_owned()),
data: "first".to_owned(),
id: Some("42".to_owned()),
},
SseEvent {
event: None,
data: "second\n padded".to_owned(),
id: Some("42".to_owned()),
},
SseEvent {
event: None,
data: "\u{1F330}".to_owned(),
id: Some("42".to_owned()),
},
]
}
#[test]
fn a_stream_decodes_into_its_events() {
assert_eq!(decode(&[SAMPLE]), sample_events());
}
#[test]
fn a_sample_split_at_every_byte_offset_decodes_the_same() {
for split in 0..=SAMPLE.len() {
let events = decode(&[&SAMPLE[..split], &SAMPLE[split..]]);
assert_eq!(events, sample_events(), "split at {split}");
}
}
#[test]
fn a_sample_dripped_byte_by_byte_decodes_the_same() {
let chunks: Vec<&[u8]> = SAMPLE.iter().map(std::slice::from_ref).collect();
assert_eq!(decode(&chunks), sample_events());
}
#[test]
fn lone_carriage_returns_break_lines_too() {
let events = decode(&[b"data:a\r\rdata:b\r\r"]);
assert_eq!(
events,
vec![
SseEvent {
event: None,
data: "a".to_owned(),
id: None,
},
SseEvent {
event: None,
data: "b".to_owned(),
id: None,
},
]
);
}
#[test]
fn a_crlf_pair_is_one_break_not_two() {
let events = decode(&[b"data:x\r\ndata:y\r\n\r\n"]);
assert_eq!(events.len(), 1, "one event, and the second line joined");
assert_eq!(events[0].data, "x\ny");
}
#[test]
fn an_event_without_data_lines_is_never_dispatched() {
let events = decode(&[b"event: ping\n\ndata: hi\n\n"]);
assert_eq!(
events,
vec![SseEvent {
event: None,
data: "hi".to_owned(),
id: None,
}]
);
}
#[test]
fn an_empty_data_line_still_dispatches_an_empty_event() {
let events = decode(&[b"data:\n\n"]);
assert_eq!(
events,
vec![SseEvent {
event: None,
data: String::new(),
id: None,
}]
);
}
#[test]
fn an_id_persists_and_an_empty_id_clears_it() {
let events = decode(&[b"id: 1\ndata: a\n\nid: 2\ndata: b\n\nid:\ndata: c\n\n"]);
let ids: Vec<Option<&str>> = events.iter().map(|event| event.id.as_deref()).collect();
assert_eq!(ids, vec![Some("1"), Some("2"), None], "{events:?}");
}
#[test]
fn an_id_carrying_a_nul_is_ignored_wholesale() {
let events = decode(&[b"id: a\0b\ndata: x\n\n"]);
assert_eq!(events[0].id, None, "{events:?}");
}
#[test]
fn a_leading_byte_order_mark_is_not_content() {
let events = decode(&[b"\xEF\xBB\xBFdata: hi\n\n"]);
assert_eq!(events[0].data, "hi");
let events = decode(&[b"\xEF\xBB", b"\xBFdata: hi\n\n"]);
assert_eq!(events[0].data, "hi");
}
#[test]
fn utf8_split_across_chunks_survives() {
let events = decode(&[b"data: \xF0\x9F", b"\x8C\xB0\n\n"]);
assert_eq!(events[0].data, "\u{1F330}");
}
#[test]
fn finish_discards_an_unterminated_event_and_resets() {
let mut decoder = SseDecoder::default();
assert!(decoder.push(b"data: half").is_empty());
assert_eq!(decoder.finish(), None, "no blank line, no event");
let events = decoder.push(b"data: fresh\n\n");
assert_eq!(events.len(), 1);
assert_eq!(events[0].data, "fresh");
assert_eq!(decoder.finish(), None);
}
#[test]
fn a_line_at_the_ceiling_is_an_event_a_line_past_it_overflows() {
let mut decoder = SseDecoder::default();
let at_cap = format!("data: {}\n\n", "a".repeat(MAX_SSE_LINE_BYTES - 6));
let events = decoder.push(at_cap.as_bytes());
assert_eq!(events.len(), 1);
assert_eq!(events[0].data.len(), MAX_SSE_LINE_BYTES - 6);
assert!(!decoder.overflowed(), "a line at the cap is a line");
let mut decoder = SseDecoder::default();
let over = format!("data: {}", "a".repeat(MAX_SSE_LINE_BYTES));
assert!(decoder.push(over.as_bytes()).is_empty());
assert!(decoder.overflowed());
assert!(
decoder.push(b"data: more\n\n").is_empty(),
"a fused decoder takes nothing more"
);
assert_eq!(decoder.finish(), None);
assert!(!decoder.overflowed(), "finish resets the latch");
let events = decoder.push(b"data: fresh\n\n");
assert_eq!(events.len(), 1, "the next body decodes from empty");
}
#[test]
fn complete_lines_never_trip_the_ceiling_however_large_the_chunk() {
let line = format!("data: {}\n\n", "a".repeat(MAX_SSE_LINE_BYTES - 6));
let mut body = String::new();
body.push_str(&line.repeat(3));
let mut decoder = SseDecoder::default();
let events = decoder.push(body.as_bytes());
assert_eq!(events.len(), 3);
assert!(!decoder.overflowed());
}
#[pollster::test]
async fn sse_events_refuses_a_line_past_the_ceiling_and_ends() {
let small = b"data: small\n\n".to_vec();
let oversized = format!("data: {}", "x".repeat(MAX_SSE_LINE_BYTES + 1));
let body: ByteStream = Box::pin(futures_util::stream::iter(vec![
Ok::<Bytes, HttpError>(Bytes::from(small)),
Ok(Bytes::from(oversized)),
]));
let items: Vec<Result<SseEvent, HttpError>> = sse_events(body).collect().await;
assert_eq!(
items.len(),
2,
"the small event, then the refusal, then the end: {items:?}"
);
assert_eq!(items[0].as_ref().expect("the small event").data, "small");
assert!(
matches!(
&items[1],
Err(HttpError::ResponseTooLarge { limit }) if *limit == MAX_SSE_LINE_BYTES
),
"the line ceiling is the limit: {:?}",
items[1]
);
}
#[pollster::test]
async fn sse_events_drains_a_body_into_events() {
let body: ByteStream = Box::pin(futures_util::stream::iter(vec![
Ok::<Bytes, HttpError>(Bytes::from_static(b"event: a\ndata: one")),
Ok(Bytes::from_static(b"\n\ndata: two\n\n")),
]));
let events: Vec<SseEvent> = sse_events(body)
.filter_map(|item| async move { item.ok() })
.collect()
.await;
assert_eq!(events.len(), 2);
assert_eq!(events[0].event.as_deref(), Some("a"));
assert_eq!(events[0].data, "one");
assert_eq!(events[1].data, "two");
}
#[pollster::test]
async fn sse_events_surfaces_a_body_error_and_ends() {
let body: ByteStream = Box::pin(futures_util::stream::iter(vec![
Ok::<Bytes, HttpError>(Bytes::from_static(b"data: one\n\n")),
Err(HttpError::Transport("socket dropped".to_owned())),
]));
let items: Vec<Result<SseEvent, HttpError>> = sse_events(body).collect().await;
assert_eq!(items.len(), 2, "the event, then the error, then the end");
assert_eq!(items[0].as_ref().unwrap().data, "one");
assert!(items[1].is_err(), "the transport error surfaces as-is");
}
fn round_trip(event: &SseEvent) -> Vec<SseEvent> {
let mut decoder = SseDecoder::default();
decoder.push(&event.encode())
}
#[test]
fn an_event_encodes_to_what_the_decoder_reads_back() {
let event = SseEvent {
event: Some("token".to_owned()),
data: "first\nsecond".to_owned(),
id: Some("42".to_owned()),
};
assert_eq!(round_trip(&event), vec![event.clone()]);
assert_eq!(
&event.encode()[..],
b"event: token\nid: 42\ndata: first\ndata: second\n\n"
);
}
#[test]
fn crlf_and_a_lone_cr_break_data_lines_when_encoding() {
let event = SseEvent::new("a\r\nb\rc\nd");
assert_eq!(
round_trip(&event),
vec![SseEvent {
event: None,
data: "a\nb\nc\nd".to_owned(),
id: None,
}]
);
}
#[test]
fn a_line_break_in_event_or_id_cannot_forge_a_field() {
let event = SseEvent {
event: Some("to\r\nid: forged".to_owned()),
data: "x".to_owned(),
id: Some("a\nbroke".to_owned()),
};
let decoded = round_trip(&event);
assert_eq!(decoded.len(), 1, "one event, not two");
assert_eq!(decoded[0].event.as_deref(), Some("toid: forged"));
assert_eq!(decoded[0].id.as_deref(), Some("abroke"));
}
#[test]
fn an_encoding_survives_being_dripped_byte_by_byte() {
let event = SseEvent {
event: Some("tick".to_owned()),
data: "one\ntwo".to_owned(),
id: None,
};
let wire = event.encode();
let mut decoder = SseDecoder::default();
let mut events = Vec::new();
for byte in &wire {
events.extend(decoder.push(std::slice::from_ref(byte)));
}
assert_eq!(events, vec![event]);
}
}