Single Page Apps and Realtime APIs

About me

Software Engineer at Mathspace:

https://mathspace.co/

Main author of SocketCluster: 

http://socketcluster.io/

Jonathan Gros-Dubois

What is realtime?

What problem does it solve?

Protocol comparison

  • HTTP follows a request-response mode of interaction - The client initiates the connection.
  • The server can only respond to requests from the client - It cannot initiate.
  • With WebSockets, the client initiates the connection, but afterwards, either party is free to send a message to the other.
  • The connection is kept alive.
  • After the initial handshake, raw messages can be sent using WebSocket frames - These are lightweight - Unlike HTTP requests, they carry no headers.

HTTP polling hacks

There are ways to emulate WebSockets using HTTP long polling.

SockJS

These solutions are great at first but things can get difficult when you need to scale beyond a single process/host.

Emulating a single realtime connection over multiple HTTP requests has the following shortcomings:

 

  • You lose the in-order message delivery guarantee which a single TCP connection would otherwise provide.
  • Each new HTTP request contains extra header information which is not needed - This will consume extra CPU and memory.
  • In a multi-process or multi-host setup, some requests could be sent to the wrong process, so you have to use sticky load balancers to route requests to the correct server. This has scalability and security implications.

Performance comparison

Reactive/declarative programming and live binding of data in views

Renders to =>

Basic example (PolymerJS):

Declarative over imperative means that you don't need to get references to particular elements and explicitly render stuff inside them.

 

You declare the relationship between your views and your data and the rest is automatic. Data is free to flow throughout your views. Less control logic.

On the front-end, data binding allows you to relinquish unecessary control

Too much control can be dangerous

With Realtime APIs, you should also relinquish some control

Writing too much control logic on the server is an anti-pattern.

Because WebSockets are bidirectional, you may be tempted to get references to specific client sockets when you want to provide them with data.

The most common question from new users

of SocketCluster who are running their code on

multiple processes goes like this:

 

"I want to send a message to a specific user from the server side. How can I get a reference to a

socket which is hosted on a different worker process?"

The problem

A naive

solution

Naive solution at scale

Good patterns for realtime

The server should regulate data flow, not dictate it. That means avoiding doing lookups to get a hold of specific sockets! Managing propertyName=>socketID mappings is hard work... Even if you've only running your code on a single process, you have to remember to cleanup the mappings when sockets leave.

 

The solution is to give more control to the consumer of the data (the front-end) - Pub/Sub is a good, scalable way to achieve that. With a middleware/authorization layer, you can enforce access control.

SocketCluster lets you live bind directly to server-side data in realtime.

You can use it with any database you like.

A REST-like pattern for realtime APIs

socket.emit('get', {type: 'Product', id: 123, field: 'price'}, errorAndDataHandlerFn);

socket.emit('set', {type: 'Product', id: 123, field: 'price', value: 99.95}, errorHandlerFn);

socket.emit('delete', {type: 'Product', id: 123}, errorHandlerFn);

var productChannel = socket.subscribe('Product/123/price');   

productChannel.watch(channelDataHandler);

// Realtime equivalent to REST's GET method

// Equivalent to REST's POST and UPDATE methods - You can also separate this

// into 'set' and 'update' if you like

// Equivalent to REST's DELETE method

// This is the one which gives us realtime updates. In practice, you could use

// REST over HTTP instead of all the others and only use sockets for this one; that

// way the realtime update can be a progressive enhancement for newer browsers only.

On the server-side

scServer.on('connection', function (socket) {
  
    socket.on('get', function (query, callback) {
      var deepKey = [query.type, query.id];
      if (query.field) {
        deepKey.push(query.field);
      }
      scServer.global.get(deepKey, callback);
    });
    
    socket.on('set', function (query, callback) {
      var deepKey = [query.type, query.id];
      if (query.field) {
        deepKey.push(query.field);
      }
      scServer.global.set(deepKey, query.value, function (err) {
        if (!err) {
          var channelName;
          
          if (query.field) {
            scServer.global.publish(query.type + '/' + query.id + '/' + query.field, query.value);
          }
          // TODO: Also publish to higher levels in hierarchy: query.type + '/' + query.id and just query.type channels
        }
        callback(err);
      });
    });
  });

Here we are using SC's internal 'global' hierarchical data store to hold the data in memory (not persistent - So only for demo purposes) - You can use any database/datastore you like.

Try it yourself

Single Page Apps and Realtime APIs

By grosjona

Single Page Apps and Realtime APIs

  • 8,549