Server-sent Events (SSE)

Traditionally, a web page has to send a request to the server to receive new data; that is, the page requests data from the server. With server-sent events, it's possible for a server to send new data to a web page at any time, by pushing messages to the web page.

SSE VS WebSockets

Server-Sent Events (SSEs) and WebSockets are both technologies that push data from the server to the client, a process also known as “server push”. However, they have fundamental differences and are designed for different use cases.

Server-Sent Events (SSEs)

SSEs are unidirectional and allow data to be sent only from the server to the client. They are less complex to implement and have built-in support for reconnection, meaning the connection will automatically be reestablished if it's lost. This feature reduces the amount of code needed to maintain a connection.

SSEs are specified in the HTML specification and are supported by all major browsers. However, SSEs have some limitations:

  • They only support UTF-8 message transport and do not support binary data.

  • They can only maintain six concurrent open connections per browser at a time.

  • They are uni-directional, meaning they can only send messages from server to client.

Due to these characteristics, SSEs are particularly useful for apps that only require reading data from the server, like livestock or news tickers.

WebSockets

WebSockets, on the other hand, provides a bidirectional communication channel over a single TCP connection. This means that data can be sent from the server to the client and vice versa simultaneously.

A WebSocket connection starts with an HTTP connection, which is then upgraded to a WebSocket connection through a TCP handshake. Once the handshake is complete, bidirectional communication is possible.

WebSockets are preferable for use cases such as multiplayer collaboration and chat apps, where real-time, two-way communication is required. They also support both binary data and UTF-8 message transport

To send an event using SSE

You have to add a response header of content type to 'text/event-stream' and the actual message to be sent should be prepended with this text 'data: '

for example

const app = express()

app('/', (req, res) => {
    res.setHeader('Content-Type', 'text/event-stream')
    res.write('data: Hello World \n\n')
})

And on the frontend of things, we'll instantiate an EventSource object and listen to incoming messages, EventSource is the api that helps us to handle Server-sent event

const eventSource = new EventSource('http://localhost:3000');

eventSource.onmessage = (event) => {
    console.log(event.data)
}

Creating an anonymous group chat with SSE

Our frontend

const form = document.getElementsByTagName('form')[0]
const eventSource = new EventSource('http://localhost:1337/sse');

form.addEventListener('submit', (event) => {
    event.preventDefault()
    const input = document.getElementById('message')
    const value = input.value
    const url = `http://localhost:1337/chat?message=${value}`
    fetch(url)
    input.value = ''
})

eventSource.onmessage = (event) => {
    const chatMessage = document.getElementById('chatMessages')
    const div = document.createElement('div')
    div.textContent = event.data
    chatMessage.appendChild(div)
}

Our Backend

const express = require("express");
const EventEmitter = require("events");
const app = express();

const chatEmitter = new EventEmitter();

app.get("/chat", (req, res) => {
    res.setHeader("Access-Control-Allow-Origin", "*");
    const message = req.query.message;
    chatEmitter.emit("message", message);
})

app.get("/sse", (req, res) => {
    res.setHeader("Access-Control-Allow-Origin", "*");
    res.setHeader("Content-Type", "text/event-stream");
    chatEmitter.on('message', (message) => {
        res.write(`data: ${message} \n\n`)
    })

    req.on('close', () => {
        res.end()
        chatEmitter.removeListener('message', () => {
            console.log("REMOVED LISTENER")
        })
    })

})

app.listen(1337, () => {
    console.log("SERVER CONNECTED");
})

We use an event listener so that we can receive and emit our message back to the client through the "/sse" end point.

In the chatEmitter "message" listener notice that we use res.write as oppossed to res.send.

The use of res.send() within the event listener callback causes headers to be set and a response sent each time a 'message' event is received. This action continues even after the initial response has been completed and sent off which then results in the 'ERR_HTTP_HEADERS_SENT' error.

This difference is critical — whereas res.send() ends the response, res.write() does not. And it's worth noting that calling res.write() after res.send() would lead to an error because res.send() would close the connection.

At the same time, it's also worth noting that we should remove the event listener (with removeListener) when the client disconnects (which we can track via req.on('close', ...)), so we don't keep sending messages to a client that's no longer connected.

References