Skip to main content

trillium_sse/
lib.rs

1//! # Trillium tools for server sent events
2//!
3//! [`sse`] builds a [`Handler`](trillium::Handler) from any [`SseHandler`], which produces a
4//! [`Stream`](futures_lite::Stream) of [`Eventable`] items per connected client. Each event is
5//! written to the client as the stream yields it.
6//!
7//! Only requests whose `Accept` header names `text/event-stream` are answered with an event
8//! stream; anything else — including a wildcard `Accept` or none at all — is passed through to
9//! subsequent handlers untouched.
10//!
11//! When a client goes away, the event stream is dropped promptly on every protocol, so a
12//! stream backed by a subscription can use `Drop` to unsubscribe. A client that vanishes
13//! without signalling — a killed process, a severed network — is noticed once the transport
14//! notices, which on HTTP/3 is QUIC's negotiated idle timeout.
15//!
16//! ```
17//! use broadcaster::BroadcastChannel;
18//! use std::time::Duration;
19//! use trillium::Conn;
20//! use trillium_sse::sse;
21//!
22//! let channel = BroadcastChannel::<String>::new();
23//!
24//! let handler = sse(move |_: &mut Conn| channel.clone()).with_heartbeat(Duration::from_secs(15));
25//! ```
26//!
27//! Often, you will want this stream to be something like a channel, but
28//! the specifics of that are dependent on the event fanout
29//! characteristics of your application.
30//!
31//! ## Events
32//!
33//! This crate implements [`Eventable`] for an [`Event`] type that you can
34//! use in your application, for `String`, and for `&'static str`. You can
35//! also implement [`Eventable`] for any type in your application.
36//!
37//! In addition to data events, the stream can carry comments — messages that clients ignore,
38//! sent periodically so that an idle stream is still producing traffic. See
39//! [`Event::new_comment`], or [`Sse::with_heartbeat`] to have them sent automatically whenever
40//! the stream goes quiet.
41#![forbid(unsafe_code)]
42#![deny(
43    missing_copy_implementations,
44    rustdoc::missing_crate_level_docs,
45    missing_debug_implementations,
46    nonstandard_style,
47    unused_qualifications
48)]
49#![warn(missing_docs)]
50
51#[cfg(test)]
52#[doc = include_str!("../README.md")]
53mod readme {}
54
55mod handler;
56
57pub use handler::{Sse, SseHandler, sse};
58use std::{borrow::Cow, fmt::Write, time::Duration};
59
60fn write_multiline_field(output: &mut String, prefix: &str, value: &str) {
61    for line in value.split('\n') {
62        let line = line.strip_suffix('\r').unwrap_or(line);
63        if line.is_empty() {
64            writeln!(output, "{prefix}").unwrap();
65        } else {
66            writeln!(output, "{prefix} {line}").unwrap();
67        }
68    }
69}
70
71/// Returns `None` for an event with no fields set at all, which would otherwise be written as a
72/// bare message terminator.
73pub(crate) fn encode(event: &impl Eventable) -> Option<String> {
74    let mut output = String::new();
75
76    if let Some(comment) = event.comment() {
77        write_multiline_field(&mut output, ":", comment);
78    }
79
80    if let Some(event_type) = event.event_type() {
81        writeln!(&mut output, "event: {event_type}").ok()?;
82    }
83
84    if let Some(id) = event.id() {
85        writeln!(&mut output, "id: {id}").ok()?;
86    }
87
88    if let Some(retry) = event.retry() {
89        writeln!(&mut output, "retry: {}", retry.as_millis()).ok()?;
90    }
91
92    if let Some(data) = event.data() {
93        write_multiline_field(&mut output, "data:", data);
94    }
95
96    if output.is_empty() {
97        None
98    } else {
99        writeln!(&mut output).ok()?;
100        Some(output)
101    }
102}
103
104/// A trait that allows any Unpin + Send + Sync type to act as an event.
105///
106/// For a concrete implementation of this trait, you can use [`Event`],
107/// but it is also implemented for [`String`] and [`&'static str`].
108pub trait Eventable: Unpin + Send + Sync + 'static {
109    /// return the data for this event, if any
110    ///
111    /// Returning `None` yields a message with no `data:` field. Clients dispatch no event for
112    /// such a message, so it is only useful in combination with [`comment`](Eventable::comment).
113    fn data(&self) -> Option<&str>;
114
115    /// return a comment to send alongside this event, optionally
116    ///
117    /// Comments are ignored by clients. They are chiefly used as a heartbeat, sent periodically
118    /// so that an idle event stream is still producing traffic.
119    fn comment(&self) -> Option<&str> {
120        None
121    }
122
123    /// return the event type, optionally
124    fn event_type(&self) -> Option<&str> {
125        None
126    }
127
128    /// return a unique event id, optionally
129    fn id(&self) -> Option<&str> {
130        None
131    }
132
133    /// return a reconnection time to request of the client, optionally
134    ///
135    /// Sent as a `retry:` field in milliseconds, truncated. Clients that reconnect on their own
136    /// — such as the browser `EventSource` — wait this long before doing so. Whether and how it
137    /// is honored is entirely up to the client.
138    fn retry(&self) -> Option<Duration> {
139        None
140    }
141}
142
143impl Eventable for Event {
144    fn data(&self) -> Option<&str> {
145        Event::data(self)
146    }
147
148    fn comment(&self) -> Option<&str> {
149        Event::comment(self)
150    }
151
152    fn event_type(&self) -> Option<&str> {
153        Event::event_type(self)
154    }
155
156    fn id(&self) -> Option<&str> {
157        Event::id(self)
158    }
159
160    fn retry(&self) -> Option<Duration> {
161        Event::retry(self)
162    }
163}
164
165impl Eventable for &'static str {
166    fn data(&self) -> Option<&str> {
167        Some(self)
168    }
169}
170
171impl Eventable for String {
172    fn data(&self) -> Option<&str> {
173        Some(self)
174    }
175}
176
177/// Events are a concrete implementation of the [`Eventable`] trait.
178#[derive(Debug, Clone, Eq, PartialEq, Default, fieldwork::Fieldwork)]
179#[fieldwork(get, set, get_mut, with, option_set_some, into)]
180pub struct Event {
181    /// the data for this event
182    data: Option<Cow<'static, str>>,
183    /// a comment for this event
184    comment: Option<Cow<'static, str>>,
185    /// the type for this event
186    #[field(with = with_type, set = set_type, get_mut = type_mut)]
187    event_type: Option<Cow<'static, str>>,
188    /// the id for this event
189    id: Option<Cow<'static, str>>,
190    /// reconnection time for this stream
191    #[field(copy, into = false)]
192    retry: Option<Duration>,
193}
194
195impl From<&'static str> for Event {
196    fn from(s: &'static str) -> Self {
197        Self::from(Cow::Borrowed(s))
198    }
199}
200
201impl From<String> for Event {
202    fn from(s: String) -> Self {
203        Self::from(Cow::Owned(s))
204    }
205}
206
207impl From<Cow<'static, str>> for Event {
208    fn from(data: Cow<'static, str>) -> Self {
209        Event {
210            data: Some(data),
211            ..Self::default()
212        }
213    }
214}
215
216impl Event {
217    /// builds a new [`Event`]
218    ///
219    /// by default, this event has no event type. to set an event type,
220    /// use [`Event::with_type`] or [`Event::set_type`]
221    pub fn new(data: impl Into<Cow<'static, str>>) -> Self {
222        Self::from(data.into())
223    }
224
225    /// builds a new comment-only [`Event`], with no data
226    ///
227    /// Clients ignore comments and dispatch no event for this message. Sending one periodically
228    /// keeps an otherwise idle event stream from being closed by intermediaries.
229    ///
230    /// ```
231    /// let event = trillium_sse::Event::new_comment("heartbeat");
232    /// assert_eq!(event.comment(), Some("heartbeat"));
233    /// assert_eq!(event.data(), None);
234    /// ```
235    pub fn new_comment(comment: impl Into<Cow<'static, str>>) -> Self {
236        Self {
237            comment: Some(comment.into()),
238            ..Self::default()
239        }
240    }
241}