EventSource

Standardized real-time events

Burak Yigit Kaya / @madBYK

if this talk is interesting

we're hiring! - disqus.com/jobs

What is Disqus?

Why Realtime?

  • Capture and deliver the moment
  • Increase engagement
  • Looks cool 😎

How did it start?

Periodical requests to the server

If ain' broken, don't fix it?

So why did we "fix" it?

Coz it was broken.

What was broken?

  • Not scalable, although based on memcache
  • Not really realtime due to limitations on frequency
  • Running script directly, hard to play with

Show me some code


var interval = setInterval(function () {
    var cache = DISQUS.cache.realtime,
        url   = DISQUS.jsonData.urls.realtime;

    url += '?timestamp=' + cache.last_checked +
        '&thread_id=' + DISQUS.jsonData.thread.id +
        '&f=' + DISQUS.jsonData.forum.url + '&';

    if (!cache.ongoing_request && DISQUS.jsonData.realtime_enabled) {
        if (cache.prev_script && cache.prev_script.parentNode) {
            DISQUS.nodes.remove(cache.prev_script);
        }

        cache.ongoing_request = true;
        cache.prev_script = DISQUS.request.get(url, undefined, true);
    }
}, DISQUS.jsonData.context.realtime_speed);
                    

What's next then?

Node.JS + Socket.IO!

with Flash fallback

Nginx PushStream
+
Streaming XHR

Streaming XHR?


var interval = setInterval(function () {
    // Do nothing if the server didn't push anything new.
    if (xhr.responseText && len !== xhr.responseText.length) {
        /* ...try parsing responseText... */
        
        /* ...trigger events etc... */
        
        len = xhr.responseText.length;
    }

    if (xhr.readyState == 4) {  // detect close
        xhr.abort();
        clearInterval(interval);
        return void self.poll();
    }
}, frequency);
                        

Still using timers / periodic checks :(

Better streaming XHR!

brought to you by HTML5 and XHR2

Using onProgress


onProgress: function () {
    var resp = this.xhr.responseText;
    var advance = 0;
    var rows;

    // Sanity check
    if (!resp || this.marker >= resp.length)
        return;

    /* ...try parsing responseText... */
    
    /* ...trigger events etc... */

    if (advance > 0)
        this.marker += (advance - 1);  // -1 since no new line at the end
}
                        

Still not good enough

  • Parsing line-by-line is hard
  • Infinite browser loading spinner
  • Needs periodic reconnects due to memory issues

WebSocket:
An Elegant Weapon

Initial WebSocket Code


run: function () {
    var socket = this.socket = new WebSocket(this.url);

    socket.onmessage = this.onmessage.bind(this);
    socket.onclose = this.close.bind(close);
},

onmessage: function (evt) {
    var msg = JSON.parse(evt.data);
    this.handleMessage(msg);
}
                        

Detecting browser support

!!window.WebSocket

Only if everything was perfect...


// Hi, I'm Safari 5.1, the smartest browser in the world who implemented
// an old WebSocket draft without a vendor prefix, just for fun!
window.WebSocket && WebSocket.CLOSING === 2,
                            

What if connection drops?


onClose: function (evt) {
    if (!evt.wasClean)
        return;  // means error, which is handled by onError

    DISQUS.log('RT: [Socket] Connection closed. Restarting...');
    this.trigger('close', this);
    this.run();
}
                        

What if connection drops due to server error?


onError: function () {
    this.trigger('error');

    if (this.interval <= BACKOFF_LIMIT)
        this.interval *= EXP_BASE;

    DISQUS.logError('RT: Connection error, backing off...');
    _.delay(this._boundRun, this.interval * 1000);
}
                        

Detect handshake failure


onOpen: function () {
    this.handshakeSuccess = true;
},
onError: function () {
    if (!this.handshakeSuccess) {
        this.trigger('fail');
        return;
    }  /* ... */
},
initialize: function (threadId, handlers, context) {
    /* ... */
    if (ws) {  // if browser supports WebSockets
        pipe.on('fail', function () {
            this._wsSupported = false;
            /* reinit */
        }, this);
    }
    /* ... */
}
                        

Bleh...

Status Check

Problems Solved

  • Infinite browser loading spinner
  • Manual stream parsing

Problems Standing

  • Manual connection control
  • Manual event distribution

Problems Added

  • Detecting browser support
  • Detecting network support
  • Implementing WebSockets backend

Revenge of the Socket

We need a hero...

EventSource: A New Hope

What do we have here?

  • An event system, based on a simple data structure
  • Semi-automatic connection control
  • Good old HTTP with just a new MIME type

Sample Data


id: 0ab2a49455664f0c9bca9a057b35af8a
event: Vote
data: { /* Some JSON data in a single line */ }
                        

How to use?


var handleGeoEvent = function(e) {
    var geo = JSON.parse(e.data).message_body.geo;
    addGeoPoint(geo.latitude, geo.longitude);
};

var ev = new EventSource("http://realtime.services.disqus.com/api/raw/orbital");
ev.addEventListener("Post", handleGeoEvent);
ev.addEventListener("Vote", handleGeoEvent);
ev.addEventListener("ThreadVote", handleGeoEvent);
                        

Status Check

Problems Solved

  • Infinite browser loading spinner*
  • Manual stream parsing*
  • Manual connection control (sort of)
  • Manual event distribution
  • Detecting network support
  • Implementing WebSockets backend (averted)

* Already solved by WebSockets

Web Dev's Dream?

Problems Standing

  • Detecting browser support

    Polyfills + if (window.EventSource)

Gotcha: CORS on Webkit

If on different origin:


try {
    var ev = window.EventSource && new EventSource(url);
catch (err) {  // different origin will trigger an error immediately
    // fallback
};
                        

Or a static check that's built into Modernizr?

if (Modernizr.EventSourceWithCORS)

Toolchain Summary

Reasons for using WebSockets over EventSource

  • Low-latency client-to-server messages
  • Somewhat better CORS support
  • Low level operations (binary messages etc.)

Reasons for using EventSource over WebSockets

  • Structured data and events
  • No extra backend requirements
  • No proxy/firewall problems, just HTTP
  • Fallback is a JS-only polyfill

Conclusion?

Use Nginx PushStream with EventSource, especially if you just need to update the client!

Psst... more about Nginx PushStream here: Making Disqus Realtime

Credits

Lego Star Wars Images by Mike Stimpson

Moral support and reviews by:

References

Still hiring!

come, join us! - disqus.com/jobs

Thanks!

Questions?