Tuesday, May 7, 2013

Syncing with Server-Sent Events

Server-Sent Events are a recent HTML5 addition which allows web apps to open and listen to event-streams from remote servers. Grimwire uses SSEs as a tool to keep programs and interfaces in sync. This post will give a quick overview of the APIs and how they can be used.

local.http.subscribe()

This function takes a url and issues a GET request for the "text/event-stream" type. If targeting a remote server, it will leverage BrowserRemoteEventStream. Otherwise, it issues an HTTPL request and manually watches for updates on the stream. From your perspective, it all looks like this:

var stream = local.http.subscribe('httpl://foo.usr/some/resource');
stream.on('update', function(e) {
    console.log(e.data);
});
stream.on(['foo','bar'], function(e) {
    console.log(e.event); // 'foo' or 'bar'
});
// ...
stream.close(); // client-side disconnect

In keeping with browser implementations, closing the event stream (from the server or the client) will emit an 'error' event with an undefined e.data.

navigator.subscribe()

If you find yourself needing to navigate to the event-stream, you can use navigator.subscribe(), which promises the EventStream interface:

local.http.navigator('httpl://foo.usr')
    .collection('some')
    .item('resource')
    .subscribe()
    .then(function(stream) {
        stream.on('foo', onFoo);
    });

data-subscribe

Grimwire doesn't allow any javascript to enter the document, meaning there's no client-side code. To create rich realtime UIs, servers use some added response directives, content-types, and HTML behaviors. One of the most common of these is the "data-subscribe" attribute.

<div data-subscribe="httpl://someserver.usr/some/resource">
    <p>This was last updated at 10:27:54</p>
</div>
<p>This will not be updated.</p>

Any element with this attribute will open an event-stream to the target and listen for the "update" event. Every time this occurs, the client region will issue a GET request to the same resource, then replace its innerHTML with the response. You can think of this as triggering targeted page-refreshes.

If the content of the refresh is at a different location than the event-stream, you can specify that URL afterward.

<div data-subscribe="httpl://foo.usr/bar httpl://foo.usr/buz">

Lastly, if the "data-subscribe" attribute is set on a form element, the form's "accept" attribute will be used, and its inputs will be enumerated in the query parameters of the request. This can be used to push up information about the client state.

local.http.broadcaster()

On the server end of things, you'll need to track any event-stream responses you have open so that you can write events to it. Grimwire gives you local.http.broadcaster() to help with this.

// This server is an open event relay
// - it rebroadcasts any data POSTed to it
var mybroadcast = local.http.broadcaster();
function main(request, response) {
    if (/event-stream/.test(request.headers.accept)) {
        response.writeHead(200, 'ok', {
            'content-type':'text/event-stream'
        });
        mybroadcast.addStream(response);
        // NOTE: don't call end() on the response!
    }
    else if (request.method == 'POST') {
        response.writeHead(204, 'no content').end();
        mybroadcast.emit('post', request.body);
        // ^ will broadcast to all open streams
    } else
        response.writeHead(400, 'bad request').end();
}

You can read more about its api in the Local documentation.

Wrapping up

Server-Sent Events are a very useful tool for synchronizing programs with other programs and with their interfaces. Consumers can access them using the subscriber APIs, and servers can broadcast them from any URL. Combining their "push" architecture with REST's "pull" requests makes it possible to build tightly-synced applications without tight coupling.

Read more at github.com/grimwire/grimwire.

No comments:

Post a Comment