trillium_sse/handler.rs
1use crate::{Eventable, SseConnExt, heartbeat::WithHeartbeat};
2use futures_lite::Stream;
3use std::{
4 fmt::{self, Debug, Formatter},
5 future::Future,
6 time::Duration,
7};
8use trillium::{Conn, Handler, Info, KnownHeaderName};
9use trillium_server_common::Runtime;
10
11/// The trait that defines an event source for the [`Sse`] handler.
12///
13/// Implement this on a type that holds whatever fanout mechanism your application uses — a
14/// broadcast channel, a subscription registry — and return a per-client [`Stream`] from
15/// [`connect`](SseHandler::connect).
16///
17/// ```
18/// use broadcaster::BroadcastChannel;
19/// use trillium::Conn;
20/// use trillium_sse::{Sse, SseHandler};
21///
22/// struct Notifications {
23/// channel: BroadcastChannel<String>,
24/// }
25///
26/// impl SseHandler for Notifications {
27/// type Event = String;
28/// type EventStream = BroadcastChannel<String>;
29///
30/// async fn connect(&self, _conn: &mut Conn) -> Self::EventStream {
31/// self.channel.clone()
32/// }
33/// }
34///
35/// let handler = Sse::new(Notifications {
36/// channel: BroadcastChannel::new(),
37/// });
38/// ```
39///
40/// This trait is also implemented for any `Fn(&mut Conn) -> Stream`, for the common case
41/// where nothing needs to be awaited in order to build the stream:
42///
43/// ```
44/// use futures_lite::stream;
45/// use trillium::Conn;
46/// use trillium_sse::sse;
47///
48/// let handler = sse(|_: &mut Conn| stream::iter(["one", "two"]));
49/// ```
50pub trait SseHandler: Send + Sync + Sized + 'static {
51 /// The type yielded by this handler's [`EventStream`](SseHandler::EventStream).
52 type Event: Eventable;
53
54 /// A [`Stream`] of events to send to a connected client, built per client in
55 /// [`connect`](SseHandler::connect).
56 type EventStream: Stream<Item = Self::Event> + Unpin + Send + 'static;
57
58 /// Called once per request, to build the stream of events for that client.
59 ///
60 /// Returning `None` leaves the conn untouched, so it continues on to subsequent handlers.
61 /// The conn is borrowed mutably to allow setting response headers or state, but note that
62 /// [`Sse`] sets the status, headers, and body itself when a stream is returned.
63 fn connect(&self, conn: &mut Conn) -> impl Future<Output = Self::EventStream> + Send;
64}
65
66impl<F, S, E> SseHandler for F
67where
68 F: Fn(&mut Conn) -> S + Send + Sync + 'static,
69 S: Stream<Item = E> + Unpin + Send + 'static,
70 E: Eventable,
71{
72 type Event = E;
73 type EventStream = S;
74
75 async fn connect(&self, conn: &mut Conn) -> Self::EventStream {
76 self(conn)
77 }
78}
79
80/// A [`Handler`] that responds to requests with a server-sent event stream.
81///
82/// Build one from any [`SseHandler`] with [`Sse::new`] or [`sse`]. Unlike
83/// [`SseConnExt::with_sse_stream`], this can send heartbeat comments, because it obtains a
84/// runtime from the server at startup.
85///
86/// The conn is passed through untouched — continuing on to subsequent handlers — if the request's
87/// `Accept` header excludes `text/event-stream`, or if [`SseHandler::connect`] returns `None`. A
88/// request with no `Accept` header accepts anything, per [RFC 9110 §12.5.1][rfc].
89///
90/// [rfc]: https://www.rfc-editor.org/rfc/rfc9110.html#name-accept
91pub struct Sse<H> {
92 handler: H,
93 heartbeat: Option<Duration>,
94 runtime: Option<Runtime>,
95}
96
97/// Builds a new [`Sse`] handler. Alias for [`Sse::new`].
98pub fn sse<H: SseHandler>(sse_handler: H) -> Sse<H> {
99 Sse::new(sse_handler)
100}
101
102impl<H: SseHandler> Sse<H> {
103 /// Builds a new [`Sse`] handler from any [`SseHandler`].
104 pub fn new(handler: H) -> Self {
105 Self {
106 handler,
107 heartbeat: None,
108 runtime: None,
109 }
110 }
111
112 /// Sends an empty comment whenever `heartbeat` elapses without the stream yielding an event.
113 ///
114 /// The interval is measured from the most recent event, not from the start of the response,
115 /// so a busy stream sends no heartbeats at all. Clients discard the comment.
116 ///
117 /// This is a contract about what the server sends, not a guarantee about the connection.
118 /// Regular traffic makes an idle stream less likely to be dropped by an intermediary, and
119 /// makes the server notice a departed client sooner — it only learns of one when it next
120 /// writes — but neither is assured.
121 pub fn with_heartbeat(mut self, heartbeat: Duration) -> Self {
122 self.heartbeat = Some(heartbeat);
123 self
124 }
125}
126
127/// Whether the client is willing to receive `text/event-stream`.
128///
129/// A request with no `Accept` header accepts anything. An otherwise-matching media range with
130/// `q=0` is a refusal. Preference ordering is not considered, as there is only one media type on
131/// offer — the question is acceptability, not which of several to send.
132fn accepts_event_stream(conn: &Conn) -> bool {
133 let Some(accept) = conn.request_headers().get_str(KnownHeaderName::Accept) else {
134 return true;
135 };
136
137 accept.split(',').any(|media_range| {
138 let mut parts = media_range.split(';').map(str::trim);
139
140 let matches_range = parts.next().is_some_and(|range| {
141 ["text/event-stream", "text/*", "*/*"]
142 .iter()
143 .any(|acceptable| range.eq_ignore_ascii_case(acceptable))
144 });
145
146 matches_range
147 && !parts.any(|parameter| {
148 parameter
149 .strip_prefix("q=")
150 .and_then(|q| q.parse::<f32>().ok())
151 .is_some_and(|q| q <= 0.0)
152 })
153 })
154}
155
156impl<H: SseHandler> Handler for Sse<H> {
157 async fn run(&self, mut conn: Conn) -> Conn {
158 if !accepts_event_stream(&conn) {
159 return conn;
160 }
161
162 let stream = self.handler.connect(&mut conn).await;
163
164 match (self.heartbeat, &self.runtime) {
165 (Some(heartbeat), Some(runtime)) => {
166 conn.with_sse_stream(WithHeartbeat::new(stream, runtime.clone(), heartbeat))
167 }
168 _ => conn.with_sse_stream(stream),
169 }
170 }
171
172 async fn init(&mut self, info: &mut Info) {
173 self.runtime = info.shared_state::<Runtime>().cloned();
174
175 if self.heartbeat.is_some() && self.runtime.is_none() {
176 log::warn!(
177 "no runtime in shared state; sse heartbeats are disabled. this handler was \
178 probably not initialized by a trillium runtime adapter."
179 );
180 }
181 }
182}
183
184impl<H> Debug for Sse<H>
185where
186 H: Debug,
187{
188 fn fmt(&self, f: &mut Formatter<'_>) -> fmt::Result {
189 f.debug_struct("Sse")
190 .field("handler", &self.handler)
191 .field("heartbeat", &self.heartbeat)
192 .field("runtime", &self.runtime)
193 .finish()
194 }
195}