use std::borrow::Cow;
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum Framing {
Sse,
Ndjson,
Whole,
}
impl Framing {
pub fn framer(self) -> Option<Framer> {
let lines = match self {
Self::Sse => Lines::Sse(SseFramer::new()),
Self::Ndjson => Lines::Ndjson(NdjsonFramer::new()),
Self::Whole => return None,
};
Some(Framer { lines })
}
pub fn split(&self, bytes: &[u8]) -> Vec<WireFrame> {
let Some(mut framer) = self.framer() else {
return (!bytes.is_empty())
.then(|| WireFrame::from_bytes(bytes.to_vec()))
.into_iter()
.collect();
};
let mut frames: Vec<WireFrame> = framer.push(bytes).collect();
frames.extend(framer.finish());
frames
}
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub enum WireFrame {
Text(String),
Bytes(Vec<u8>),
}
impl WireFrame {
pub fn as_str(&self) -> Cow<'_, str> {
match self {
Self::Text(text) => Cow::Borrowed(text),
Self::Bytes(bytes) => String::from_utf8_lossy(bytes),
}
}
pub fn as_bytes(&self) -> &[u8] {
match self {
Self::Text(text) => text.as_bytes(),
Self::Bytes(bytes) => bytes,
}
}
pub fn from_bytes(bytes: Vec<u8>) -> Self {
match String::from_utf8(bytes) {
Ok(text) => Self::Text(text),
Err(error) => Self::Bytes(error.into_bytes()),
}
}
}
#[derive(Debug)]
pub struct Framer {
lines: Lines,
}
#[derive(Debug)]
enum Lines {
Sse(SseFramer),
Ndjson(NdjsonFramer),
}
impl Framer {
pub fn push(&mut self, chunk: &[u8]) -> impl Iterator<Item = WireFrame> + '_ {
let (sse, ndjson) = match &mut self.lines {
Lines::Sse(framer) => (Some(framer.push(chunk)), None),
Lines::Ndjson(framer) => (None, Some(framer.push(chunk))),
};
let events = sse.into_iter().flatten();
let events = events.filter(|event| !event.data.trim().is_empty());
let lines = ndjson.into_iter().flatten().map(WireFrame::from_bytes);
events.map(|event| WireFrame::Text(event.data)).chain(lines)
}
pub fn finish(&mut self) -> Option<WireFrame> {
match &mut self.lines {
Lines::Sse(_) => None,
Lines::Ndjson(framer) => framer.finish().map(WireFrame::from_bytes),
}
}
pub fn pending(&self) -> usize {
match &self.lines {
Lines::Sse(framer) => framer.pending(),
Lines::Ndjson(framer) => framer.pending(),
}
}
}
#[derive(Debug, Clone, Default, PartialEq, Eq)]
pub struct SseEvent {
pub event: Cow<'static, str>,
pub data: String,
pub id: String,
pub retry: Option<u64>,
}
const BOM: [u8; 3] = [0xef, 0xbb, 0xbf];
#[derive(Debug, Default)]
pub struct SseFramer {
buffer: Vec<u8>,
scanned: usize,
ready: Vec<SseEvent>,
event_type: String,
data: String,
last_event_id: String,
retry: Option<u64>,
bom_prefix: usize,
bom_done: bool,
since_dispatch: usize,
}
impl SseFramer {
pub fn new() -> Self {
Self::default()
}
pub fn push(&mut self, chunk: &[u8]) -> std::vec::Drain<'_, SseEvent> {
let chunk = self.strip_bom(chunk);
self.buffer.extend_from_slice(chunk);
let buffer = std::mem::take(&mut self.buffer);
let mut rest = buffer.as_slice();
while let Some((line, next)) = terminated_line(rest, self.scanned) {
self.scanned = 0;
self.since_dispatch = self.since_dispatch.saturating_add(rest.len() - next.len());
self.line(&String::from_utf8_lossy(line));
rest = next;
}
self.scanned = rest.len().saturating_sub(1);
let consumed = buffer.len() - rest.len();
self.buffer = buffer;
self.buffer.drain(..consumed);
self.ready.drain(..)
}
pub fn pending(&self) -> usize {
self.since_dispatch.saturating_add(self.buffer.len())
}
fn strip_bom<'a>(&mut self, chunk: &'a [u8]) -> &'a [u8] {
if self.bom_done {
return chunk;
}
let mut rest = chunk;
while let Some(&byte) = rest.first() {
if BOM.get(self.bom_prefix) != Some(&byte) {
let matched = std::mem::take(&mut self.bom_prefix);
self.bom_done = true;
self.buffer
.extend_from_slice(BOM.get(..matched).unwrap_or_default());
return rest;
}
self.bom_prefix += 1;
rest = rest.get(1..).unwrap_or_default();
if self.bom_prefix == BOM.len() {
self.bom_prefix = 0;
self.bom_done = true;
return rest;
}
}
rest
}
fn line(&mut self, line: &str) {
if line.is_empty() {
self.dispatch();
return;
}
if let Some(rest) = line.strip_prefix(':') {
let _ = rest;
return;
}
let (field, value) = match line.split_once(':') {
Some((field, value)) => (field, value.strip_prefix(' ').unwrap_or(value)),
None => (line, ""),
};
match field {
"event" => {
self.event_type.clear();
self.event_type.push_str(value);
}
"data" => {
self.data.push_str(value);
self.data.push('\n');
}
"id" if !value.contains('\0') => {
self.last_event_id.clear();
self.last_event_id.push_str(value);
}
"retry" if !value.is_empty() && value.bytes().all(|b| b.is_ascii_digit()) => {
self.retry = value.parse().ok();
}
_ => {}
}
}
fn dispatch(&mut self) {
self.since_dispatch = 0;
if self.data.is_empty() {
self.event_type.clear();
return;
}
self.data.pop();
let event = if self.event_type.is_empty() {
Cow::Borrowed("message")
} else {
Cow::Owned(std::mem::take(&mut self.event_type))
};
self.ready.push(SseEvent {
event,
data: std::mem::take(&mut self.data),
id: self.last_event_id.clone(),
retry: self.retry.take(),
});
self.event_type.clear();
}
}
#[derive(Debug, Default)]
pub struct NdjsonFramer {
buffer: Vec<u8>,
scanned: usize,
ready: Vec<Vec<u8>>,
}
impl NdjsonFramer {
pub fn new() -> Self {
Self::default()
}
pub fn push(&mut self, chunk: &[u8]) -> std::vec::Drain<'_, Vec<u8>> {
self.buffer.extend_from_slice(chunk);
let mut rest = self.buffer.as_slice();
while let Some((line, end)) = split_line(rest, self.scanned, |byte| *byte == b'\n') {
self.scanned = 0;
let line = line.strip_suffix(b"\r").unwrap_or(line);
if !line.is_empty() {
self.ready.push(line.to_vec());
}
rest = end.get(1..).unwrap_or_default();
}
self.scanned = rest.len();
let consumed = self.buffer.len() - rest.len();
self.buffer.drain(..consumed);
self.ready.drain(..)
}
pub fn finish(&mut self) -> Option<Vec<u8>> {
let line = std::mem::take(&mut self.buffer);
(!line.is_empty()).then_some(line)
}
pub fn pending(&self) -> usize {
self.buffer.len()
}
}
fn terminated_line(buffer: &[u8], scanned: usize) -> Option<(&[u8], &[u8])> {
match split_line(buffer, scanned, |byte| *byte == b'\n' || *byte == b'\r')? {
(line, [b'\r', b'\n', rest @ ..]) => Some((line, rest)),
(_, [b'\r'] | []) => None,
(line, [_, rest @ ..]) => Some((line, rest)),
}
}
fn split_line(buffer: &[u8], scanned: usize, ends: impl Fn(&u8) -> bool) -> Option<(&[u8], &[u8])> {
buffer.split_at_checked(scanned + buffer.iter().skip(scanned).position(ends)?)
}
#[cfg(test)]
mod tests;