Do you need to impleent Server-Sent Events in Node.js? In this post, I'll explain how it works and how to implement it using Node.js back-end and JavaSript front-end.

What is Server-Sent Events?

Server-Sent Events (SSE) is a technology in which the server sends update to the client (usually a web browser) automatically. The standard of how the server should send data to the client has been standarized as a part of HTML5.

What is It Used For?

SSE is usually used in a condition where you need to send continuous stream or data message updates to client. It makes use of JavaScript's Event Source API. For example, your server takes a long time for processing something (maybe for a few minutes or even hours). On the other side, the client needs to know the progress of that long process. With SSE, the server can tell the progress to the client by sending a sequence of messages. It's the responsibility of the server to keep the client get the progress. The client doesn't need to perform pooling or send any requests to get the update message.

 Unfortunately not all browsers support SSE, as listed on the table below

Browser SSE Support Notes
Google Chrome Yes From version 6
Mozilla Firefox Yes From version 6.0
Microsoft Internet Explorer No  
Microsoft Edge Yes Under consideration
Opera Yes From version 11
Safari Yes From version 5.0

SSE on Internet Explorer and Edge

As the Microsoft browsers don't have built-in SSE suppot, we need a workaround. The easiest way is using event-soure polyfill. You only need to load this script on the page.

  <script src="https://cdnjs.cloudflare.com/ajax/libs/event-source-polyfill/0.0.9/eventsource.min.js"></script>

SSE Format

1. Header

For starting a new connection. The client creates a new instance of  EventSource. It will send a new HTTP GET request to the server

  const es = new EventSource('http://localhost:4000/sse');

Then the server will receive the request. To indicate that the response will be an event-stream, the server has to return a response with the following headers

  response.writeHead(200, {
    'Content-Type': 'text/event-stream',
    'Cache-Control': 'no-cache',
    'Access-Control-Allow-Origin': '*',
  });

2. Retry

If something cause the connection error, you can tell the browser to retry. In order to do so, you have to send a message with the following format before sending any data.

  response.write(retry: 10000) // Retry in 10 seconds

3. Data

Every message should have id and data. The end of a message is indicated by double endline \n\n. You can send a message cotaining multiple lines separated by \n. The id part is useful when the client retries to connect to the server, it will include Last-Event-ID in the header. Therefore it's possible for the server to resume processing an incoming request without having to start from the beginning.

  response.write('id: 1\n')
  response.write('data: some text\n\n') 

The client needs a listener to process messages sent by the server. We add listeners for three event types: open, message, and error. The open event is triggered when the connection sucessfully established.  The message event is triggered everytime the server sends data. If something error, the error event will be triggered

  const listener = function (event) {
    const type = event.type;

    window.console.log(`${type}: ${event.data || es.url}`);

    if (type === 'result') {
      es.close();
    }
  };

  es.addEventListener('open', listener);
  es.addEventListener('message', listener);
  es.addEventListener('error', listener);

By default if we send data without specifying the event name, it will be conceived as a message event. However we can set a custom event name like this one

  response.write('event: custom\n')
response.write('id: 1\n') response.write('data: some text\n\n')

That means we also need a new event listener.

  es.addEventListener('custom', listener);

4. Ending the Connection

After everything done, the server needs to end the connection. On the client side, the EventSource needs to be closed as well. To indicate it's the end of an event-stream, we can use a custom event name

  response.write('event: result\n');
  response.write(`data: ${data}\n\n`);
  response.end();

We need to add the following inside the client's listener function

  if (type === 'result') {
    es.close();
  }

And also a new event listener

  es.addEventListener('result', listener);

Code Example for Node.js

Based on the explanation above, this is the helper for handling SSE connection.

helpers/sse.js

  const _ = require('lodash');

  /**
   * The constructor.
   *
   * @param {Object} response
   * @param {Object} options
   * @return {SSE}
   * @constructor
   */
  function SSE(response, options) {
    if (!(this instanceof SSE)) {
      return new SSE(response, options);
    }

    options = _.defaults({}, options, {
      retry: 10000,
    });

    this.response = response;

    // This is the SSE header
    response.writeHead(200, {
      'Content-Type': 'text/event-stream',
      'Cache-Control': 'no-cache',
      'Access-Control-Allow-Origin': '*',
    });

    response.write(`:${Array(2049).join(' ')}\n`); // 2kB padding for IE

    // Set time before retry if connection error.
    // This need to be set before sending any message.
    response.write(`retry: ${options.retry}\n`);
  }

  /**
   * Send message.
   *
   * @param {*} id
   * @param {*} data
   */
  SSE.prototype.write = function (id, data) {
    this.response.write(`id: ${id}\n`);
    this.response.write(`data: ${data}\n\n`);
  };

  /**
   * End connection.
   *
   * @param {*} data
   */
  SSE.prototype.end = function (data) {
    this.response.write('event: result\n');
    this.response.write(`data: ${data}\n\n`);
    this.response.end();
  };

  module.exports = SSE;

We also need to make a route using the helper above for handling SSE connection

routes/index.js

  // We've created node module for that helper
  // Recommended to use that module
  // But if you don't want to install that module, you can copy the helper and import it directly
  const SSE = require('@woolha.com/sse');
  // const SSE = require('../helpers/sse');

  router.get('/sse', (req, res, next) => {
    const sse = SSE(res);

    const messages = ['first', 'second', 'third', 'fourth', 'fifth'];

    return Bluebird.each(messages, (message, index) => {
      sse.write(index, message);

      return Bluebird.delay(2000);
    })
      .then(() => {
        sse.end('qwerty');
      })
      .catch(next);
  });

Client Code

Here is the full example of client-side code

  <!DOCTYPE html>
  <html>
  <head>
  <meta charset="utf-8" />
  <title>SSE EventSource Example by Woolha</title>
  <meta http-equiv="X-UA-Compatible" content="IE=edge">
    <script src="https://cdnjs.cloudflare.com/ajax/libs/event-source-polyfill/0.0.9/  eventsource.min.js"></script>
    <script>
      const es = new EventSource('http://localhost:4000/sse');

      const listener = function (event) {
        const type = event.type;

        window.console.log(`${type}: ${event.data || es.url}`);

        if (type === 'result') {
          es.close();
        }
      };

      es.addEventListener('open', listener);
      es.addEventListener('message', listener);
      es.addEventListener('error', listener);
      es.addEventListener('result', listener);
    </script>
  </head>
  <body>
  </body>
  </html>

That's all about the tutorial of how to implement SSE in Node.js.