rama_http/service/web/endpoint/response/
sse.rs1use rama_core::error::BoxError;
4use rama_core::futures::Stream;
5use rama_core::futures::TryStream;
6use rama_http_headers::{CacheControl, Connection, ContentType};
7use rama_http_types::{
8 Body, Response,
9 sse::{
10 Event, EventDataWrite,
11 server::{KeepAlive, KeepAliveStream, SseResponseBody},
12 },
13};
14
15use super::{Headers, IntoResponse};
16
17#[must_use]
19#[derive(Debug, Clone)]
20pub struct Sse<S> {
21 stream: S,
22}
23
24impl<S> Sse<S> {
25 pub fn new<T>(stream: S) -> Self
28 where
29 S: TryStream<Ok = Event<T>> + Send + 'static,
30 S::Error: Into<BoxError>,
31 T: EventDataWrite,
32 {
33 Self { stream }
34 }
35
36 pub fn with_keep_alive<T, E>(self, keep_alive: KeepAlive<T>) -> Sse<KeepAliveStream<S, T>>
40 where
41 S: Stream<Item = Result<Event<T>, E>>,
42 E: Into<BoxError>,
43 T: EventDataWrite,
44 {
45 Sse {
46 stream: KeepAliveStream::new(keep_alive, self.stream),
47 }
48 }
49}
50
51impl<S, E, T> IntoResponse for Sse<S>
52where
53 S: Stream<Item = Result<Event<T>, E>> + Send + 'static,
54 E: Into<BoxError>,
55 T: EventDataWrite,
56{
57 fn into_response(self) -> Response {
58 (
59 Headers((
60 CacheControl::default().with_no_cache(),
61 ContentType::text_event_stream(),
62 Connection::keep_alive(),
64 )),
65 Body::new(SseResponseBody::new(self.stream)),
66 )
67 .into_response()
68 }
69}
70
71#[cfg(test)]
72mod tests {
73 use super::*;
74 use crate::service::{client::HttpClientExt as _, web::Router};
75 use ahash::{HashMap, HashMapExt as _};
76 use rama_core::futures::stream;
77 use rama_core::stream::StreamExt as _;
78 use rama_core::{Service as _, combinators::Either};
79 use rama_http_types::sse::JsonEventData;
80 use rama_utils::str::smol_str::SmolStr;
81 use std::{convert::Infallible, time::Duration};
82
83 #[tokio::test]
84 async fn basic() {
85 let client = Router::new()
86 .with_get("/", async || {
87 let stream = stream::iter(vec![
88 Event::default()
89 .with_data(Either::A("one"))
90 .try_with_comment(SmolStr::new_static("this is a comment"))
91 .unwrap(),
92 Event::default().with_data(Either::B(JsonEventData(
93 serde_json::json!({ "foo": "bar" }),
94 ))),
95 Event::default()
96 .try_with_event(SmolStr::new_static("three"))
97 .unwrap()
98 .with_retry(30_000)
99 .try_with_id(SmolStr::new_static("unique-id"))
100 .unwrap(),
101 ])
102 .map(Ok::<_, Infallible>);
103 Sse::new(stream)
104 })
105 .boxed();
106
107 let response = client.get("http://example.com").send().await.unwrap();
108
109 assert_eq!(response.headers()["content-type"], "text/event-stream");
110 assert_eq!(response.headers()["cache-control"], "no-cache");
111
112 let mut stream = response.into_body();
113
114 let event_fields =
115 parse_event(std::str::from_utf8(&stream.chunk().await.unwrap().unwrap()).unwrap());
116 assert_eq!(&event_fields["data"], "one");
117 assert_eq!(&event_fields["comment"], "this is a comment");
118
119 let event_fields =
120 parse_event(std::str::from_utf8(&stream.chunk().await.unwrap().unwrap()).unwrap());
121 assert_eq!(&event_fields["data"], "{\"foo\":\"bar\"}");
122 assert!(!event_fields.contains_key("comment"));
123
124 let event_fields =
125 parse_event(std::str::from_utf8(&stream.chunk().await.unwrap().unwrap()).unwrap());
126 assert_eq!(&event_fields["event"], "three");
127 assert_eq!(&event_fields["retry"], "30000");
128 assert_eq!(&event_fields["id"], "unique-id");
129 assert!(!event_fields.contains_key("comment"));
130
131 assert!(stream.chunk().await.unwrap().is_none());
132 }
133
134 #[tokio::test(start_paused = true)]
135 async fn keep_alive() {
136 const DELAY: Duration = Duration::from_secs(5);
137
138 let client = Router::new()
139 .with_get("/", async || {
140 let stream = stream::repeat_with(|| Event::default().with_data("msg"))
141 .map(Ok::<_, Infallible>)
142 .throttle(DELAY);
143
144 Sse::new(stream).with_keep_alive(
145 KeepAlive::<&'static str>::new()
146 .with_interval(Duration::from_secs(1))
147 .try_with_text("keep-alive-text")
148 .unwrap(),
149 )
150 })
151 .boxed();
152
153 let mut stream = client
154 .get("http://example.com")
155 .send()
156 .await
157 .unwrap()
158 .into_body();
159
160 for _ in 0..5 {
161 let event_fields =
163 parse_event(std::str::from_utf8(&stream.chunk().await.unwrap().unwrap()).unwrap());
164 assert_eq!(&event_fields["data"], "msg");
165
166 for _ in 0..4 {
168 tokio::time::sleep(Duration::from_secs(1)).await;
169 let event_fields = parse_event(
170 std::str::from_utf8(&stream.chunk().await.unwrap().unwrap()).unwrap(),
171 );
172 assert_eq!(&event_fields["comment"], "keep-alive-text");
173 }
174 }
175 }
176
177 #[tokio::test(start_paused = true)]
178 async fn keep_alive_ends_when_the_stream_ends() {
179 const DELAY: Duration = Duration::from_secs(5);
180
181 let client = Router::new()
182 .with_get("/", async || {
183 let stream = stream::repeat_with(|| Event::default().with_data("msg"))
184 .map(Ok::<_, Infallible>)
185 .throttle(DELAY)
186 .take(2);
187
188 Sse::new(stream).with_keep_alive(
189 KeepAlive::<&'static str>::new()
190 .with_interval(Duration::from_secs(1))
191 .try_with_text("keep-alive-text")
192 .unwrap(),
193 )
194 })
195 .boxed();
196
197 let mut stream = client
198 .get("http://example.com")
199 .send()
200 .await
201 .unwrap()
202 .into_body();
203
204 let event_fields =
206 parse_event(std::str::from_utf8(&stream.chunk().await.unwrap().unwrap()).unwrap());
207 assert_eq!(&event_fields["data"], "msg");
208
209 for _ in 0..4 {
211 tokio::time::sleep(Duration::from_secs(1)).await;
212 let event_fields =
213 parse_event(std::str::from_utf8(&stream.chunk().await.unwrap().unwrap()).unwrap());
214 assert_eq!(&event_fields["comment"], "keep-alive-text");
215 }
216
217 let event_fields =
219 parse_event(std::str::from_utf8(&stream.chunk().await.unwrap().unwrap()).unwrap());
220 assert_eq!(&event_fields["data"], "msg");
221
222 assert!(stream.chunk().await.unwrap().is_none());
224 }
225
226 fn parse_event(payload: &str) -> HashMap<String, String> {
227 let mut fields = HashMap::new();
228
229 let mut lines = payload.lines().peekable();
230 while let Some(line) = lines.next() {
231 if line.is_empty() {
232 assert_eq!(None, lines.next());
233 break;
234 }
235
236 let (mut key, value) = line.split_once(':').unwrap();
237 let value = value.trim();
238 if key.is_empty() {
239 key = "comment";
240 }
241 fields.insert(key.to_owned(), value.to_owned());
242 }
243
244 fields
245 }
246}