use axum::response::sse::{Event, KeepAlive, Sse};
use futures::StreamExt;
use futures::stream::BoxStream;
use std::convert::Infallible;
use std::time::Duration;
const SSE_KEEP_ALIVE_INTERVAL: Duration = Duration::from_secs(15);
fn next_event_id(counter: &mut u64) -> String {
*counter += 1;
counter.to_string()
}
pub fn sse_from_stream<T: serde::Serialize + Send + 'static>(
stream: BoxStream<'static, Result<T, a2a::A2AError>>,
) -> Sse<impl futures::Stream<Item = Result<Event, Infallible>>> {
let mut event_id = 0u64;
let sse_stream = stream.map(move |item| {
let data = match item {
Ok(val) => serde_json::to_string(&val).unwrap_or_default(),
Err(err) => serde_json::to_string(&err.to_jsonrpc_error()).unwrap_or_default(),
};
Ok::<_, Infallible>(Event::default().id(next_event_id(&mut event_id)).data(data))
});
Sse::new(sse_stream).keep_alive(KeepAlive::new().interval(SSE_KEEP_ALIVE_INTERVAL))
}
pub fn sse_jsonrpc_stream<T: serde::Serialize + Send + 'static>(
request_id: a2a::JsonRpcId,
stream: BoxStream<'static, Result<T, a2a::A2AError>>,
) -> Sse<impl futures::Stream<Item = Result<Event, Infallible>>> {
let mut event_id = 0u64;
let sse_stream = stream.map(move |item| {
let data = match item {
Ok(val) => {
let result_val = serde_json::to_value(&val).unwrap_or_default();
let resp = a2a::JsonRpcResponse::success(request_id.clone(), result_val);
serde_json::to_string(&resp).unwrap_or_default()
}
Err(err) => {
let resp = a2a::JsonRpcResponse::error(request_id.clone(), err.to_jsonrpc_error());
serde_json::to_string(&resp).unwrap_or_default()
}
};
Ok::<_, Infallible>(Event::default().id(next_event_id(&mut event_id)).data(data))
});
Sse::new(sse_stream).keep_alive(KeepAlive::new().interval(SSE_KEEP_ALIVE_INTERVAL))
}
#[cfg(test)]
mod tests {
use super::*;
use a2a::*;
use axum::response::IntoResponse;
use futures::stream;
use http_body_util::BodyExt;
#[tokio::test]
async fn test_sse_from_stream_ok() {
let s: BoxStream<'static, Result<serde_json::Value, A2AError>> =
Box::pin(stream::once(async {
Ok(serde_json::json!({"key": "value"}))
}));
let sse = sse_from_stream(s);
let resp = sse.into_response();
assert_eq!(resp.status(), 200);
let body = resp.into_body().collect().await.unwrap().to_bytes();
let body_str = String::from_utf8(body.to_vec()).unwrap();
assert!(body_str.contains("data:"));
assert!(body_str.contains("value"));
}
#[tokio::test]
async fn test_sse_from_stream_err() {
let s: BoxStream<'static, Result<serde_json::Value, A2AError>> =
Box::pin(stream::once(async { Err(A2AError::internal("fail")) }));
let sse = sse_from_stream(s);
let resp = sse.into_response();
let body = resp.into_body().collect().await.unwrap().to_bytes();
let body_str = String::from_utf8(body.to_vec()).unwrap();
assert!(body_str.contains("data:"));
assert!(body_str.contains("fail"));
}
#[tokio::test]
async fn test_sse_jsonrpc_stream_ok() {
let s: BoxStream<'static, Result<serde_json::Value, A2AError>> =
Box::pin(stream::once(async {
Ok(serde_json::json!({"status": "ok"}))
}));
let sse = sse_jsonrpc_stream(JsonRpcId::Number(1), s);
let resp = sse.into_response();
let body = resp.into_body().collect().await.unwrap().to_bytes();
let body_str = String::from_utf8(body.to_vec()).unwrap();
assert!(body_str.contains("data:"));
assert!(body_str.contains("jsonrpc"));
assert!(body_str.contains("2.0"));
}
#[tokio::test]
async fn test_sse_jsonrpc_stream_err() {
let s: BoxStream<'static, Result<serde_json::Value, A2AError>> =
Box::pin(stream::once(async { Err(A2AError::internal("fail")) }));
let sse = sse_jsonrpc_stream(JsonRpcId::Number(1), s);
let resp = sse.into_response();
let body = resp.into_body().collect().await.unwrap().to_bytes();
let body_str = String::from_utf8(body.to_vec()).unwrap();
assert!(body_str.contains("fail"));
assert!(body_str.contains("error"));
}
#[tokio::test]
async fn test_sse_jsonrpc_stream_multiple_events() {
let s: BoxStream<'static, Result<serde_json::Value, A2AError>> =
Box::pin(stream::iter(vec![
Ok(serde_json::json!({"n": 1})),
Ok(serde_json::json!({"n": 2})),
Err(A2AError::internal("done")),
]));
let sse = sse_jsonrpc_stream(JsonRpcId::String("req-1".into()), s);
let resp = sse.into_response();
let body = resp.into_body().collect().await.unwrap().to_bytes();
let body_str = String::from_utf8(body.to_vec()).unwrap();
let data_count = body_str.matches("data:").count();
assert!(
data_count >= 3,
"expected >= 3 data lines, got {data_count}"
);
}
fn event_ids(body: &str) -> Vec<String> {
body.lines()
.filter_map(|line| line.strip_prefix("id:"))
.map(|id| id.trim().to_string())
.collect()
}
#[tokio::test]
async fn test_sse_from_stream_assigns_monotonic_event_ids() {
let s: BoxStream<'static, Result<serde_json::Value, A2AError>> =
Box::pin(stream::iter(vec![
Ok(serde_json::json!({"n": 1})),
Ok(serde_json::json!({"n": 2})),
Err(A2AError::internal("boom")),
]));
let body = sse_from_stream(s)
.into_response()
.into_body()
.collect()
.await
.unwrap()
.to_bytes();
let body = String::from_utf8(body.to_vec()).unwrap();
assert_eq!(event_ids(&body), vec!["1", "2", "3"], "{body}");
}
#[tokio::test]
async fn test_sse_jsonrpc_stream_assigns_monotonic_event_ids() {
let s: BoxStream<'static, Result<serde_json::Value, A2AError>> =
Box::pin(stream::iter(vec![
Ok(serde_json::json!({"n": 1})),
Ok(serde_json::json!({"n": 2})),
Ok(serde_json::json!({"n": 3})),
]));
let body = sse_jsonrpc_stream(JsonRpcId::Number(1), s)
.into_response()
.into_body()
.collect()
.await
.unwrap()
.to_bytes();
let body = String::from_utf8(body.to_vec()).unwrap();
assert_eq!(event_ids(&body), vec!["1", "2", "3"], "{body}");
}
#[tokio::test]
async fn test_event_ids_are_scoped_to_one_response() {
let make = || -> BoxStream<'static, Result<serde_json::Value, A2AError>> {
Box::pin(stream::once(async { Ok(serde_json::json!({"n": 1})) }))
};
for _ in 0..2 {
let body = sse_from_stream(make())
.into_response()
.into_body()
.collect()
.await
.unwrap()
.to_bytes();
let body = String::from_utf8(body.to_vec()).unwrap();
assert_eq!(event_ids(&body), vec!["1"], "{body}");
}
}
}