DEV Community

Cover image for Event Source Interface, an unidirectional alternative to Sockets in JavaScript
Felippe Regazio
Felippe Regazio

Posted on

Event Source Interface, an unidirectional alternative to Sockets in JavaScript

A Web Socket is a computer communication protocol that provides a bidirectional communication between the server and the client. Its specially useful if you need a persistent connection for long time running tasks with decision flow, games, chats, etc. and its pretty faster if compared to polling. In a socket you can send information to your server anytime and wait for the response and vice-versa. Also a socket supports many connections, so the same socket can have multiple-clients sharing the same channel.

But sometimes you don't need to exchange information with the server in that fashion, you just need to be notified by the server about something, for exemple a social network status or keep monitoring a long running task. In these cases, you could use a socket anyway but would be a waste of resources. You could also sending many requests to a server endpoint, but would be harmful for your performance. Would be better to use the Event Source Interface.

Unlike WebSockets, server-sent events are unidirectional; that is, data messages are delivered in one direction, from the server to the client (such as a user's web browser). That makes them an excellent choice when there's no need to send data from the client to the server in message form. For example, EventSource is a useful approach for handling things like social media status updates, news feeds, or delivering data into a client-side storage mechanism like IndexedDB or web storage | https://developer.mozilla.org/en-US/docs/Web/API/EventSource

For the backend, we will create an endpoint for the Event Source. This will be our Event Emitter, and it must follow a pre-defined structure. We will need to set some headers:

Content-Type: text/event-stream
Cache-Control: no-cache
Enter fullscreen mode Exit fullscreen mode

And the data must be sent on the body like this:

data: some string
Enter fullscreen mode Exit fullscreen mode

Thats all. Traducing it to PHP, you would have something like this:

<?php

header('Content-Type: text/event-stream');
header('Cache-Control: no-cache');

function send_message ($message) {
  echo "data: " . json_encode($message) . PHP_EOL;
  echo PHP_EOL;
  ob_flush();
  flush();
}
Enter fullscreen mode Exit fullscreen mode

I'll use PHP for that purpose but you can write your backend in any language that you prefer.

On the code above we are setting the proper headers, and a function that flushes the data: "{...}" on the body. The message don't need to be a encoded json, it can be a plain string, but lets encode it to be more scalable.

The EventSource connection will be persistent. The client request activates the backend, and it keeps re-starting the server script every time that it ends till you explicitly tells your client to stop.

Lets write a backend that counts to 50 waiting 2 seconds between every number. Before pass to the next number, we will send our current index to our client.

Here's the entire PHP script for that:

<?php

header('Content-Type: text/event-stream');
header('Cache-Control: no-cache');

function send_message ($message) {
  echo "data: " . json_encode($message) . PHP_EOL;
  echo PHP_EOL;
  ob_flush();
  flush();
}

for ($i = 0; $i < 50; $i++) {
    send_message($i + 1);
    sleep(2);
}

send_message('done');
Enter fullscreen mode Exit fullscreen mode

If you access a file with that code, you will see the data being flushed on the page. Every number is an event being sent from the server to the client. Now we need to listen those events on our application and handle them as we prefer. Thats a job for the Event Source Interface.

The client will be pretty simple. We will create a new EventSource using its constructor, point it to our backend script and start to listen the messages.

The events emitted by the EventSource instance are onopen, onmessage and onerror. They are pretty descriptive and our JavaScript must be quite simple. We will create our EventSource instance, listen to the events from the server and execute a function to handle those events properly.

// here we are defining the backend endpoint
const EVENT_SOURCE_ENDPOINT = 'backend/event_server.php';

// instantiating the EventSource and pointing it to our endpoint
const ServerEvents = new EventSource(EVENT_SOURCE_ENDPOINT);

// listening to the connection with the server
ServerEvents.addEventListener('open', e => {
    handleServerConnection();
});

// listening to server messages
ServerEvents.addEventListener('message', e => {
    const data = JSON.parse(e.data);
    handleServerMessage(data);
});

// listening to errors
ServerEvents.addEventListener('error', e => {
    handleServerError(e);
});
Enter fullscreen mode Exit fullscreen mode

The comments on the JS code above will must be enough to give a pretty notion about whats happening. The server will be sending messages to our client application which is listening to them. Every time a message is delivered, the client listens the event source event and runs our handle.

The application will be still available for the user and those functions will be running asynchronously executing the handlers always when the event happens. This is just my way to handle it, you could just write the code inside the events callbacks and do whatever you want if you prefer.

Here is the complete JS example:

(function () {

// here we are defining the backend endpoint
const EVENT_SOURCE_ENDPOINT = 'backend/event_server.php';

// instantiating the EventSource and pointing it to our endpoint
const ServerEvents = new EventSource(EVENT_SOURCE_ENDPOINT);

// listening to the connection with the server
ServerEvents.addEventListener('open', e => {
    handleServerConnection();
});

// listening to server messages
ServerEvents.addEventListener('message', e => {
    const data = JSON.parse(e.data);
    handleServerMessage(data);
});

// listening to errors
ServerEvents.addEventListener('error', e => {
    handleServerError(e);
});

// ------------------------------------------------------

// append a string (msg) on our <pre> element
uiRenderMessage = (msg) => {
    document.getElementById('server-messages').append(`${msg}\n`);
}

// show the connected message when connect to the server
handleServerConnection = () => {
    uiRenderMessage('A connection with server has been established\n');
}

// handle the messages received by the server
handleServerMessage = msg => {
    uiRenderMessage(`Server said: ${msg}`);
    if (msg === 'done') {
        // if you don't handle a closing message, the process will
        // start all over again.
        uiRenderMessage('\n');
        ServerEvents.close();
    }
}

handleServerError = evt => {
    uiRenderMessage('An unexpected error occurred :(');
    console.error(evt);
}

})();
Enter fullscreen mode Exit fullscreen mode

Once the backend script is started by the client requisition, you must send a close message on the end of the process and use it to close your EventSource on the client. If you don't do that, the process will keep repeating all over and over again. This would result in our application starting to count till 50 again on every end. If you look at the handleServerMessage() function, we wait for the 'done' message to know when the server has all the work done. Depending on your usage, you wont
need a close flag.

Quick Tip: When instantiating the EventSource, you can send some data to the backend by attaching it on the URL GET Params. Its not a form of communication since you can send the data only one time, but its useful anyway. In the case of this example, you would add the data on the GET Params of the EVENT_SOURCE_ENDPOINT.

I created a repo on GitHub with this example running. You must clone the repo, and access the index.html. Its a stupidly simple example with 3 main files with not more then 20 lines each. You will see a simple page with a text area (to check the page availability), and an element showing the server messages appearing one by one, counting till 50 as the server updates them.

If you don't have the LAMP stack on your computer to test it but have PHP, you can go to the "backend" folder on the repo and run:

php -S 0.0.0.0:9999
Enter fullscreen mode Exit fullscreen mode

This will run a built-in php server on that folder.
Now change the EVENT_SOURCE_ENDPOINT on the JS file to "http://localhost:9999/event_server.php".

With that pattern is possible to construct a lot of cool things, i guess. Happy code. Thats all folks!

Cover Photo by Tian Kuan on Unsplash

Latest comments (4)

Collapse
 
aslasn profile image
Ande • Edited

Note that the EventSource API isn't supported in any version of Microsoft Edge, Internet Explorer and few other minor browsers. Here's the compatibility table. While web sockets are supported in quite a lot of browsers including IE10.

Collapse
 
felipperegazio profile image
Felippe Regazio • Edited

You're right, and thanks for your contribution : ) You will have to consider your needings and use case. Also, Polyfills are available for browsers that do not support EventSource API but i thing they just fallbacks to polling (maybe, i didnt check).

Collapse
 
diek profile image
diek

I don't really understand the difference between doing this and request a GET to retrieve that data using a recursive timeout. Can you extend this?
Thank you.

Collapse
 
felipperegazio profile image
Felippe Regazio • Edited

I can see some benefits to not Polling (your suggestion) depending on your needing:

  1. Performance. You can create a new request like a heartbeat and keeps asking data to the server, consuming extra memory, while with SSE (Server Sent Events, or the EventSource), the server pokes your app and say "heres new data".

  2. Provides a pattern. If you use the javascript EventSource API your code will be easier to understand considering new devs on the base and the already-made online docs.

  3. Polling is not always a good option. Imagine a very long task on server, it could lead to timeouts on both sides (server timeouts and client timeouts), with the proper protocol or interface, this wouldn't happen.

I could say some things more but i believe that really depends of your use case. This article is a good source to discover the differences between Polling, SSE and Sockets deeply:

codeburst.io/polling-vs-sse-vs-web...