1use crate::response::Response;
34use crate::upgrade::Upgraded;
35use rustlavel_core::Json;
36use std::time::Duration;
37use tokio::io::AsyncWriteExt;
38use tokio::sync::mpsc;
39
40pub const KEEPALIVE: Duration = Duration::from_secs(15);
42
43#[derive(Debug, Clone, PartialEq)]
45pub struct Event {
46 pub id: Option<String>,
49 pub event: Option<String>,
52 pub data: String,
53 pub retry: Option<Duration>,
56}
57
58impl Event {
59 pub fn data(data: impl Into<String>) -> Event {
61 Event { id: None, event: None, data: data.into(), retry: None }
62 }
63
64 pub fn named(event: impl Into<String>, data: impl Into<String>) -> Event {
66 Event { id: None, event: Some(event.into()), data: data.into(), retry: None }
67 }
68
69 pub fn json(event: impl Into<String>, data: Json) -> Event {
71 Event::named(event, data.to_string())
72 }
73
74 pub fn id(mut self, id: impl Into<String>) -> Event {
75 self.id = Some(id.into());
76 self
77 }
78
79 pub fn retry(mut self, after: Duration) -> Event {
80 self.retry = Some(after);
81 self
82 }
83
84 pub fn to_bytes(&self) -> Vec<u8> {
86 let mut out = String::new();
87 if let Some(id) = &self.id {
88 out.push_str(&format!("id: {}\n", strip_newlines(id)));
89 }
90 if let Some(event) = &self.event {
91 out.push_str(&format!("event: {}\n", strip_newlines(event)));
92 }
93 if let Some(retry) = self.retry {
94 out.push_str(&format!("retry: {}\n", retry.as_millis()));
95 }
96 for line in self.data.split('\n') {
99 out.push_str("data: ");
100 out.push_str(line.trim_end_matches('\r'));
101 out.push('\n');
102 }
103 out.push('\n');
104 out.into_bytes()
105 }
106}
107
108fn strip_newlines(text: &str) -> String {
110 text.replace(['\r', '\n'], " ")
111}
112
113pub fn channel(capacity: usize) -> (mpsc::Sender<Event>, mpsc::Receiver<Event>) {
117 mpsc::channel(capacity.max(1))
118}
119
120impl Response {
121 pub fn events(events: mpsc::Receiver<Event>) -> Response {
127 let events = std::sync::Mutex::new(Some(events));
128 Response::ok()
129 .with_header("content-type", "text/event-stream")
130 .with_header("cache-control", "no-cache")
131 .with_header("connection", "close")
134 .with_header("x-accel-buffering", "no")
137 .streaming(move |connection: Upgraded| {
138 let events = events.lock().ok().and_then(|mut held| held.take());
139 async move {
140 if let Some(events) = events {
141 pump(connection, events).await;
142 }
143 }
144 })
145 }
146}
147
148async fn pump(mut connection: Upgraded, mut events: mpsc::Receiver<Event>) {
150 let mut keepalive = tokio::time::interval(KEEPALIVE);
151 keepalive.tick().await; loop {
154 let bytes = tokio::select! {
155 event = events.recv() => match event {
156 Some(event) => event.to_bytes(),
157 None => break,
159 },
160 _ = keepalive.tick() => b": keepalive\n\n".to_vec(),
161 };
162
163 if connection.writer.write_all(&bytes).await.is_err() {
166 break;
167 }
168 if connection.writer.flush().await.is_err() {
169 break;
170 }
171 }
172 let _ = connection.writer.shutdown().await;
173}
174
175#[cfg(test)]
176mod tests {
177 use super::*;
178
179 #[test]
180 fn an_event_is_written_in_the_shape_event_source_reads() {
181 let bytes = Event::named("progress", "42").id("7").retry(Duration::from_secs(2)).to_bytes();
182 assert_eq!(
183 String::from_utf8(bytes).unwrap(),
184 "id: 7\nevent: progress\nretry: 2000\ndata: 42\n\n"
185 );
186 assert_eq!(String::from_utf8(Event::data("hello").to_bytes()).unwrap(), "data: hello\n\n");
187 }
188
189 #[test]
192 fn multi_line_data_is_split_across_data_lines() {
193 let text = String::from_utf8(Event::data("line one\nline two\r\nline three").to_bytes()).unwrap();
194 assert_eq!(text, "data: line one\ndata: line two\ndata: line three\n\n");
195 }
196
197 #[test]
199 fn a_line_break_cannot_be_smuggled_into_a_field() {
200 let text = String::from_utf8(Event::named("a\nevent: b", "x").id("1\n2").to_bytes()).unwrap();
201 assert_eq!(text.lines().filter(|l| l.starts_with("event:")).count(), 1, "{text}");
203 assert_eq!(text.lines().filter(|l| l.starts_with("id:")).count(), 1, "{text}");
204 assert!(text.starts_with("id: 1 2\nevent: a event: b\n"), "{text}");
205 }
206
207 #[test]
208 fn json_events_carry_the_document_on_one_line() {
209 let text = String::from_utf8(
210 Event::json("progress", Json::object([("percent", Json::from(50))])).to_bytes(),
211 )
212 .unwrap();
213 assert_eq!(text, "event: progress\ndata: {\"percent\":50}\n\n");
214 }
215
216 #[tokio::test]
221 async fn events_arrive_as_they_are_sent_and_the_stream_ends_with_the_sender() {
222 use crate::{Request, Router, Server};
223 use rustlavel_core::Context;
224 use tokio::io::AsyncReadExt;
225 use tokio::net::{TcpListener, TcpStream};
226
227 let (hand_over, mut take) = mpsc::channel::<mpsc::Sender<Event>>(1);
230 let hand_over = std::sync::Arc::new(hand_over);
231 let mut router = Router::new();
232 router.get("/events", move |_req: Request| {
233 let hand_over = std::sync::Arc::clone(&hand_over);
234 async move {
235 let (tx, rx) = channel(8);
236 hand_over.try_send(tx).expect("the test is waiting for the sender");
237 Response::events(rx)
238 }
239 });
240 let server = std::sync::Arc::new(Server::new(router, Context::default()));
241
242 let listener = TcpListener::bind("127.0.0.1:0").await.unwrap();
243 let addr = listener.local_addr().unwrap();
244 tokio::spawn(async move {
245 let (stream, peer) = listener.accept().await.unwrap();
246 let _ = server.serve_connection(stream, peer).await;
247 });
248
249 let mut client = TcpStream::connect(addr).await.unwrap();
250 client.write_all(b"GET /events HTTP/1.1\r\nHost: t\r\n\r\n").await.unwrap();
251
252 let mut head = vec![0u8; 512];
254 let n = client.read(&mut head).await.unwrap();
255 let head = String::from_utf8_lossy(&head[..n]).to_string();
256 assert!(head.starts_with("HTTP/1.1 200 OK"), "{head}");
257 assert!(head.contains("text/event-stream"), "{head}");
258 assert!(!head.to_ascii_lowercase().contains("content-length"), "{head}");
259
260 let tx = take.recv().await.expect("the handler ran");
261
262 for step in [10, 50, 100] {
264 tx.send(Event::json("progress", Json::object([("percent", Json::from(step))]))).await.unwrap();
265 let mut chunk = vec![0u8; 256];
266 let n = tokio::time::timeout(Duration::from_secs(2), client.read(&mut chunk))
267 .await
268 .expect("an event did not arrive within two seconds — the stream is buffering")
269 .unwrap();
270 let text = String::from_utf8_lossy(&chunk[..n]).to_string();
271 assert!(text.contains(&format!("\"percent\":{step}")), "step {step}: {text}");
272 }
273
274 drop(tx);
276 let mut rest = Vec::new();
277 tokio::time::timeout(Duration::from_secs(2), client.read_to_end(&mut rest))
278 .await
279 .expect("the connection stayed open after the sender was dropped")
280 .unwrap();
281 }
282
283 #[test]
286 fn the_response_is_a_stream_with_no_length() {
287 let (_tx, rx) = channel(4);
288 let response = Response::events(rx);
289 let head = String::from_utf8(response.to_bytes(false)).unwrap();
290
291 assert!(head.starts_with("HTTP/1.1 200 OK\r\n"), "{head}");
292 assert!(head.contains("content-type: text/event-stream\r\n"), "{head}");
293 assert!(head.contains("cache-control: no-cache\r\n"), "{head}");
294 assert!(!head.to_ascii_lowercase().contains("content-length"), "a length would end the stream: {head}");
295 assert!(response.upgrades(), "the socket is not handed over");
296 }
297}