EventSource permanent auto reconnection
Asked Answered
I

3

10

I am using JavaScript EventSource in my project front-end.

Sometimes, the connection between the browser and the server fails or the server crashes. In these cases, EventSource tries to reconnect after 3 seconds, as described in the documentation.

But it tries only once. If there is still no connection, the EventSource stops to try reconnection and the user have to refresh the browser window in order to be connected again.

How I can prevent this behavior? I need the EventSource to try reconnecting forever, not only once.

The browser is Firefox.

Indoiranian answered 17/2, 2014 at 14:7 Comment(0)
R
9

I deal with this by implementing a keep-alive system; if the browser reconnects for me that is all well and good, but I assume sometimes it won't work, and also that different browsers might behave differently.

I spend a fair few pages on this in chapter five of my book (Blatant plug, find it at O'Reilly here: Data Push Applications Using HTML5 SSE), but if you want a very simple solution that does not require any back-end changes, set up a global timer that will trigger after, say, 30 seconds. If it triggers then it will kill the EventSource object and create another one. The last piece of the puzzle is in your event listener(s): each time you get data from the back-end, kill the timer and recreate it. I.e. as long as you get fresh data at least every 30 seconds, the timer will never trigger.

Here is some minimal code to show this:

var keepAliveTimer = null;

function gotActivity(){
  if(keepaliveTimer != null)clearTimeout(keepaliveTimer);
  keepaliveTimer = setTimeout(connect, 30 * 1000);
}

function connect(){
  gotActivity();
  var es = new EventSource("/somewhere/");
  es.addEventListener('message', function(e){
    gotActivity();
    },false);
}
...
connect();

Also note that I call gotActivity() just before connecting. Otherwise a connection that fails, or dies before it gets chance to deliver any data, would go unnoticed.

By the way, if you are able to change the back-end too, it is worth sending out a blank message (a "heartbeat") after 25-30 seconds of quiet. Otherwise the front-end will have to assume the back-end has died. No need to do anything, of course, if your server is sending out regular messages that are never more than 25-30 seconds apart.

If your application relies on the Event-Last-Id header, realize your keep-alive system has to simulate this; that gets a bit more involved.

Rios answered 18/2, 2014 at 6:52 Comment(5)
Well, the gotActivity call should be before "new EventSource", but all works after some small fixing.Indoiranian
@Indoiranian What was the problem case needing gotActivity() to come before the new EventSource() call?Rios
I have very little JS knowledge. But in my tests, if the new event source failed to connect, then gotActivity was not executed at all and so, the timer got not restarted for another attempt....Indoiranian
@Indoiranian Thanks. I see what you mean, and I've adjusted my answer. (I had it after so if the client doesn't support EventSource, or any of the fallbacks I add in, it wouldn't waste effort trying again every 30 seconds; but that is a more minor issue.)Rios
Nice design. However, if the server requires a login with credentials, and the server is restarted (invalidating any session authentication cookie), things get more complicated. :-)Snowfield
M
3

In my experience, browsers will usually reconnect if there's a network-level error but not if the server responds with an HTTP error (e.g. status 500).

Our team made a simple wrapper library to reconnect in all cases: reconnecting-eventsource. Maybe it's helpful.

Marcello answered 6/1, 2019 at 19:18 Comment(1)
Very helpful, Chrome 88 does not need this because readystate is 0 on disconnect. FF 85 does because readystate seems to go immediately to 2(closed) on error. This wrapper correctly attempts reopen on readystate=2(FF) so safe to use on both browsers.Jolandajolanta
D
1

Below, I demonstrate an approach that reconnects at a reasonable rate, forever.

This code uses a debounce function along with reconnect interval doubling. During my testing, it works well. It connects at 1 second, 4, 8, 16...up to a maximum of 64 seconds at which it keeps retrying at the same rate.

function isFunction(functionToCheck) {
  return functionToCheck && {}.toString.call(functionToCheck) === '[object Function]';
}

function debounce(func, wait) {
    var timeout;
    var waitFunc;

    return function() {
        if (isFunction(wait)) {
            waitFunc = wait;
        }
        else {
            waitFunc = function() { return wait };
        }

        var context = this, args = arguments;
        var later = function() {
            timeout = null;
            func.apply(context, args);
        };
        clearTimeout(timeout);
        timeout = setTimeout(later, waitFunc());
    };
}

// reconnectFrequencySeconds doubles every retry
var reconnectFrequencySeconds = 1;
var evtSource;

var reconnectFunc = debounce(function() {
    setupEventSource();
    // Double every attempt to avoid overwhelming server
    reconnectFrequencySeconds *= 2;
    // Max out at ~1 minute as a compromise between user experience and server load
    if (reconnectFrequencySeconds >= 64) {
        reconnectFrequencySeconds = 64;
    }
}, function() { return reconnectFrequencySeconds * 1000 });

function setupEventSource() {
    evtSource = new EventSource(/* URL here */); 
    evtSource.onmessage = function(e) {
      // Handle even here
    };
    evtSource.onopen = function(e) {
      // Reset reconnect frequency upon successful connection
      reconnectFrequencySeconds = 1;
    };
    evtSource.onerror = function(e) {
      evtSource.close();
      reconnectFunc();
    };
}
setupEventSource();
Defer answered 27/1, 2019 at 6:8 Comment(0)

© 2022 - 2024 — McMap. All rights reserved.