Untangling with promises

In my Ephemeral Exchange project, I use socket.io to handle push notifications when any cache values are updated, are deleted or expire. Where you have a lot of asynchronicity going on, it can be hard to deal with all the callbacks.

For example, let's say that you try to set up a push notification - which then uses (or creates) a socket.io connection, and then set up another before the connection is complete, you would create a new connection unless you are tracking that there is already a connection in progress - but even then how do you communicate back to the second requestor when the first requestor is finished. 

All this is made very easy to orchestrate with promises - Here's how the conversation to asynchronously connect and authenticate with socket.io can be simplified.

The caller

The caller would like to get a socket, or use one that is already in place (or in the progress of being in place). The socket is managed centrally for a single client, so the first step is to make one if we don't have one. 
This is not asnynchronous, so is not troublesome.
 if (!sting) {
  sting = new Socketing (apiEnv);
}
 
Next, we can use the getConnected method - which simply returns a promise. The same promise is used by all connection attempts, so either we already have one, one is in progress or this is the first attempt. The great thing is you don't need to care which.
sting.getConnected(pushId).then (()=> { // do things with the connection });

The socketing namespace

Here's what getConnected looks like in the Socketing namespace - it returns the current promise for connection status, or kicks one off if there isn't one.
ns.getConnected = (pushId) => getConnected_ || connect_(pushId);

Now lets' take a look at the connect_ function, which actually has the conversation with the socket.io server
let getConnected_ = null;
const connect_ = (pushId) => {
  // get connected
  const socket = io.connect(config.socketBase + ":" + config.socketPort);
  // if a promise, then can be used to handle in progress too.
  getConnected_ = new Promise((resolve, reject) => {
        // have the conversation with the server and resolve or reject the promise as required
  });
  return getConnected_;
};

The server conversation is a little more complex since it has to manage server events, timeouts and also authenticate using a passphrase. However this, too can be broken down into promises. Here's the sequence of events
      // deal with the sequnce of connection events
      connectionEvent()
        .then(() => passEvent())
        .then((passResult) => {
          if (passResult.ok) {
            ns.connection.socket = socket;
            ns.connection.message = passResult;
            ns.connection.pushId = pushId;
            resolve(ns);
          }
          else {
            reject('failed to sync passes');
          }
        })
        .catch((err)=>reject (err));

First the connectionEvent
    // handle connection event from socket.io
    function connectionEvent() {

      return new Promise((resolve, reject) => {
        // deal with the connection event
        socket.on('connect', () => resolve());
      });

    }

Now the passphrase conversation
      // handle the password conversation
    function passEvent() {

      return new Promise((resolve, reject) => {

        // the payload to send over
        var pack = {
          pass: config.socketPass,
          id: socket.id,
          pushId: pushId
        };

        
        // but only wait a while
        pTimer_(pack , PASS_TIMEOUT).then (()=>{
          if (!ns.isConnected())reject('passevent attempt timed out');
        });
        
        // try the conversation
        socket.emit('pass', pack, (result)=> resolve(result));
        
      });

    }

And the timeout function is also a promise
  function pTimer_(id, ms) {
    return new Promise((resolve, reject) => {
      setTimeout(() => resolve(id), ms);
    });
  }

And finally, deal with disconnect events. The key here is to set the getConnected_ promise to null, so future attempts will initiate a connection again.
    // deal with a disconnection event
    socket.on('disconnect', function(data) {
      ns.connection.socket = null;
      ns.connection.message = data;
      getConnected_ = null;
    });

For more like this, see Google Apps Scripts snippets. Why not join our community , follow the blog, twitter, G+ .You want to learn Google Apps Script?

Learning Apps Script, (and transitioning from VBA) are covered comprehensively in my my book, Going Gas - from VBA to Apps script, available All formats are available now from O'Reilly,Amazon and all good bookshops. You can also read a preview on O'Reilly

If you prefer Video style learning I also have two courses available. also published by O'Reilly.
Google Apps Script for Developers and Google Apps Script for Beginners.



Comments