1use a2a_protocol_types::{JsonRpcResponse, StreamResponse};
35use hyper::body::Bytes;
36use tokio::sync::mpsc;
37use tokio::task::AbortHandle;
38
39use crate::error::{ClientError, ClientResult};
40use crate::streaming::sse_parser::SseParser;
41
42pub(crate) type BodyChunk = ClientResult<Bytes>;
46
47pub struct EventStream {
58 rx: mpsc::Receiver<BodyChunk>,
60 parser: SseParser,
62 done: bool,
64 abort_handle: Option<AbortHandle>,
66 status_code: u16,
73 jsonrpc_envelope: bool,
79 first_event_timeout: Option<std::time::Duration>,
87 first_chunk_received: bool,
89}
90
91impl EventStream {
92 #[must_use]
98 #[cfg(any(test, feature = "websocket"))]
99 pub(crate) fn new(rx: mpsc::Receiver<BodyChunk>) -> Self {
100 Self {
101 rx,
102 parser: SseParser::new(),
103 done: false,
104 abort_handle: None,
105 status_code: 200,
106 jsonrpc_envelope: true,
107 first_event_timeout: None,
108 first_chunk_received: false,
109 }
110 }
111
112 #[must_use]
117 #[cfg(test)]
118 pub(crate) fn with_abort_handle(
119 rx: mpsc::Receiver<BodyChunk>,
120 abort_handle: AbortHandle,
121 ) -> Self {
122 Self {
123 rx,
124 parser: SseParser::new(),
125 done: false,
126 abort_handle: Some(abort_handle),
127 status_code: 200,
128 jsonrpc_envelope: true,
129 first_event_timeout: None,
130 first_chunk_received: false,
131 }
132 }
133
134 #[must_use]
137 pub(crate) fn with_status(
138 rx: mpsc::Receiver<BodyChunk>,
139 abort_handle: AbortHandle,
140 status_code: u16,
141 ) -> Self {
142 Self {
143 rx,
144 parser: SseParser::new(),
145 done: false,
146 abort_handle: Some(abort_handle),
147 status_code,
148 jsonrpc_envelope: true,
149 first_event_timeout: None,
150 first_chunk_received: false,
151 }
152 }
153
154 #[must_use]
159 pub(crate) const fn with_jsonrpc_envelope(mut self, envelope: bool) -> Self {
160 self.jsonrpc_envelope = envelope;
161 self
162 }
163
164 #[must_use]
175 pub(crate) const fn with_first_event_timeout(mut self, timeout: std::time::Duration) -> Self {
176 self.first_event_timeout = Some(timeout);
177 self
178 }
179
180 #[must_use]
185 pub const fn status_code(&self) -> u16 {
186 self.status_code
187 }
188
189 pub async fn next(&mut self) -> Option<ClientResult<StreamResponse>> {
196 loop {
197 if let Some(result) = self.parser.next_frame() {
199 match result {
200 Ok(frame) => return Some(self.decode_frame(&frame.data)),
201 Err(e) => {
202 return Some(Err(ClientError::Transport(e.to_string())));
203 }
204 }
205 }
206
207 if self.done {
208 return None;
209 }
210
211 let chunk = match self.first_event_timeout {
216 Some(timeout) if !self.first_chunk_received => {
217 let Ok(chunk) = tokio::time::timeout(timeout, self.rx.recv()).await else {
218 self.done = true;
219 return Some(Err(ClientError::Timeout(
220 "stream produced no data before the first-event timeout".into(),
221 )));
222 };
223 chunk
224 }
225 _ => self.rx.recv().await,
226 };
227 match chunk {
228 None => {
229 self.done = true;
231 if let Some(result) = self.parser.next_frame() {
233 match result {
234 Ok(frame) => return Some(self.decode_frame(&frame.data)),
235 Err(e) => {
236 return Some(Err(ClientError::Transport(e.to_string())));
237 }
238 }
239 }
240 return None;
241 }
242 Some(Err(e)) => {
243 self.done = true;
244 return Some(Err(e));
245 }
246 Some(Ok(bytes)) => {
247 self.first_chunk_received = true;
248 self.parser.feed(&bytes);
249 }
250 }
251 }
252 }
253
254 fn decode_frame(&mut self, data: &str) -> ClientResult<StreamResponse> {
257 if self.jsonrpc_envelope {
258 let envelope: JsonRpcResponse<StreamResponse> =
260 serde_json::from_str(data).map_err(ClientError::Serialization)?;
261
262 match envelope {
263 JsonRpcResponse::Success(ok) => {
264 if is_terminal(&ok.result) {
265 self.done = true;
266 }
267 Ok(ok.result)
268 }
269 JsonRpcResponse::Error(err) => {
270 self.done = true;
271 let a2a = crate::transport::map_jsonrpc_error(
272 err.error.code,
273 err.error.message,
274 err.error.data,
275 );
276 Err(ClientError::Protocol(a2a))
277 }
278 }
279 } else {
280 let event: StreamResponse =
283 serde_json::from_str(data).map_err(ClientError::Serialization)?;
284 if is_terminal(&event) {
285 self.done = true;
286 }
287 Ok(event)
288 }
289 }
290}
291
292impl Drop for EventStream {
293 fn drop(&mut self) {
294 if let Some(handle) = self.abort_handle.take() {
295 handle.abort();
296 }
297 }
298}
299
300#[allow(clippy::missing_fields_in_debug)]
301impl std::fmt::Debug for EventStream {
302 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
303 f.debug_struct("EventStream")
305 .field("done", &self.done)
306 .field("pending_frames", &self.parser.pending_count())
307 .finish()
308 }
309}
310
311const fn is_terminal(event: &StreamResponse) -> bool {
313 matches!(
314 event,
315 StreamResponse::StatusUpdate(ev) if ev.status.state.is_terminal()
316 )
317}
318
319#[cfg(test)]
322mod tests {
323 use super::*;
324 use a2a_protocol_types::{
325 JsonRpcSuccessResponse, JsonRpcVersion, TaskId, TaskState, TaskStatus,
326 TaskStatusUpdateEvent,
327 };
328 use std::time::Duration;
329
330 const TEST_TIMEOUT: Duration = Duration::from_secs(5);
333
334 fn make_status_event(state: TaskState, _is_final: bool) -> StreamResponse {
335 StreamResponse::StatusUpdate(TaskStatusUpdateEvent {
336 task_id: TaskId::new("t1"),
337 context_id: a2a_protocol_types::ContextId::new("c1"),
338 status: TaskStatus {
339 state,
340 message: None,
341 timestamp: None,
342 },
343 metadata: None,
344 })
345 }
346
347 fn sse_frame(event: &StreamResponse) -> String {
348 let resp = JsonRpcSuccessResponse {
349 jsonrpc: JsonRpcVersion,
350 id: Some(serde_json::json!(1)),
351 result: event.clone(),
352 };
353 let json = serde_json::to_string(&resp).unwrap();
354 format!("data: {json}\n\n")
355 }
356
357 #[tokio::test]
358 async fn stream_delivers_events() {
359 let (tx, rx) = mpsc::channel(8);
360 let mut stream = EventStream::new(rx);
361
362 let event = make_status_event(TaskState::Working, false);
363 let sse_bytes = sse_frame(&event);
364 tx.send(Ok(Bytes::from(sse_bytes))).await.unwrap();
365 drop(tx);
366
367 let result = tokio::time::timeout(TEST_TIMEOUT, stream.next())
368 .await
369 .expect("timed out")
370 .unwrap()
371 .unwrap();
372 assert!(
373 matches!(result, StreamResponse::StatusUpdate(ref ev) if ev.status.state == TaskState::Working)
374 );
375 }
376
377 #[tokio::test]
378 async fn stream_ends_on_final_event() {
379 let (tx, rx) = mpsc::channel(8);
380 let mut stream = EventStream::new(rx);
381
382 let event = make_status_event(TaskState::Completed, true);
383 let sse_bytes = sse_frame(&event);
384 tx.send(Ok(Bytes::from(sse_bytes))).await.unwrap();
385
386 let result = tokio::time::timeout(TEST_TIMEOUT, stream.next())
388 .await
389 .expect("timed out waiting for final event")
390 .unwrap()
391 .unwrap();
392 assert!(
393 matches!(result, StreamResponse::StatusUpdate(ref ev) if ev.status.state == TaskState::Completed)
394 );
395
396 let end = tokio::time::timeout(TEST_TIMEOUT, stream.next())
398 .await
399 .expect("timed out waiting for stream end");
400 assert!(end.is_none());
401 }
402
403 #[tokio::test]
404 async fn stream_propagates_body_error() {
405 let (tx, rx) = mpsc::channel(8);
406 let mut stream = EventStream::new(rx);
407
408 tx.send(Err(ClientError::Transport("network error".into())))
409 .await
410 .unwrap();
411
412 let result = tokio::time::timeout(TEST_TIMEOUT, stream.next())
413 .await
414 .expect("timed out")
415 .unwrap();
416 assert!(result.is_err());
417 }
418
419 #[tokio::test]
420 async fn stream_ends_when_channel_closed() {
421 let (tx, rx) = mpsc::channel(8);
422 let mut stream = EventStream::new(rx);
423 drop(tx);
424
425 let result = tokio::time::timeout(TEST_TIMEOUT, stream.next())
426 .await
427 .expect("timed out");
428 assert!(result.is_none());
429 }
430
431 #[tokio::test]
432 async fn drop_aborts_background_task() {
433 let (tx, rx) = mpsc::channel::<BodyChunk>(8);
434 let handle = tokio::spawn(async move {
436 let _tx = tx;
438 tokio::time::sleep(Duration::from_secs(60 * 60)).await;
440 });
441 let abort_handle = handle.abort_handle();
442 let stream = EventStream::with_abort_handle(rx, abort_handle);
443 drop(stream);
445 let result = tokio::time::timeout(TEST_TIMEOUT, handle)
447 .await
448 .expect("timed out waiting for task abort");
449 assert!(result.is_err(), "task should have been aborted");
450 assert!(
451 result.unwrap_err().is_cancelled(),
452 "task should be cancelled"
453 );
454 }
455
456 #[test]
457 fn debug_output_contains_fields() {
458 let (_tx, rx) = mpsc::channel::<BodyChunk>(8);
459 let stream = EventStream::new(rx);
460 let debug = format!("{stream:?}");
461 assert!(debug.contains("EventStream"), "should contain struct name");
462 assert!(debug.contains("done"), "should contain 'done' field");
463 assert!(
464 debug.contains("pending_frames"),
465 "should contain 'pending_frames' field"
466 );
467 }
468
469 #[test]
470 fn is_terminal_returns_false_for_working() {
471 let event = make_status_event(TaskState::Working, false);
472 assert!(!is_terminal(&event), "Working state should not be terminal");
473 }
474
475 #[test]
476 fn is_terminal_returns_true_for_completed() {
477 let event = make_status_event(TaskState::Completed, true);
478 assert!(is_terminal(&event), "Completed state should be terminal");
479 }
480
481 #[tokio::test]
484 async fn stream_decodes_jsonrpc_error_as_protocol_error() {
485 use a2a_protocol_types::{JsonRpcErrorResponse, JsonRpcVersion};
486
487 let (tx, rx) = mpsc::channel(8);
488 let mut stream = EventStream::new(rx);
489
490 let error_resp = JsonRpcErrorResponse {
492 jsonrpc: JsonRpcVersion,
493 id: Some(serde_json::json!(1)),
494 error: a2a_protocol_types::JsonRpcError {
495 code: -32601,
496 message: "method not found".into(),
497 data: None,
498 },
499 };
500 let json = serde_json::to_string(&error_resp).unwrap();
501 let sse_data = format!("data: {json}\n\n");
502 tx.send(Ok(Bytes::from(sse_data))).await.unwrap();
503 drop(tx);
504
505 let result = tokio::time::timeout(TEST_TIMEOUT, stream.next())
506 .await
507 .expect("timed out")
508 .unwrap();
509 assert!(result.is_err(), "JSON-RPC error should produce Err");
510 match result.unwrap_err() {
511 ClientError::Protocol(err) => {
512 assert!(
513 format!("{err}").contains("method not found"),
514 "error message should be preserved"
515 );
516 }
517 other => panic!("expected Protocol error, got {other:?}"),
518 }
519
520 let end = tokio::time::timeout(TEST_TIMEOUT, stream.next())
522 .await
523 .expect("timed out");
524 assert!(end.is_none(), "stream should end after JSON-RPC error");
525 }
526
527 #[tokio::test]
530 async fn stream_invalid_json_returns_serialization_error() {
531 let (tx, rx) = mpsc::channel(8);
532 let mut stream = EventStream::new(rx);
533
534 let sse_data = "data: {not valid json}\n\n";
535 tx.send(Ok(Bytes::from(sse_data))).await.unwrap();
536 drop(tx);
537
538 let result = tokio::time::timeout(TEST_TIMEOUT, stream.next())
539 .await
540 .expect("timed out")
541 .unwrap();
542 assert!(result.is_err(), "invalid JSON should produce Err");
543 assert!(
544 matches!(result.unwrap_err(), ClientError::Serialization(_)),
545 "should be a Serialization error"
546 );
547 }
548
549 #[tokio::test]
552 async fn stream_drains_parser_after_channel_close() {
553 let (tx, rx) = mpsc::channel(8);
554 let mut stream = EventStream::new(rx);
555
556 let event = make_status_event(TaskState::Working, false);
559 let sse_bytes = sse_frame(&event);
560 let (first_half, second_half) = sse_bytes.split_at(sse_bytes.len() / 2);
561
562 tx.send(Ok(Bytes::from(first_half.to_owned())))
563 .await
564 .unwrap();
565 tx.send(Ok(Bytes::from(second_half.to_owned())))
566 .await
567 .unwrap();
568 drop(tx);
569
570 let result = tokio::time::timeout(TEST_TIMEOUT, stream.next())
571 .await
572 .expect("timed out")
573 .unwrap();
574 let event = result.unwrap();
575 assert!(
576 matches!(event, StreamResponse::StatusUpdate(ref ev) if ev.status.state == TaskState::Working),
577 "should deliver Working event from drained parser"
578 );
579 }
580
581 #[tokio::test]
583 async fn status_code_returns_set_value() {
584 let (_tx, rx) = mpsc::channel::<BodyChunk>(8);
585 let stream = EventStream::new(rx);
586 assert_eq!(stream.status_code(), 200, "default status should be 200");
587 }
588
589 #[tokio::test]
591 async fn status_code_with_custom_value() {
592 let (_tx, rx) = mpsc::channel::<BodyChunk>(8);
593 let task = tokio::spawn(async { tokio::time::sleep(Duration::from_secs(60)).await });
594 let stream = EventStream::with_status(rx, task.abort_handle(), 201);
595 assert_eq!(stream.status_code(), 201);
596 }
597
598 #[tokio::test]
601 async fn first_event_timeout_fires_when_no_data_arrives() {
602 let (_tx, rx) = mpsc::channel::<BodyChunk>(8);
604 let mut stream = EventStream::new(rx).with_first_event_timeout(Duration::from_millis(50));
605 let result = tokio::time::timeout(Duration::from_secs(2), stream.next())
608 .await
609 .expect("first-event timeout must fire well within 2s");
610 assert!(
611 matches!(result, Some(Err(ClientError::Timeout(_)))),
612 "expected first-event timeout, got {result:?}"
613 );
614 let done = tokio::time::timeout(Duration::from_secs(2), stream.next())
616 .await
617 .expect("a completed stream must return promptly");
618 assert!(done.is_none());
619 }
620
621 #[tokio::test]
624 async fn first_event_timeout_lifted_after_first_chunk() {
625 let (tx, rx) = mpsc::channel(8);
626 let mut stream = EventStream::new(rx)
627 .with_jsonrpc_envelope(false)
628 .with_first_event_timeout(Duration::from_millis(50));
629 let event = make_status_event(TaskState::Working, false);
631 tx.send(Ok(Bytes::from(bare_sse_frame(&event))))
632 .await
633 .unwrap();
634 let first = stream.next().await;
635 assert!(
636 matches!(first, Some(Ok(_))),
637 "first event should parse, got {first:?}"
638 );
639 let pending = tokio::time::timeout(Duration::from_millis(120), stream.next()).await;
642 assert!(
643 pending.is_err(),
644 "stream must remain open (pending) after first chunk, got {pending:?}"
645 );
646 }
647
648 #[tokio::test]
651 async fn stream_transport_error_from_channel() {
652 let (tx, rx) = mpsc::channel(8);
653 let mut stream = EventStream::new(rx);
654
655 tx.send(Err(ClientError::HttpClient("connection reset".into())))
657 .await
658 .unwrap();
659
660 let result = tokio::time::timeout(TEST_TIMEOUT, stream.next())
661 .await
662 .expect("timed out")
663 .unwrap();
664 match result {
665 Err(ClientError::HttpClient(msg)) => {
666 assert!(msg.contains("connection reset"));
667 }
668 other => panic!("expected HttpClient error, got {other:?}"),
669 }
670
671 let end = tokio::time::timeout(TEST_TIMEOUT, stream.next())
673 .await
674 .expect("timed out");
675 assert!(end.is_none(), "stream should end after transport error");
676 }
677
678 #[tokio::test]
679 async fn non_terminal_event_does_not_end_stream() {
680 let (tx, rx) = mpsc::channel(8);
681 let mut stream = EventStream::new(rx);
682
683 let working = make_status_event(TaskState::Working, false);
685 let completed = make_status_event(TaskState::Completed, true);
686 tx.send(Ok(Bytes::from(sse_frame(&working)))).await.unwrap();
687 tx.send(Ok(Bytes::from(sse_frame(&completed))))
688 .await
689 .unwrap();
690
691 let first = tokio::time::timeout(TEST_TIMEOUT, stream.next())
693 .await
694 .expect("timed out on first event")
695 .unwrap()
696 .unwrap();
697 assert!(
698 matches!(first, StreamResponse::StatusUpdate(ref ev) if ev.status.state == TaskState::Working)
699 );
700
701 let second = tokio::time::timeout(TEST_TIMEOUT, stream.next())
703 .await
704 .expect("timed out on second event")
705 .unwrap()
706 .unwrap();
707 assert!(
708 matches!(second, StreamResponse::StatusUpdate(ref ev) if ev.status.state == TaskState::Completed)
709 );
710
711 let end = tokio::time::timeout(TEST_TIMEOUT, stream.next())
713 .await
714 .expect("timed out waiting for stream end");
715 assert!(end.is_none());
716 }
717
718 fn bare_sse_frame(event: &StreamResponse) -> String {
722 let json = serde_json::to_string(event).unwrap();
723 format!("data: {json}\n\n")
724 }
725
726 #[tokio::test]
727 async fn bare_stream_delivers_events() {
728 let (tx, rx) = mpsc::channel(8);
729 let mut stream = EventStream::new(rx).with_jsonrpc_envelope(false);
730
731 let event = make_status_event(TaskState::Working, false);
732 tx.send(Ok(Bytes::from(bare_sse_frame(&event))))
733 .await
734 .unwrap();
735 drop(tx);
736
737 let result = tokio::time::timeout(TEST_TIMEOUT, stream.next())
738 .await
739 .expect("timed out")
740 .unwrap()
741 .unwrap();
742 assert!(
743 matches!(result, StreamResponse::StatusUpdate(ref ev) if ev.status.state == TaskState::Working)
744 );
745 }
746
747 #[tokio::test]
748 async fn bare_stream_ends_on_terminal() {
749 let (tx, rx) = mpsc::channel(8);
750 let mut stream = EventStream::new(rx).with_jsonrpc_envelope(false);
751
752 let event = make_status_event(TaskState::Completed, true);
753 tx.send(Ok(Bytes::from(bare_sse_frame(&event))))
754 .await
755 .unwrap();
756
757 let result = tokio::time::timeout(TEST_TIMEOUT, stream.next())
758 .await
759 .expect("timed out")
760 .unwrap()
761 .unwrap();
762 assert!(
763 matches!(result, StreamResponse::StatusUpdate(ref ev) if ev.status.state == TaskState::Completed)
764 );
765
766 let end = tokio::time::timeout(TEST_TIMEOUT, stream.next())
767 .await
768 .expect("timed out");
769 assert!(end.is_none(), "bare stream should end after terminal event");
770 }
771
772 #[tokio::test]
773 async fn bare_stream_rejects_jsonrpc_envelope() {
774 let (tx, rx) = mpsc::channel(8);
775 let mut stream = EventStream::new(rx).with_jsonrpc_envelope(false);
776
777 let event = make_status_event(TaskState::Working, false);
779 let envelope_frame = sse_frame(&event); tx.send(Ok(Bytes::from(envelope_frame))).await.unwrap();
781 drop(tx);
782
783 let result = tokio::time::timeout(TEST_TIMEOUT, stream.next())
784 .await
785 .expect("timed out")
786 .unwrap();
787 assert!(
788 result.is_err(),
789 "bare stream should reject JSON-RPC envelope as invalid"
790 );
791 }
792
793 #[tokio::test]
794 async fn envelope_stream_rejects_bare_response() {
795 let (tx, rx) = mpsc::channel(8);
796 let mut stream = EventStream::new(rx); let event = make_status_event(TaskState::Working, false);
800 let bare_frame = bare_sse_frame(&event);
801 tx.send(Ok(Bytes::from(bare_frame))).await.unwrap();
802 drop(tx);
803
804 let result = tokio::time::timeout(TEST_TIMEOUT, stream.next())
805 .await
806 .expect("timed out")
807 .unwrap();
808 assert!(
809 result.is_err(),
810 "envelope stream should reject bare StreamResponse"
811 );
812 }
813
814 #[tokio::test]
815 async fn bare_stream_multiple_events() {
816 let (tx, rx) = mpsc::channel(8);
817 let mut stream = EventStream::new(rx).with_jsonrpc_envelope(false);
818
819 let working = make_status_event(TaskState::Working, false);
820 let completed = make_status_event(TaskState::Completed, true);
821 tx.send(Ok(Bytes::from(bare_sse_frame(&working))))
822 .await
823 .unwrap();
824 tx.send(Ok(Bytes::from(bare_sse_frame(&completed))))
825 .await
826 .unwrap();
827
828 let first = tokio::time::timeout(TEST_TIMEOUT, stream.next())
829 .await
830 .expect("timed out")
831 .unwrap()
832 .unwrap();
833 assert!(
834 matches!(first, StreamResponse::StatusUpdate(ref ev) if ev.status.state == TaskState::Working)
835 );
836
837 let second = tokio::time::timeout(TEST_TIMEOUT, stream.next())
838 .await
839 .expect("timed out")
840 .unwrap()
841 .unwrap();
842 assert!(
843 matches!(second, StreamResponse::StatusUpdate(ref ev) if ev.status.state == TaskState::Completed)
844 );
845
846 let end = tokio::time::timeout(TEST_TIMEOUT, stream.next())
847 .await
848 .expect("timed out");
849 assert!(end.is_none());
850 }
851}