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}