Skip to main content

sse_core/
stream.rs

1use alloc::sync::Arc;
2use core::{
3    fmt,
4    pin::Pin,
5    str,
6    task::{self, ready, Poll},
7};
8use thiserror::Error;
9
10use bytes::Buf;
11use futures_core::{
12    stream::{FusedStream, Stream},
13    TryStream,
14};
15use pin_project_lite::pin_project;
16
17use crate::{PayloadTooLargeError, SseDecoder, SseEvent};
18
19pin_project! {
20    /// An asynchronous stream wrapper that parses SSE events from an underlying byte stream.
21    #[derive(Clone)]
22    pub struct SseStream<T: TryStream> {
23        #[pin]
24        inner: Option<T>,
25        buf: Option<T::Ok>,
26
27        decoder: SseDecoder,
28    }
29}
30
31impl<T: TryStream> SseStream<T> {
32    /// Creates a new, disconnected [`SseStream`].
33    ///
34    /// A disconnected stream will immediately yield [`None`] (terminated) if polled.
35    /// This constructor is primarily useful when you need to store the [`SseStream`]
36    /// inside a struct before the network connection is established.
37    ///
38    /// To make the stream active, you must attach an inner stream using
39    /// [`attach()`](Self::attach).
40    ///
41    /// # Example
42    /// ```
43    /// # use futures_core::stream::Stream;
44    /// # use sse_core::*;
45    /// # async fn fetch_http_stream() -> impl Stream<Item = Result<&'static [u8], ()>> {
46    /// #     tokio_test::stream_mock::StreamMockBuilder::new().build()
47    /// # }
48    /// # tokio_test::block_on(async {
49    /// let mut stream = SseStream::disconnected();
50    ///
51    /// // ... later, when the network is ready:
52    /// let byte_stream = fetch_http_stream().await;
53    /// stream.attach(byte_stream);
54    /// # })
55    /// ```
56    #[inline]
57    #[must_use]
58    pub fn disconnected() -> Self {
59        Self::with_decoder(SseDecoder::new())
60    }
61
62    /// Creates a disconnected stream initialized with a custom decoder.
63    ///
64    /// See the [`disconnected()`](Self::disconnected) function for more information.
65    #[inline]
66    #[must_use]
67    pub fn with_decoder(decoder: SseDecoder) -> Self {
68        Self {
69            inner: None,
70            buf: None,
71            decoder,
72        }
73    }
74
75    /// Creates a new [`SseStream`] wrapping the provided inner stream.
76    #[inline]
77    #[must_use]
78    pub fn new(inner: T) -> Self {
79        let mut slf = Self::disconnected();
80        slf.inner = Some(inner);
81        slf
82    }
83
84    /// Consumes the stream and returns the underlying state-machine decoder.
85    #[inline]
86    pub fn take_decoder(self) -> SseDecoder {
87        let Self { mut decoder, .. } = self;
88        decoder.reconnect();
89        decoder
90    }
91
92    /// Returns `true` if the stream is currently disconnected.
93    #[inline]
94    #[must_use]
95    pub fn is_closed(&self) -> bool {
96        self.inner.is_none()
97    }
98
99    /// Returns the current `Last-Event-ID` parsed by the underlying decoder.
100    #[inline]
101    #[must_use]
102    pub fn last_event_id(&self) -> Option<&Arc<str>> {
103        self.decoder.last_event_id()
104    }
105
106    /// Disconnects the inner stream while retaining the underlying parser's state.
107    ///
108    /// This drops the active network connection but safely preserves the most
109    /// recently parsed `Last-Event-ID` within the decoder. This is the standard
110    /// method to temporarily pause a stream or handle a dropped connection,
111    /// allowing you to later resume exactly where you left off.
112    ///
113    /// * To close the stream and **inject** a new ID for the next connection, use [`close_with_id()`](Self::close_with_id).
114    /// * To close the stream and completely **wipe** the session state, use [`close_and_clear()`](Self::close_and_clear).
115    #[inline]
116    pub fn close(&mut self) {
117        self.decoder.reconnect();
118        self.clear_bufs();
119    }
120
121    /// Disconnects the stream and completely purges the underlying parser's state.
122    ///
123    /// This drops the inner stream, clears all internal byte buffers, and
124    /// permanently drops the currently tracked `Last-Event-ID`. It effectively
125    /// returns the `SseStream` to the exact state it was in when initially
126    /// created via [`disconnected()`](Self::disconnected).
127    ///
128    /// * To close the stream and **keep** the current ID, use [`close()`](Self::close).
129    /// * To close the stream and **inject** a new ID, use [`close_with_id()`](Self::close_with_id).
130    #[inline]
131    pub fn close_and_clear(&mut self) {
132        self.decoder.clear();
133        self.clear_bufs();
134    }
135
136    /// Disconnects the inner stream and explicitly overrides the underlying
137    /// decoder's `Last-Event-ID` in preparation for a future connection.
138    ///
139    /// This is particularly useful in async contexts where you must drop the
140    /// active stream, inject a new ID, and then yield back to the runtime before
141    /// establishing a new network connection. The injected ID will be available
142    /// immediately via [`last_event_id()`](Self::last_event_id).
143    ///
144    /// * To close the stream and **keep** the current ID, use [`close()`](Self::close).
145    /// * To close the stream and completely **wipe** the session state, use [`close_and_clear()`](Self::close_and_clear).
146    #[inline]
147    pub fn close_with_id(&mut self, id: Option<Arc<str>>) {
148        self.decoder.reconnect_with_id(id);
149        self.clear_bufs();
150    }
151
152    /// Attaches a new inner stream to resume processing events.
153    ///
154    /// This method resets the underlying parser's buffers but safely retains the most
155    /// recently parsed `Last-Event-ID`. It is the standard way to recover from
156    /// a dropped network connection, allowing you to resume exactly where you left off.
157    ///
158    /// * To attach a stream and **inject** a new ID, use [`attach_with_id()`](Self::attach_with_id).
159    /// * To attach a stream and completely **wipe** the session state, use [`clear_and_attach()`](Self::clear_and_attach).
160    #[inline]
161    pub fn attach(&mut self, inner: T) {
162        self.close();
163        self.inner = Some(inner);
164    }
165
166    /// Attaches a new inner stream and completely purges the underlying parser's state.
167    ///
168    /// This method is used when you want to reuse an existing `SseStream` allocation
169    /// for a completely fresh connection or a different server. It clears all internal
170    /// byte buffers and permanently drops the currently tracked `Last-Event-ID`.
171    ///
172    /// * To attach a stream and **keep** the current ID, use [`attach()`](Self::attach).
173    /// * To attach a stream and **inject** a new ID, use [`attach_with_id()`](Self::attach_with_id).
174    #[inline]
175    pub fn clear_and_attach(&mut self, inner: T) {
176        self.close_and_clear();
177        self.inner = Some(inner);
178    }
179
180    /// Attaches a new inner stream to resume processing, explicitly overriding
181    /// the `Last-Event-ID` in the underlying decoder.
182    ///
183    /// This method is primarily used when recovering an offline session where
184    /// you need to initialize the stream with a saved ID (e.g., from a local database)
185    /// right as you provide the new HTTP response stream.
186    ///
187    /// * To attach a stream and **keep** the current ID, use [`attach()`](Self::attach).
188    /// * To attach a stream and completely **wipe** the session state, use [`clear_and_attach()`](Self::clear_and_attach).
189    #[inline]
190    pub fn attach_with_id(&mut self, inner: T, id: Option<Arc<str>>) {
191        self.close_with_id(id);
192        self.inner = Some(inner);
193    }
194
195    #[inline]
196    fn clear_bufs(&mut self) {
197        self.inner = None;
198        self.buf = None;
199    }
200}
201
202/// Equivalent to [`SseStream::disconnected()`].
203impl<T: TryStream> Default for SseStream<T> {
204    #[inline]
205    fn default() -> Self {
206        Self::disconnected()
207    }
208}
209
210impl<T: TryStream> fmt::Debug for SseStream<T> {
211    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
212        f.debug_struct("SseStream")
213            .field("is_closed", &self.is_closed())
214            .field("decoder", &self.decoder)
215            .finish_non_exhaustive()
216    }
217}
218
219/// An alias for [`Result`] with the error set to [`SseStreamError<E>`].
220pub type SseStreamResult<T, E> = Result<T, SseStreamError<E>>;
221
222/// Errors that can occur while reading from an [`SseStream`].
223#[derive(Debug, Clone, PartialEq, Eq, Error)]
224#[cfg_attr(feature = "serde", derive(serde::Deserialize, serde::Serialize))]
225pub enum SseStreamError<T> {
226    /// A single field (e.g., data or Last-Event-ID) exceeded the configured byte limit.
227    #[error("{0}")]
228    PayloadTooLarge(PayloadTooLargeError),
229    /// An error propagated from the inner [`TryStream`].
230    #[error("{0}")]
231    Inner(#[from] T),
232}
233
234impl<T: TryStream> Stream for SseStream<T>
235where
236    T::Ok: Buf,
237{
238    type Item = SseStreamResult<SseEvent, T::Error>;
239
240    fn poll_next(self: Pin<&mut Self>, cx: &mut task::Context<'_>) -> Poll<Option<Self::Item>> {
241        let mut slf = self.project();
242
243        let Some(mut inner) = slf.inner.as_mut().as_pin_mut() else {
244            return Poll::Ready(None);
245        };
246
247        loop {
248            if let Some(event) = (slf.buf.as_mut())
249                .and_then(|buf| slf.decoder.next(buf))
250                .transpose()
251                .map_err(SseStreamError::PayloadTooLarge)?
252            {
253                return Poll::Ready(Some(Ok(event)));
254            };
255
256            *slf.buf = ready!(inner.as_mut().try_poll_next(cx)?);
257            if slf.buf.is_none() {
258                slf.inner.set(None);
259                return Poll::Ready(None);
260            }
261        }
262    }
263}
264
265impl<T: TryStream> FusedStream for SseStream<T>
266where
267    T::Ok: Buf,
268{
269    fn is_terminated(&self) -> bool {
270        self.is_closed()
271    }
272}
273
274#[test]
275fn hard_parse() -> Result<(), PayloadTooLargeError> {
276    use crate::MessageEvent;
277    use std::slice;
278    use tokio_stream::StreamExt;
279
280    tokio_test::block_on(async {
281        // Source: https://github.com/jpopesculian/eventsource-stream/blob/v0.2.3/tests/eventsource-stream.rs
282        let bytes = "
283
284:
285
286event: my-event\r
287data:line1
288data: line2
289:
290id: my-id
291:should be ignored too\rretry:42
292retry:
293
294data:second
295
296";
297
298        let mut inner = tokio_test::stream_mock::StreamMockBuilder::new();
299        for b in bytes.as_bytes() {
300            inner = inner.next(Ok(slice::from_ref(b)));
301        }
302        inner = inner
303            .next(Err(()))
304            .next(Ok(b"data: hello\n\ndata:ignored\n"));
305
306        let id = Some("my-id".into());
307
308        let mut stream = SseStream::new(inner.build());
309        let events: Vec<_> = (&mut stream).collect().await;
310
311        assert_eq!(
312            events,
313            &[
314                Ok(SseEvent::Retry(42)),
315                Ok(SseEvent::Message(MessageEvent {
316                    event: "my-event".into(),
317                    data: "line1\nline2".into(),
318                    last_event_id: id.clone()
319                })),
320                Ok(SseEvent::Message(MessageEvent {
321                    event: "message".into(),
322                    data: "second".into(),
323                    last_event_id: id.clone()
324                })),
325                Err(SseStreamError::Inner(())),
326                Ok(SseEvent::Message(MessageEvent {
327                    event: "message".into(),
328                    data: "hello".into(),
329                    last_event_id: id.clone()
330                })),
331            ]
332        );
333
334        assert!(stream.is_closed());
335
336        Ok(())
337    })
338}