1#![forbid(unsafe_code)]
62#![deny(
63 missing_copy_implementations,
64 rustdoc::missing_crate_level_docs,
65 missing_debug_implementations,
66 nonstandard_style,
67 unused_qualifications
68)]
69#![warn(missing_docs)]
70
71#[cfg(test)]
72#[doc = include_str!("../README.md")]
73mod readme {}
74
75mod handler;
76mod heartbeat;
77
78use futures_lite::{AsyncRead, stream::Stream};
79pub use handler::{Sse, SseHandler, sse};
80use heartbeat::WithHeartbeat;
81use std::{
82 borrow::Cow,
83 fmt::Write,
84 io,
85 marker::PhantomData,
86 pin::Pin,
87 task::{Context, Poll},
88 time::Duration,
89};
90use trillium::{Body, Conn, KnownHeaderName, Status};
91use trillium_server_common::Runtime;
92
93struct SseBody<S, E> {
94 stream: S,
95 buffer: Vec<u8>,
96 event: PhantomData<E>,
97}
98
99impl<S, E> SseBody<S, E>
100where
101 S: Stream<Item = E> + Unpin + Send + 'static,
102 E: Eventable,
103{
104 pub fn new(stream: S) -> Self {
105 Self {
106 stream,
107 buffer: Vec::new(),
108 event: PhantomData,
109 }
110 }
111}
112
113fn write_multiline_field(output: &mut String, prefix: &str, value: &str) {
114 for line in value.split('\n') {
115 let line = line.strip_suffix('\r').unwrap_or(line);
116 if line.is_empty() {
117 writeln!(output, "{prefix}").unwrap();
118 } else {
119 writeln!(output, "{prefix} {line}").unwrap();
120 }
121 }
122}
123
124fn encode(event: &impl Eventable) -> Option<String> {
127 let mut output = String::new();
128
129 if let Some(comment) = event.comment() {
130 write_multiline_field(&mut output, ":", comment);
131 }
132
133 if let Some(event_type) = event.event_type() {
134 writeln!(&mut output, "event: {event_type}").ok()?;
135 }
136
137 if let Some(id) = event.id() {
138 writeln!(&mut output, "id: {id}").ok()?;
139 }
140
141 if let Some(retry) = event.retry() {
142 writeln!(&mut output, "retry: {}", retry.as_millis()).ok()?;
143 }
144
145 if let Some(data) = event.data() {
146 write_multiline_field(&mut output, "data:", data);
147 }
148
149 if output.is_empty() {
150 None
151 } else {
152 writeln!(&mut output).ok()?;
153 Some(output)
154 }
155}
156
157impl<S, E> AsyncRead for SseBody<S, E>
158where
159 S: Stream<Item = E> + Unpin + Send + 'static,
160 E: Eventable,
161{
162 fn poll_read(
163 self: Pin<&mut Self>,
164 cx: &mut Context<'_>,
165 buf: &mut [u8],
166 ) -> Poll<io::Result<usize>> {
167 let Self { buffer, stream, .. } = self.get_mut();
168
169 let buffer_read = buffer.len().min(buf.len());
170 if buffer_read > 0 {
171 buf[0..buffer_read].copy_from_slice(&buffer[0..buffer_read]);
172 buffer.drain(0..buffer_read);
173 return Poll::Ready(Ok(buffer_read));
174 }
175
176 loop {
177 break match Pin::new(&mut *stream).poll_next(cx) {
178 Poll::Pending => Poll::Pending,
179 Poll::Ready(Some(item)) => {
180 let Some(data) = encode(&item) else { continue };
181 let data = data.into_bytes();
182 let writable_len = data.len().min(buf.len());
183 buf[0..writable_len].copy_from_slice(&data[0..writable_len]);
184 if writable_len < data.len() {
185 buffer.extend_from_slice(&data[writable_len..]);
186 }
187 Poll::Ready(Ok(writable_len))
188 }
189
190 Poll::Ready(None) => Poll::Ready(Ok(0)),
191 };
192 }
193 }
194}
195
196impl<S, E> From<SseBody<S, E>> for Body
197where
198 S: Stream<Item = E> + Unpin + Send + 'static,
199 E: Eventable,
200{
201 fn from(sse_body: SseBody<S, E>) -> Self {
202 Body::new_streaming(sse_body, None)
203 }
204}
205
206pub trait SseConnExt {
208 fn with_sse_stream<S, E>(self, sse_stream: S) -> Self
215 where
216 S: Stream<Item = E> + Unpin + Send + 'static,
217 E: Eventable;
218
219 fn with_sse_stream_and_heartbeat<S, E>(self, sse_stream: S, heartbeat: Duration) -> Self
225 where
226 S: Stream<Item = E> + Unpin + Send + 'static,
227 E: Eventable;
228}
229
230impl SseConnExt for Conn {
231 fn with_sse_stream<S, E>(self, sse_stream: S) -> Self
232 where
233 S: Stream<Item = E> + Unpin + Send + 'static,
234 E: Eventable,
235 {
236 let body = SseBody::new(self.swansong().interrupt(sse_stream));
237 self.set_sse_headers().with_body(body)
238 }
239
240 fn with_sse_stream_and_heartbeat<S, E>(self, sse_stream: S, heartbeat: Duration) -> Self
241 where
242 S: Stream<Item = E> + Unpin + Send + 'static,
243 E: Eventable,
244 {
245 let Some(runtime) = self.shared_state::<Runtime>().cloned() else {
246 log::warn!(
247 "no runtime in shared state; sending sse stream without a heartbeat. this conn \
248 was probably not served by a trillium server."
249 );
250 return self.with_sse_stream(sse_stream);
251 };
252
253 let stream = WithHeartbeat::new(sse_stream, runtime, heartbeat);
254 let body = SseBody::new(self.swansong().interrupt(stream));
255 self.set_sse_headers().with_body(body)
256 }
257}
258
259trait SseHeaders {
260 fn set_sse_headers(self) -> Self;
261}
262
263impl SseHeaders for Conn {
264 fn set_sse_headers(self) -> Self {
265 self.with_response_header(KnownHeaderName::ContentType, "text/event-stream")
266 .with_response_header(KnownHeaderName::CacheControl, "no-cache")
267 .with_response_header(KnownHeaderName::Connection, "close")
271 .with_status(Status::Ok)
272 .halt()
273 }
274}
275
276pub trait Eventable: Unpin + Send + Sync + 'static {
281 fn data(&self) -> Option<&str>;
286
287 fn comment(&self) -> Option<&str> {
292 None
293 }
294
295 fn event_type(&self) -> Option<&str> {
297 None
298 }
299
300 fn id(&self) -> Option<&str> {
302 None
303 }
304
305 fn retry(&self) -> Option<Duration> {
311 None
312 }
313}
314
315impl Eventable for Event {
316 fn data(&self) -> Option<&str> {
317 Event::data(self)
318 }
319
320 fn comment(&self) -> Option<&str> {
321 Event::comment(self)
322 }
323
324 fn event_type(&self) -> Option<&str> {
325 Event::event_type(self)
326 }
327
328 fn id(&self) -> Option<&str> {
329 Event::id(self)
330 }
331
332 fn retry(&self) -> Option<Duration> {
333 Event::retry(self)
334 }
335}
336
337impl Eventable for &'static str {
338 fn data(&self) -> Option<&str> {
339 Some(self)
340 }
341}
342
343impl Eventable for String {
344 fn data(&self) -> Option<&str> {
345 Some(self)
346 }
347}
348
349#[derive(Debug, Clone, Eq, PartialEq, Default, fieldwork::Fieldwork)]
351#[fieldwork(get, set, get_mut, with, option_set_some, into)]
352pub struct Event {
353 data: Option<Cow<'static, str>>,
355 comment: Option<Cow<'static, str>>,
357 #[field(with = with_type, set = set_type, get_mut = type_mut)]
359 event_type: Option<Cow<'static, str>>,
360 id: Option<Cow<'static, str>>,
362 #[field(copy, into = false)]
364 retry: Option<Duration>,
365}
366
367impl From<&'static str> for Event {
368 fn from(s: &'static str) -> Self {
369 Self::from(Cow::Borrowed(s))
370 }
371}
372
373impl From<String> for Event {
374 fn from(s: String) -> Self {
375 Self::from(Cow::Owned(s))
376 }
377}
378
379impl From<Cow<'static, str>> for Event {
380 fn from(data: Cow<'static, str>) -> Self {
381 Event {
382 data: Some(data),
383 ..Self::default()
384 }
385 }
386}
387
388impl Event {
389 pub fn new(data: impl Into<Cow<'static, str>>) -> Self {
394 Self::from(data.into())
395 }
396
397 pub fn new_comment(comment: impl Into<Cow<'static, str>>) -> Self {
408 Self {
409 comment: Some(comment.into()),
410 ..Self::default()
411 }
412 }
413}