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))

(def route-table
  #{["/events" :get [(start-event-stream stream-ready)] :route-name ::events]})

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.