Skip to main content

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}