Server-Sent Events
The Pedestal service library includes support for Server-Sent Events (SSE). The SSE protocol defines a mechanism for sending event notifications from a server to a client using a form of long polling. However, unlike conventional long polling, SSE does not send each event as a separate response, with an expectation that the client will make a new request in between each one. Rather, SSE sends all its events as part of a single response stream. The stream is kept alive over time by sending events and/or periodic heart-beat data. In the event that the stream is closed for some reason, the client can send a request to re-open it and events notifications can continue. All modern browsers have built in support for SSE via the EventSource API.
Making an SSE Interceptor
To define an endpoint that will send SSE, make a route with an
interceptor created by
start-event-stream
. Note
that start-event-stream
is not itself an interceptor, but rather a
function that returns an interceptor.
start-event-stream
requires a "ready function." The ready function
will be called later, when Pedestal has prepared the HTTP response and
informed the client that an SSE stream is starting.
Request Processing
When a request reaches the SSE interceptor, it will:
-
Pause interceptor execution
-
Send HTTP response headers to tell the client that an event stream is starting
-
Initiate a timed heartbeat to keep the connection alive
After that, it will call the ready function with two arguments: a core.async channel for the events and the current interceptor context.
Ready Function
The ready function may put events into the channel to send them to the client. Events are maps with keys :name and :data. Both take string values.
When the ready function has finished sending events, it should close the channel. Pedestal will then clean up the connection.
If a client closes its connection, Pedestal will close the event channel. The next time the ready function tries to put a message into the event channel, the put
call will return false. The ready function must detect this and clean up any resources it allocated.
Example
Here is an example that shows how an SSE event stream can be used.
(defn stream-ready [event-chan context]
(dotimes [_ 20]
(async/>!! event-chan {:name "foo" :data "bar"})
(Thread/sleep 1000))
(async/close! event-chan))
(defroutes route-table
[[["/events" {:get [::events (start-event-stream stream-ready)]}]]])
Further Interceptor Processing
When an SSE interceptor starts the event stream, it sends a partial HTTP response to the client. Any downstream interceptors can examine the context map, including the response map. But since the response has already been sent, they cannot alter it. Interceptors that attempt to alter the response (for example, by setting cookies or other headers) will log an exception indicating that the data could not be sent.