1use std::pin::Pin;
4use std::sync::Arc;
5use std::task::{Context, Poll};
6use std::time::{Duration, Instant};
7
8use futures_core::Stream;
9use futures_util::StreamExt;
10use serde::de::DeserializeOwned;
11
12use crate::client::{collect_headers, Inner};
13use crate::error::{ApiError, Error, Problem, Result};
14
15#[derive(Debug, Clone, PartialEq, Eq)]
17pub struct Event {
18 pub id: Option<String>,
21 pub event: String,
24 pub data: String,
26 pub retry: Option<u64>,
28}
29
30impl Event {
31 pub fn json<T: DeserializeOwned>(&self) -> Result<T> {
33 serde_json::from_str(&self.data).map_err(Error::Decode)
34 }
35}
36
37#[derive(Debug, Clone, Copy, PartialEq, Eq)]
40pub enum StreamState {
41 Connecting,
44 Connected,
46 Reconnecting(u32),
48 Disconnected,
51}
52
53pub struct StreamOptions {
58 pub reconnect: bool,
60 pub max_reconnects: u32,
62 pub terminal_events: Vec<String>,
65 pub inactivity_timeout: Option<Duration>,
70 pub base_retry: Duration,
72 pub max_backoff: Duration,
74 pub stability_reset: Duration,
77 pub on_state: Option<Arc<dyn Fn(StreamState) + Send + Sync>>,
82}
83
84impl Default for StreamOptions {
85 fn default() -> Self {
86 Self {
87 reconnect: true,
88 max_reconnects: 5,
89 terminal_events: Vec::new(),
90 inactivity_timeout: None,
91 base_retry: Duration::from_millis(2_000),
92 max_backoff: Duration::from_millis(8_000),
93 stability_reset: Duration::from_millis(60_000),
94 on_state: None,
95 }
96 }
97}
98
99impl Clone for StreamOptions {
100 fn clone(&self) -> Self {
101 Self {
102 reconnect: self.reconnect,
103 max_reconnects: self.max_reconnects,
104 terminal_events: self.terminal_events.clone(),
105 inactivity_timeout: self.inactivity_timeout,
106 base_retry: self.base_retry,
107 max_backoff: self.max_backoff,
108 stability_reset: self.stability_reset,
109 on_state: self.on_state.clone(),
110 }
111 }
112}
113
114impl std::fmt::Debug for StreamOptions {
115 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
116 f.debug_struct("StreamOptions")
117 .field("reconnect", &self.reconnect)
118 .field("max_reconnects", &self.max_reconnects)
119 .field("terminal_events", &self.terminal_events)
120 .field("inactivity_timeout", &self.inactivity_timeout)
121 .field("base_retry", &self.base_retry)
122 .field("max_backoff", &self.max_backoff)
123 .field("stability_reset", &self.stability_reset)
124 .field("on_state", &self.on_state.as_ref().map(|_| "<stream state observer>"))
125 .finish()
126 }
127}
128
129pub struct EventStream {
144 inner: Pin<Box<dyn Stream<Item = Result<Event>> + Send>>,
145}
146
147impl std::fmt::Debug for EventStream {
148 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
149 f.debug_struct("EventStream").finish_non_exhaustive()
150 }
151}
152
153impl EventStream {
154 pub(crate) fn new(
155 client: Arc<Inner>,
156 url: Result<url::Url>,
157 headers: Vec<(String, String)>,
158 options: StreamOptions,
159 ) -> Self {
160 let on_state = options.on_state.clone();
161 let stream = async_stream::try_stream! {
162 let url = url?;
163 let mut last_event_id: Option<String> = None;
164 let mut attempt: u32 = 0;
165 let mut base_retry = options.base_retry;
166
167 if let Some(cb) = &on_state {
168 cb(StreamState::Connecting);
169 }
170
171 'reconnect: loop {
172 if attempt > 0 {
176 if let Some(cb) = &on_state {
177 cb(StreamState::Reconnecting(attempt));
178 }
179 tokio::time::sleep(stream_backoff(attempt, base_retry, options.max_backoff)).await;
180 }
181
182 let mut builder = client
183 .http
184 .request(reqwest::Method::GET, url.clone())
185 .header(reqwest::header::ACCEPT, "text/event-stream")
186 .header(reqwest::header::USER_AGENT, &client.user_agent)
187 .headers(client.default_headers.clone());
188 if !client.api_key.is_empty() {
192 builder = builder.header(
193 reqwest::header::AUTHORIZATION,
194 format!("Bearer {}", client.api_key),
195 );
196 }
197 let resumed = &last_event_id;
202 for (name, value) in &headers {
203 if resumed.is_some() && name.eq_ignore_ascii_case("Last-Event-ID") {
204 continue;
205 }
206 builder = builder.header(name.as_str(), value);
207 }
208 if let Some(id) = resumed {
209 builder = builder.header("Last-Event-ID", id);
210 }
211
212 let response = match builder.send().await {
213 Ok(response) => response,
214 Err(err) => {
215 if !options.reconnect || attempt >= options.max_reconnects {
218 Err(if err.is_timeout() { Error::Timeout } else { Error::Connection(err) })?;
219 }
220 attempt += 1;
221 continue 'reconnect;
222 }
223 };
224
225 if !response.status().is_success() {
226 let status = response.status().as_u16();
227 if status == 401 || !options.reconnect || attempt >= options.max_reconnects {
232 let response_headers = collect_headers(response.headers());
233 let problem = response
234 .bytes()
235 .await
236 .ok()
237 .and_then(|bytes| serde_json::from_slice::<Problem>(&bytes).ok())
238 .unwrap_or_default();
239 Err(Error::from(ApiError { status, problem, headers: response_headers }))?;
240 }
241 attempt += 1;
242 continue 'reconnect;
243 }
244
245 if let Some(cb) = &on_state {
246 cb(StreamState::Connected);
247 }
248
249 let mut delivered = false;
253 let mut terminal = false;
254 let mut parser = Parser::default();
255 let mut body = response.bytes_stream();
256 let connected_at = Instant::now();
257
258 'read: loop {
259 let next = if let Some(timeout) = options.inactivity_timeout {
266 match tokio::time::timeout(timeout, body.next()).await {
267 Ok(inner) => inner,
268 Err(_elapsed) => {
269 if !options.reconnect || attempt >= options.max_reconnects {
270 break 'read;
271 }
272 attempt += 1;
273 continue 'reconnect;
274 }
275 }
276 } else {
277 body.next().await
278 };
279
280 let chunk = match next {
281 None => break 'read, Some(Err(_err)) => {
283 if !options.reconnect || attempt >= options.max_reconnects {
288 break 'read;
289 }
290 attempt += 1;
291 continue 'reconnect;
292 }
293 Some(Ok(chunk)) => chunk,
294 };
295
296 if attempt > 0 && connected_at.elapsed() >= options.stability_reset {
300 attempt = 0;
301 }
302
303 let mut hit_terminal = false;
304 for event in parser.push(&chunk) {
305 if event.id.is_some() {
306 last_event_id.clone_from(&event.id);
307 }
308 if let Some(retry) = event.retry.filter(|&retry| retry > 0) {
309 base_retry = Duration::from_millis(retry);
310 }
311 delivered = true;
312 let is_terminal_event = options.terminal_events.iter().any(|name| name == &event.event);
313 yield event;
314 if is_terminal_event {
315 hit_terminal = true;
316 break;
317 }
318 }
319 if hit_terminal || parser.is_done() {
323 terminal = true;
324 break 'read;
325 }
326 }
327
328 if !terminal {
333 if let Some(event) = parser.finish() {
334 if event.id.is_some() {
335 last_event_id.clone_from(&event.id);
336 }
337 let is_terminal_event = options.terminal_events.iter().any(|name| name == &event.event);
338 delivered = true;
339 yield event;
340 if is_terminal_event || parser.is_done() {
341 terminal = true;
342 }
343 }
344 }
345
346 if terminal {
347 if let Some(cb) = &on_state {
348 cb(StreamState::Disconnected);
349 }
350 break 'reconnect;
351 }
352
353 if delivered {
356 attempt = 0;
357 }
358 if !options.reconnect || attempt >= options.max_reconnects {
359 if let Some(cb) = &on_state {
360 cb(StreamState::Disconnected);
361 }
362 break 'reconnect;
363 }
364 attempt += 1;
365 }
366 };
367 Self {
368 inner: Box::pin(stream),
369 }
370 }
371}
372
373impl Stream for EventStream {
374 type Item = Result<Event>;
375
376 fn poll_next(mut self: Pin<&mut Self>, cx: &mut Context<'_>) -> Poll<Option<Self::Item>> {
377 self.inner.as_mut().poll_next(cx)
378 }
379}
380
381fn stream_backoff(attempt: u32, base: Duration, max: Duration) -> Duration {
386 let base_ms = base.as_millis() as u64;
387 let max_ms = max.as_millis() as u64;
388 let shift = attempt.saturating_sub(1).min(31);
389 let exponential = base_ms.saturating_mul(1u64 << shift);
390 let max_sleep = max_ms.min(exponential);
391 let half = (max_sleep / 2).max(1);
392 let jitter = (uuid::Uuid::new_v4().as_u128() as u64) % (half + 1);
395 Duration::from_millis(half + jitter)
396}
397
398#[derive(Default)]
411pub(crate) struct Parser {
412 buffer: Vec<u8>,
413 data: Vec<String>,
414 event: String,
415 id: Option<String>,
416 retry: Option<u64>,
417 has_fields: bool,
418 done: bool,
419}
420
421impl Parser {
422 pub(crate) fn is_done(&self) -> bool {
425 self.done
426 }
427
428 pub(crate) fn push(&mut self, chunk: &[u8]) -> Vec<Event> {
429 self.buffer.extend_from_slice(chunk);
430 let mut events = Vec::new();
431 while let Some(index) = self.buffer.iter().position(|byte| *byte == b'\n') {
432 let mut line = self.buffer.drain(..=index).collect::<Vec<u8>>();
433 line.pop(); if line.last() == Some(&b'\r') {
435 line.pop();
436 }
437 if let Some(event) = self.feed(&String::from_utf8_lossy(&line)) {
438 events.push(event);
439 }
440 if self.done {
441 self.buffer.clear();
444 break;
445 }
446 }
447 events
448 }
449
450 pub(crate) fn finish(&mut self) -> Option<Event> {
452 if self.done {
453 return None;
454 }
455 if !self.buffer.is_empty() {
456 let line = std::mem::take(&mut self.buffer);
457 self.feed(&String::from_utf8_lossy(&line));
458 }
459 self.dispatch()
460 }
461
462 fn feed(&mut self, line: &str) -> Option<Event> {
463 if line.is_empty() {
464 return self.dispatch();
465 }
466
467 if let Some(rest) = line.strip_prefix(':') {
471 let body = rest.trim();
472 if body.starts_with('{') {
473 return Some(self.inline_event(body));
474 }
475 return None;
476 }
477
478 if line.starts_with('{') {
480 return Some(self.inline_event(line));
481 }
482
483 let (field, value) = match line.find(':') {
484 Some(index) => (
485 &line[..index],
486 line[index + 1..]
487 .strip_prefix(' ')
488 .unwrap_or(&line[index + 1..]),
489 ),
490 None => (line, ""),
491 };
492 self.has_fields = true;
493 match field {
494 "event" => self.event = value.to_string(),
495 "data" => {
496 if value == "[DONE]" {
497 self.done = true;
498 return if !self.data.is_empty() || !self.event.is_empty() || self.id.is_some() {
502 self.dispatch()
503 } else {
504 None
505 };
506 }
507 self.data.push(value.to_string());
508 }
509 "id" => {
510 if !value.contains('\0') {
511 self.id = Some(value.to_string());
512 }
513 }
514 "retry" => self.retry = value.parse().ok(),
515 _ => {} }
517 None
518 }
519
520 fn dispatch(&mut self) -> Option<Event> {
521 if !self.has_fields {
522 return None;
523 }
524 let joined = self.data.join("\n");
525 if joined.is_empty() {
528 self.reset();
529 return None;
530 }
531 let resolved = if !self.event.is_empty() {
532 std::mem::take(&mut self.event)
533 } else {
534 extract_event_type(&joined).unwrap_or_else(|| "message".to_string())
535 };
536 let event = Event {
537 id: self.id.clone(),
538 event: resolved,
539 data: joined,
540 retry: self.retry.take(),
541 };
542 self.reset();
543 Some(event)
544 }
545
546 fn inline_event(&self, body: &str) -> Event {
549 Event {
550 id: extract_field(body, "event_id"),
551 event: extract_event_type(body).unwrap_or_else(|| "message".to_string()),
552 data: body.to_string(),
553 retry: None,
554 }
555 }
556
557 fn reset(&mut self) {
558 self.data.clear();
559 self.event.clear();
560 self.id = None;
561 self.retry = None;
562 self.has_fields = false;
563 }
568}
569
570fn extract_field(json: &str, field: &str) -> Option<String> {
575 let mut needle = String::from("\"");
576 needle.push_str(field);
577 needle.push('"');
578 let start = json.find(&needle)?;
579 let bytes = json.as_bytes();
580 let mut i = start + needle.len();
581 while i < bytes.len() && (bytes[i] == b':' || bytes[i] == b' ') {
582 i += 1;
583 }
584 if i >= bytes.len() || bytes[i] != b'"' {
585 return None;
586 }
587 i += 1;
588 let value_start = i;
589 while i < bytes.len() {
590 match bytes[i] {
591 b'\\' => {
592 i += 2;
593 continue;
594 }
595 b'"' => break,
596 _ => i += 1,
597 }
598 }
599 if i <= value_start {
600 return None;
601 }
602 Some(json[value_start..i].to_string())
603}
604
605fn extract_event_type(json: &str) -> Option<String> {
607 extract_field(json, "type")
608}
609
610#[cfg(test)]
611mod tests {
612 use super::*;
613
614 #[test]
615 fn parses_a_simple_frame() {
616 let mut parser = Parser::default();
617 let events = parser.push(b"event: run.started\ndata: {\"run_id\":\"r1\"}\n\n");
618 assert_eq!(events.len(), 1);
619 assert_eq!(events[0].event, "run.started");
620 assert_eq!(events[0].data, "{\"run_id\":\"r1\"}");
621 }
622
623 #[test]
624 fn joins_multi_line_data_and_defaults_the_name() {
625 let mut parser = Parser::default();
626 let events = parser.push(b"data: one\ndata: two\n\n");
627 assert_eq!(events[0].event, "message");
628 assert_eq!(events[0].data, "one\ntwo");
629 }
630
631 #[test]
632 fn survives_chunk_boundaries_and_crlf() {
633 let mut parser = Parser::default();
634 assert!(parser.push(b"event: par").is_empty());
635 assert!(parser.push(b"tial\r\ndata: {\"a\":").is_empty());
636 let events = parser.push(b"1}\r\n\r\n");
637 assert_eq!(events.len(), 1);
638 assert_eq!(events[0].event, "partial");
639 assert_eq!(events[0].data, "{\"a\":1}");
640 }
641
642 #[test]
643 fn ignores_comments_and_unknown_fields() {
644 let mut parser = Parser::default();
645 let events = parser.push(b": keep-alive\nfoo: bar\ndata: hello\n\n");
646 assert_eq!(events.len(), 1);
647 assert_eq!(events[0].data, "hello");
648 }
649
650 #[test]
651 fn keeps_the_id_field() {
652 let mut parser = Parser::default();
653 let events = parser.push(b"id: 42\ndata: x\n\n");
654 assert_eq!(events[0].id.as_deref(), Some("42"));
655 }
656
657 #[test]
658 fn decodes_a_comment_json_frame() {
659 let mut parser = Parser::default();
660 let events = parser.push(b":{\"type\":\"progress\",\"event_id\":\"2\",\"pct\":10}\n");
661 assert_eq!(events.len(), 1);
662 assert_eq!(events[0].event, "progress");
663 assert_eq!(events[0].id.as_deref(), Some("2"));
664 assert_eq!(events[0].data, "{\"type\":\"progress\",\"event_id\":\"2\",\"pct\":10}");
665 }
666
667 #[test]
668 fn decodes_a_bare_ndjson_line() {
669 let mut parser = Parser::default();
670 let events = parser.push(b"{\"type\":\"progress\",\"event_id\":\"3\",\"pct\":20}\n");
671 assert_eq!(events.len(), 1);
672 assert_eq!(events[0].event, "progress");
673 assert_eq!(events[0].id.as_deref(), Some("3"));
674 }
675
676 #[test]
677 fn done_frame_terminates_without_itself_emitting() {
678 let mut parser = Parser::default();
679 let events = parser.push(b"data: {\"text\":\"hi\"}\n\ndata: [DONE]\n\n");
680 assert_eq!(events.len(), 1, "the [DONE] frame emits nothing on its own");
681 assert_eq!(events[0].data, "{\"text\":\"hi\"}");
682 assert!(parser.is_done());
683 }
684
685 #[test]
686 fn an_id_or_retry_only_frame_emits_nothing() {
687 let mut parser = Parser::default();
688 let events = parser.push(b"id: 99\nretry: 500\n\ndata: later\n\n");
689 assert_eq!(events.len(), 1, "the id/retry-only frame must not emit");
690 assert_eq!(events[0].data, "later");
691 assert_eq!(events[0].id.as_deref(), None, "id is per-frame: not carried from the id-only frame");
692 }
693
694 #[test]
695 fn id_is_per_frame_and_does_not_leak_across_frames() {
696 let mut parser = Parser::default();
697 let events = parser.push(b"id: 4\ndata: first\n\ndata: second\n\n");
698 assert_eq!(events.len(), 2);
699 assert_eq!(events[0].id.as_deref(), Some("4"));
700 assert_eq!(events[1].id.as_deref(), None, "the second frame had no id: line");
701 }
702
703 #[test]
704 fn done_frame_with_only_an_id_emits_nothing() {
705 let mut parser = Parser::default();
706 let events = parser.push(b"id: 7\ndata: [DONE]\n\n");
707 assert!(events.is_empty(), "no data: → nothing to emit, even with id");
708 assert!(parser.is_done());
709 }
710
711 #[derive(serde::Deserialize, PartialEq, Debug)]
712 struct ExpectedEvent {
713 id: Option<String>,
714 event: String,
715 data: String,
716 retry: Option<u64>,
717 }
718
719 #[test]
720 fn decodes_the_shared_mixed_format_fixture_to_the_locked_expected_output() {
721 let mixed = include_str!("../../../contract/sse-fixtures/mixed.txt");
727 let expected: Vec<ExpectedEvent> =
728 serde_json::from_str(include_str!("../../../contract/sse-fixtures/mixed.expected.json"))
729 .expect("expected fixture is valid JSON");
730
731 let mut parser = Parser::default();
732 let mut events = parser.push(mixed.as_bytes());
733 if let Some(trailing) = parser.finish() {
734 events.push(trailing);
735 }
736
737 assert_eq!(events.len(), expected.len(), "event count");
738 for (got, want) in events.iter().zip(expected.iter()) {
739 assert_eq!(got.id, want.id, "id mismatch");
740 assert_eq!(got.event, want.event, "event mismatch");
741 assert_eq!(got.data, want.data, "data mismatch");
742 assert_eq!(got.retry, want.retry, "retry mismatch");
743 }
744 assert!(parser.is_done(), "fixture ends with [DONE]");
745 }
746
747 #[test]
748 fn extract_field_honours_escaped_quotes() {
749 let json = r#"{"type":"x","msg":"she said \"hi\""}"#;
750 assert_eq!(extract_field(json, "type").as_deref(), Some("x"));
751 assert_eq!(extract_field(json, "msg").as_deref(), Some(r#"she said \"hi\""#));
752 }
753}