use std::collections::VecDeque;
use crate::protocol::TaskPushNotification;
use super::A2AError;
pub struct A2ASseStream {
response: reqwest::Response,
parser: A2aSseParser,
pending: VecDeque<TaskPushNotification>,
}
impl A2ASseStream {
pub(crate) fn new(response: reqwest::Response) -> Self {
Self {
response,
parser: A2aSseParser::new(),
pending: VecDeque::new(),
}
}
pub async fn next(&mut self) -> Option<Result<TaskPushNotification, A2AError>> {
loop {
if let Some(event) = self.pending.pop_front() {
return Some(Ok(event));
}
match self.response.chunk().await {
Ok(Some(chunk)) => {
let text = String::from_utf8_lossy(&chunk);
self.pending.extend(self.parser.feed(&text));
}
Ok(None) => return None,
Err(e) => return Some(Err(A2AError::from(e))),
}
}
}
}
struct A2aSseParser {
buffer: String,
}
impl A2aSseParser {
fn new() -> Self {
Self {
buffer: String::new(),
}
}
fn feed(&mut self, chunk: &str) -> Vec<TaskPushNotification> {
self.buffer.push_str(chunk);
let mut out = Vec::new();
while let Some(pos) = self.buffer.find("\n\n") {
let event_text: String = self.buffer[..pos].to_string();
self.buffer.drain(..=pos + 1);
let data = event_text
.lines()
.filter_map(|line| line.strip_prefix("data:"))
.map(|line| line.trim_start().trim_end())
.collect::<Vec<_>>()
.join("\n");
if data.is_empty() || data == "[DONE]" {
continue;
}
match serde_json::from_str::<TaskPushNotification>(&data) {
Ok(notification) => out.push(notification),
Err(e) => {
log::warn!("skipping malformed SSE event `{data}`: {e}");
}
}
}
out
}
}