Liking cljdoc? Tell your friends :D

Server-Sent Events

Welcome

Server-sent events (SSE) make it possible to stream events from a server to a client application. They are appropriate when you only need a unidirectional flow of events from the server to the client (if you need a bidirectional connection, then WebSockets are a better choice).

What You Will Learn

After reading this guide, you will be able to:

  • Configure Pedestal to stream SSE connections.

  • Send events from Pedestal over the connection using {core_async}.

  • Connect to the server-sent event stream from a JavaScript client.

Guide Assumptions

This guide is for intermediate users who have worked through some sample applications or the first few hello-world.adoc guides. In particular, you should know how to run a build, start a REPL, and start and stop your server.

You do not need to know any other Clojure web frameworks, but if you do, you may find some of the comparisons useful. If not, that’s OK — just skip those sections.

Server-Sent Events Overview

In typical client/server interactions, the client, usually a web browser, sends a single request to the server, and the server responds with a single response.

With SSEs, this is a bit different. The client still sends a single request, and receives a single response, but it doesn’t get the response all at once. The response body "stays open", and the server can keep appending more content to it.

With SSEs, the response content type is text/event-stream. This event stream consists of multiple "events", short text-based messages.

Where We Are Going

We’ll be creating a new Pedestal project, adding a simple route to stream a counter using server-sent events, and testing it with curl.

Setting Up

Create a new project directory with a deps.edn and a source file:

$ mkdir sse-demo
$ cd sse-demo
$ mkdir src
deps.edn
link:example$server-sent-events/deps.edn[role=include]

Starting Simple

Let’s start with a basic home page and the server plumbing, then add SSE support.

We’ll need several namespaces for SSE support, so let’s bring them all in from the start:

src/sse_demo.clj
link:example$server-sent-events/src/sse_demo.clj[role=include]

A simple handler to verify things are working:

src/sse_demo.clj
link:example$server-sent-events/src/sse_demo.clj[role=include]

Start the REPL, load the namespace, and verify it works:

$ clj
Clojure 1.12
user=> (require 'sse-demo)
nil
user=> (sse-demo/start)
...
$ curl http://localhost:8890/
Hello, World!

Adding a Server-Sent Event Route

Now let’s add SSE support. We write a stream-ready callback; this function is called once the SSE connection is established. It receives a {core_async} channel and the SSE context as parameters.

You send events on the channel using regular {core_async} functions, such as put! or >!!, and ultimately terminate the connection by calling clojure.core.async/close! on the channel.

Each event is a map with :name and :data keys (and, optionally, an :id key). All values are converted to strings before being sent.

The callback must send at least one event in order to properly initialize the stream. If it doesn’t, the client will think the stream is broken and keep reconnecting.

Importantly, it is not the job of your callback to send all of the events; your callback should set in motion the concurrent machinery that sends the events. Events will continue to be sent to the client even after the callback returns, until either the client closes the connection, or your code closes the channel.

In this example, we send ten counter events at one-second intervals:

src/sse_demo.clj
link:example$server-sent-events/src/sse_demo.clj[role=include]
1The work is done in a future, so the callback returns immediately.
2Each event is a map with :name and :data keys.
3Closing the channel terminates the SSE connection.

Now we create an interceptor that calls api:start-stream[ns=io.pedestal.http.sse] to initiate the event stream:

src/sse_demo.clj
link:example$server-sent-events/src/sse_demo.clj[role=include]
1start-stream sets up the SSE connection and calls stream-ready once it’s established. It returns the context with a :response key, which is an open-ended streaming response.

An event stream is a particular kind of asynchronous response. start-stream returns a context with a response that "stays open", and the server keeps appending events from the channel to the response body.

Finally, add the /counter route:

src/sse_demo.clj
link:example$server-sent-events/src/sse_demo.clj[role=include]

And the connector plumbing:

src/sse_demo.clj
link:example$server-sent-events/src/sse_demo.clj[role=include]

Trying It Out

Restart the connector to pick up the changes:

user=> (require :reload 'sse-demo)
nil
user=> (sse-demo/restart)
...

Now test the event stream with curl in another terminal:

$ curl http://localhost:8890/counter
event: counter
data: 0

event: counter
data: 1

event: counter
data: 2

event: counter
data: 3

event: counter
data: 4

event: counter
data: 5

event: counter
data: 6

event: counter
data: 7

event: counter
data: 8

event: counter
data: 9

The events arrive at one-second intervals, then the connection closes.

SSE data is always received as a string. If you want to send JSON (or any other data format), it’s up to the sender to encode it and the receiver to decode it accordingly.

None of the Pedestal interceptors are invoked when sending SSE events. The interceptors are used for the initial connection request from the client, but not on the events themselves.

Connecting from JavaScript

To consume the event stream from a browser, use the standard EventSource API:

const eventSource = new EventSource("/counter");
eventSource.addEventListener("counter", (event) => {
    console.log("Counter:", event.data);
});

Coverage of the JavaScript EventSource (SSE) API is beyond the scope of this Pedestal guide. Consult the MDN documentation for a thorough introduction.

The Whole Shebang

For reference, here is the complete source:

src/sse_demo.clj
link:example$server-sent-events/src/sse_demo.clj[role=include]
deps.edn
link:example$server-sent-events/deps.edn[role=include]

Wrapping Up

We’ve covered a minimal setup for server-sent event configuration and usage and demonstrated how to receive events with curl and from JavaScript.

For more details, see the Server-Sent Events Reference.

Can you improve this documentation?Edit on GitHub

cljdoc builds & hosts documentation for Clojure/Script libraries

Keyboard shortcuts
Ctrl+kJump to recent docs
Move to previous article
Move to next article
Ctrl+/Jump to the search field
× close