#[derive(Default)]
pub(crate) struct SseParser {
buffer: Vec<u8>,
pending_data_lines: Vec<String>,
}
impl SseParser {
pub(crate) fn push(&mut self, chunk: &[u8]) -> Vec<String> {
self.buffer.extend_from_slice(chunk);
let mut events = Vec::new();
while let Some(newline_at) = self.buffer.iter().position(|&b| b == b'\n') {
let mut line: Vec<u8> = self.buffer.drain(..=newline_at).collect();
line.pop(); if line.last() == Some(&b'\r') {
line.pop();
}
let line = String::from_utf8_lossy(&line);
if line.is_empty() {
if !self.pending_data_lines.is_empty() {
events.push(self.pending_data_lines.join("\n"));
self.pending_data_lines.clear();
}
continue;
}
if let Some(data) = line.strip_prefix("data:") {
let data = data.strip_prefix(' ').unwrap_or(data);
if data.trim() == "[DONE]" {
continue;
}
self.pending_data_lines.push(data.to_string());
}
}
events
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn sse_parser_survives_chunk_splits_inside_multibyte_utf8() {
let event = "data: {\"text\":\"héllo → 世界 🌍\"}\n\n";
let mut parser = SseParser::default();
let mut events = Vec::new();
for byte in event.as_bytes() {
events.extend(parser.push(std::slice::from_ref(byte)));
}
assert_eq!(events, vec!["{\"text\":\"héllo → 世界 🌍\"}".to_string()]);
}
#[test]
fn sse_parser_joins_multiline_data_and_skips_done() {
let mut parser = SseParser::default();
let events = parser.push(b"data: a\ndata: b\n\ndata: [DONE]\n\nevent: x\ndata: c\r\n\r\n");
assert_eq!(events, vec!["a\nb".to_string(), "c".to_string()]);
}
}