cri.dev
about posts rss

Simple Server-sent events example in Node.js/JavaScript

Published on

With Server-sent events you are able to send one-directional events to a web page.

Here is an example of how I used this functionality in Minimal Analytics

Context

In Minimal Analytics I needed to send updates about live users active on this blog back to the dashboard available at s.cri.dev

Here is how it looks

minimal analytics live

This is a perfect use case for Server-sent Events.

Below you’ll find example code to implement it yourself in a bare Node.js based HTTP server.

All the code is available on in the Minimal Analytics Github repository

On the client side

I want to start with the client to better explain what happens under the hood.

As per MDN documentation

An EventSource instance opens a persistent connection to an HTTP server, which sends events in text/event-stream format

On the client/dashboard side, the code to start a one-directional communication channel to the server looks like this:

const eventSource = new window.EventSource('/')

eventSource.onmessage = (message) => {
  if (!message || !message.data) return console.error('skipping empty message')
  const live = JSON.parse(message.data, {})
  console.log('sse live visitors', live)
  // in a react component you could update the state
  this.setState({ live })
}

This way I’m connecting the web page through a SSE channel and listening for new messages.

Remember: only the server can send messages downstream to the client, it’s a one-directional channel (in contrast to WebSockets)

If you open the network tab in your browser, you can inspect the messages that are sent down the wire:

minimal analytics sse-messages

On the server side

In a bare Node.js HTTP server, you could integrate the SSE channel like this:

const connections = []
const server = http.createServer(function (req, res) {
  ...
  if (req.headers.accept && req.headers.accept.includes('text/event-stream')) {
    handleSSE(res, connections)
    return sendSSE(JSON.stringify(live), [res])
  }
  ...
})

connections represents all active SSE connections.

I’m setting up a condition to check if the request’s Accept header is an Event Stream.

If so, handleSSE takes care of adding this new request to the list of active connections and replying with the current live visitors with the sendSSE function.

handleSSE

handleSSE adds the response stream to the active connections array and removes it when the connections is closed.

Additionally it replies to the browser with the correct headers and status code.

Note the text/event-stream Content-Type header.

function handleSSE (res, connections = []) {
  connections.push(res)
  res.on('close', () => {
    connections.splice(connections.findIndex(c => res === c), 1)
  })
  res.writeHead(200, {
    'Content-Type': 'text/event-stream',
    'Cache-Control': 'no-cache',
    Connection: 'keep-alive'
  })
}

sendSSE

sendSSE simply loops over all active connections and broadcasts a message.

Messages consist of an id line (simply the current date could suffice) and a data line.

function sendSSE (data, connections = []) {
  connections.forEach(connection => {
    const id = new Date().toISOString()
    connection.write('id: ' + id + '\n')
    connection.write('data: ' + data + '\n\n')
  })
}

Real-world example

You can find all the code used in this post in the Minimal Analytics git repository

Here, have a slice of pizza 🍕