use crate::adapters::primary::transport::http::{SseError, SseEvent};
const MAX_BLOCK: usize = 8 * 1024;
#[derive(Debug, thiserror::Error)]
pub enum FrameError {
#[error("the peer sent {limit} bytes with no block terminator")]
BlockTooLong {
limit: usize,
},
#[error("the block is not a frame this client can read: {source}")]
Grammar {
source: SseError,
},
}
impl From<SseError> for FrameError {
fn from(source: SseError) -> Self {
Self::Grammar { source }
}
}
#[derive(Debug, Default)]
pub struct FrameReader {
buffer: Vec<u8>,
}
impl FrameReader {
#[must_use]
pub const fn new() -> Self {
Self { buffer: Vec::new() }
}
#[must_use]
pub const fn buffered(&self) -> usize {
self.buffer.len()
}
pub fn push(&mut self, chunk: &[u8]) -> Result<Vec<SseEvent>, FrameError> {
self.buffer.extend_from_slice(chunk);
if self.buffer.len() > MAX_BLOCK {
self.buffer.clear();
return Err(FrameError::BlockTooLong { limit: MAX_BLOCK });
}
let mut frames = Vec::new();
let mut consumed = 0_usize;
while let Some(tail) = self.buffer.get(consumed..) {
let Some((block, used)) = split_block(tail) else {
break;
};
if let Some(event) = SseEvent::dispatch(block)? {
frames.push(event);
}
consumed = consumed.saturating_add(used);
}
if consumed > 0 {
let _ = self.buffer.drain(..consumed);
}
Ok(frames)
}
#[must_use]
pub fn into_partial(self) -> Option<Vec<u8>> {
if self.buffer.is_empty() {
None
} else {
Some(self.buffer)
}
}
}
#[must_use]
pub fn split_block(bytes: &[u8]) -> Option<(&str, usize)> {
let mut start = 0_usize;
loop {
let end = line_end(bytes, start)?;
let blank = match bytes.get(end) {
Some(b'\n') => Some(end.saturating_add(1)),
Some(b'\r') => Some(if bytes.get(end + 1) == Some(&b'\n') {
end.saturating_add(2)
} else {
end.saturating_add(1)
}),
_ => None,
};
if let Some(consumed) = blank {
let block = bytes.get(..end)?;
return Some((decode_lossy(block), consumed));
}
start = end;
}
}
fn line_end(bytes: &[u8], from: usize) -> Option<usize> {
let mut index = from;
while index < bytes.len() {
match bytes.get(index) {
Some(b'\n') => return Some(index.saturating_add(1)),
Some(b'\r') => {
return Some(if bytes.get(index + 1) == Some(&b'\n') {
index.saturating_add(2)
} else {
index.saturating_add(1)
});
}
_ => index = index.saturating_add(1),
}
}
None
}
fn decode_lossy(bytes: &[u8]) -> &str {
core::str::from_utf8(bytes).unwrap_or_default()
}