Telerik blogs

This is Part 2 of a four part series where we explore some of the tools available to detect and manage online/offline connectivity in web/mobile applications.

In our last post we looked very briefly at the APIs currently available to help detect connectivity state in mobile/web apps. Our conclusion? Browser APIs have a long way to go in this area! Nevertheless, when used together – and in tandem with a heartbeat check that attempts to talk to a remote endpoint – these tools can be quite useful. But what's the best way to use them 'together'? At the end of part 1, I said that I think a state machine abstraction would work well to manage all of this…but you might be wondering, "What's a state machine?". Before I show you the what and how of finite state machines, I think it's important for us to understand the why of state machines. So, grab some popcorn and prepare to laugh (and hopefully commiserate) as I walk you through what it could look like to solve the connectivity problem without a proper state machine.

Is He Dead, Jim?

As I mentioned in Part 1 of this series, what better way to really tell if you're online than to reach out to a remote endpoint and see if you get a response? This is often referred to as a heartbeat check. The code involved isn't earth-shattering - consider the snippet below that's checking a "hearbeat" url, and if no response occurs, or the response isn't a 200, the hesDeadJim function is invoked.

What heartbeat checks look like…

 

$.ajax({
    type     : "GET",
    url      : "heartbeat",
    dataType : "json",
    timeout  : 5000
}).done( function () {
  notDead();
}).fail( function () {
  hesDeadJim();
});

You can imagine that the notDead() and hesDeadJim() calls above might – among other things – be flipping some sort of state variable between online and offline. But if we want to handle http requests differently if we're offline or online, and we want to keep this nicely wrapped up inside a re-usable component so that these heartbeat checks are not littered imperatively throughout the app, then we need to go ahead and create something simple to keep track of this for us:

var ConnectivityManager = function(settings) {
    this.settings = settings;
    this.state = "Unknown";
    this.queue = [];
};

ConnectivityManager.prototype.checkHeartbeat = function() {
    var self = this;
        self.trigger( 'checking-heartbeat' );
        return $.ajax( self.settings )
                    .done( function () {
                        self.state = 'online';
                    })
                    .fail( function () {
                        self.state = 'offline';
                    });
};

ConnectivityManager.prototype.mayJax = function(options) {
    var self = this;
    self.checkHearbeat().always(function() {
        if(self.state === "online") {
            $.ajax(options);
        } else {
            self.queue.push(options);
        }
    });
};

// Get an instance and pass the hearbeat settings in
var connMgr = new ConnectivityManager({
    type     : "GET",
    url      : "heartbeat",
    dataType : "json",
    timeout  : 5000
});

Ok - so it's a start, but it's not much to look at. When we new up the ConnectivityManager, we can invoke checkHeartbeat() anytime we want to check our connectivity state manually, and it's invoked for us anytime we use the mayJax method. If we're not online, the AJAX requests are being naively queued up (give me a break, the focus is connectivity! We'll get to communication strategies in later posts), otherwise, the request happens as expected.

YUCK! That's expensive! Wouldn't it be great if checkHeartbeat() was invoked for us when something occurs in the client that could indicate we are offline? We can make that happen if we hook into the window.online, window.offline and applicationCache events…

// Just showing the code that's changed….
var ConnectivityManager = function(settings) {
    var self = this;
    self.settings = settings;
    self.state = "Unknown";
    self.queue = [];
    $( window ).on( "online offline", self.checkHeartbeat );
    $( window.applicationCache ).on( "error downloading", self.checkHeartbeat );
};   

ConnectivityManager.prototype.mayJax = function(options) {
    if( this.state === "online" ) {
        $.ajax( options );
    } else {
        self.queue.push( options );
    }
};

Noticing anything missing? Yeah, me too. We're queueing up all these AJAX requests, but not doing anything with them when we move from offline back to online. Let's pretend, for the sake of this example, that it's OK to blindly iterate over the queued up requests in order and transmit them. If this is too awful for you to accept, go look at some cute kittens first, then come back and keep reading. Welcome back - those kittens are adorable, eh? (Don't worry, I would never recommend blindly re-submitting requests like this in a normal app either. Ideally, you'd want some sort of synchronization story - but that's beyond the scope of this post.) Anyway, we end up changing the checkHeartbeat method to this:

ConnectivityManager.prototype.checkHeartbeat = function() {
    var self = this, oldState = self.state;
        self.trigger( 'checking-heartbeat' );
        return $.ajax( self.settings )
                    .done( function () {
                        self.state = 'online';
                        if(self.state !== oldState) {
                            while(self.queue.length) {
                                $.ajax( self.queue.shift() );
                            }
                        }
                    })
                    .fail( function () {
                        self.state = 'offline';
                    });
};

But Wait, There's More!

Your boss reminds you that this is going to be a hybrid mobile app. "Great!", you think to yourself, "Cordova has a connection object I can use, forget this stupid ConnectivityManager thing…." Not so fast. The navigator.connection.type is a property - to use it you'd need to check it before each request. "But wait!", you exclaim, "Doesn't Cordova have a set of events we can hook into for deviceready, resume and all that?" Yes, actually, it does. We can take advantage of those events just like we are the window online/offline events:

// Just showing the code that's changed….
var ConnectivityManager = function(settings) {
    var self = this;
    self.settings = settings;
    self.state = "Unknown";
    self.queue = [];
    $( window ).on( "online offline", self.checkHeartbeat );
    $( window.applicationCache ).on( "error downloading", self.checkHeartbeat );
    $( document ).on( "deviceready resume", self.checkHeartbeat );
};   

Scope Creep

You start to notice, over time, that different devices may or may not fire events like resume, online, etc. exactly when you'd expect them to. Not only that, but you realize you've left yourself wide open for those precious moments that the user wants to do something immediately, but the app is in the middle of a heartbeat check. Then the users want some sort of UI indication that they are offline. As a result, UI behavior finds its way into the ConnectivityManager. Before you know it, it's one noodle shy of the all-you-can-eat-spaghetti-buffet.

Then your boss comes in again:

Boss: "Hey remember how the sales people wanted a visual indicator as to when they're online or offline? Ok, great. So, now they want a button."

You: "A button?"

Boss: "Yeah, you know, so they can deliberately go offline."

You: "Can't they just turn their wifi or mobile data connection off?"

Boss: …

You: …

Boss: …

You: …

Boss: "Just give 'em a 'Go Offline' button for that one app, and it needs to be different from when they accidentally lose connection."

At this point you start thinking that maybe baking the "single source of connectivity state truth" for your app into the same component that's managing communications at such a high level was a bad idea. This one component is drastically changing the app's communications behavior, it's touching the UI, and now it's about to explode with complexity since we've moved beyond a simple choice of "online/offline", now we have this additonal concept of "disconnected". And we still have this nagging feeling about how to handle requests made while we're waiting on a response to a heartbeat check.

Life was so much easier before the W3C teased us with promising, but yet "inherently unreliable" APIs. All too late, you realize that your ConnectivityManager should really be emitting events, rather than taking on the responsibilities of any component concerned with connectivity.

Then, from somewhere deep inside, the universe shows you a picture of what it's doing to you in this very moment:

Rage Against, Er, Um, With the Machines!

There's actually hope, and it's not far off. It's messy, and it's breaking about every rule of encapsulation there is, but our ConnectivityManager is the beginnings of a state machine. So, what exactly is a state machine?

Finite State Machines (FSM)

A finite state machine is a "computational abstraction" – in other words, it's something we use in order to compute a yes or no answer to a question – that is capable of reacting differently to the same input, given a change in its internal state. For "input", think "I want to send an AJAX request". For "state", think "Are we online, offline or disconnected?" Obviously, we want to respond differently to "I want to send an AJAX request" if we're online vs offline, etc.

A Few Basic Traits of an FSM

  • Has a finite number of states in which it can exist
  • Can only exist in one state at a given time
  • Accepts input
  • May produce output
  • Can transition from one state to another

We can keep our app from becoming the next noodle buffet if we apply these concepts in a more structured manner, and stop bleeding other concerns into our ConnectivityManager. Let's see how applying this could help us with the original "online vs offline" concern. We're going to be using a helper library I've written called machina.js. I'm intentionally throwing us into the deep end, but don't worry - we'll go over machina in more detail in Part 3 of this series.

Deus ex machina

machina.js provides some utilities for us to organize our states and determine how input is handled (i.e.- when methods are called, what happens, given the state we're currently in?). On a machina FSM, states are objects, and their members are functions that handle input. Let's look at simplified connectivity FSM:

var connMgr = new machina.Fsm({
    // This is the initial state in which the FSM should start
    initialState: "offline",
    
    // The states object tells machina what states your FSM has
    // Each top-level member of the states object is a state name
    states: {
        offline: {
            // the functions under a specific state like this one are
            // called "input handlers". ""sendHttpRequest" handles
            // input for making an HTTP request.
            sendHttpRequest: function(options) {
                // we'd queue things up….
            },
            goOnline: function() {
                this.transition("online");
            }
        },
        online: {
            sendHttpRequest: function(options) {
                $.ajax(options);
            },
            goOffline: function() {
                this.transition("offline");
            }
        }
    }
});

The object literal argument to the machina.Fsm constructor function is extended over the instance - so our initialState and states values (and any other members) get copied to it. In addition, machina has several helper methods we can use. The transition call in the above example is used to change the state of the FSM. The state property of a machina FSM is simply a string, but you'd never want to change it directly, since transitions can involve events and other behaviors - which wouldn't get invoked if you directly assigned a new value to the state property.

Remember how we were manually queueing up AJAX requests when we were offline in our hand-rolled-cowboy-code version? Well, machina has another helper method we can use to handle that: deferUntilTransition. When you use this method inside an input handler, it allows you to queue up the input, for it to be replayed at a later time. Calling deferUntilTransition() with no arguments means that the input will be replayed as soon as the FSM transitions to any other state. You can optionally pass the string name of the state in which you want the input to be replayed (ex - deferUntilTransition('online')). Notice below that we're calling deferUntilTransition('online') when the sendHttpRequest input is handled in the offline state:

var connMgr = new machina.Fsm({
    initialState: "offline",
    states: {
        offline: {
            sendHttpRequest: function(options) {
                // queue this up until we transition to online
                this.deferUntilTransition('online'); 
            },
            goOnline: function() {
                this.transition("online");
            }
        },
        online: {
            sendHttpRequest: function(options) {
                $.ajax(options);
            },
            goOffline: function() {
                this.transition("offline");
            }
        }
    }
});

Sending Input to the FSM

You can infer from the above code examples that:

  • Our FSM can be in either an offline or online state (you can tell this by looking at the members of the states object)
  • Each state handles a sendHttpRequest input (you can see this member under both offline and online states)
  • online handles a goOffline input
  • offline handles a goOnline input

But how do we tell the FSM to handle input? Well - the low-level way in machina is to use the handle method:

connMgr.handle('sendHttpRequest', {
    type     : "GET",
    url      : "some/api",
    dataType : "json",
    timeout  : 5000
});

Really though, that's UGLY. Most of the time the better approach would be to wrap the call to handle in a method that more expressively describes what's happening. Consider this approach:

var connMgr = new machina.Fsm({
    // …existing states, default state, etc …
    
    // Wrap the "handle" method so we can call this one method
    // directly from the FSM. This ensures the FSM is calling
    // the correct method on the current state, so that we don't
    // have to guess or figure out how to do this from outside
    // of the FSM, *and* it gives a much more meaningful method
    // name for the operation than simply calling "handle"
    sendHttpRequest: function(options) {
        // the handle method calls the specified method name
        // on the *current* state object, with the supplied
        // parameters
        this.handle('sendHttpRequest', options);
    },
    
    // wrap the handle method for goOnline input
    goOnline: function() {
        this.handle('goOnline');
    },
    
    // wrap the handle method for goOffline input
    goOffline: function() {
        this.handle('goOffline');
    }
});

Since the object literal passed to the machina constructor gets extended over the instance, all we have to do is add our own wrapper methods to that object literal. Now consumers of this FSM can call a more semantically meaningful API:

connMgr.sendHttpRequest({
    type     : "GET",
    url      : "some/api",
    dataType : "json",
    timeout  : 5000
});

connMgr.goOffline();
connMgr.goOnline();

(Like me, you might be wishing JavaScript supported "method_missing" about now…)

Are you wondering what happens if you call connMgr.goOffline() while we're already offline? Great question! Our offline state doesn't handle any input for goOffline - so it would result in a "no-op".

What's Next?

So - we survived the journey from cowboy-spaghetti-code to initial FSM concepts. However, our ConnectivityManager still has serious issues – the most critical being that the concerns of "What Connectivity State Are We In?" and "What Should I Do With An HTTP Request?" are really separate concerns, but they are being handled together. In Part 3, we'll look at why we should separate these concerns into different FSMs as well as good reference implementations for FSMs that solve both concerns.

Jim Cowart

About the Author
is an architect, developer, open source author, and overall web/hybrid mobile development geek. He is an active speaker and writer, with a passion for elevating developer knowledge of patterns and helpful frameworks. Jim works for Telerik as a Developer Advocate and is @ifandelse on Twitter.


Comments

Comments are disabled in preview mode.