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

 

Recap

Let's quickly summarize what we've learned in parts 1 and 2 of this series:

  • We have APIs to help us check connectivity state, and – to a limited degree – react when it changes.
  • Those APIs suck on their own and can be inherently unreliable.
  • A good abstraction to manage the "true picture" of our connectivity state is the finite state machine (FSM).

In Part 2 we explored a custom-rolled solution and began to see some of the traits of an FSM. Then we brought machina.js into the picture to give us a good utilitarian foundation for building FSMs in JavaScript. However, our "ConnectivityManager" FSM still had problems – the biggest of which being the fact that it was managing both the "single source of truth" of connectivity state, as well as how the app should handle HTTP requests based on that state. This blurring of concerns will tempt us to bake even more behavior into our FSM that shouldn't be there - things like UI changes based on connectivity state. I'm eager to skip ahead to the clean code examples, but I think it's important for us to explore why these responsibilities should be separated from our ConnectivityManager by seeing what happens when we don't do the right thing. This will help you recognize down the road when your FSMs are doing too much.

Hold Your Nose, Design Smells Ahead

Since we're talking state machines, let's introduce a useful tool to help you plan what an FSM should do: the State Transition Table. It's a straightforward way to lay out how the FSM should react to input while in a given state. In the context of our connectivity example, you'd use a state transition table to say "If I'm offline, and I get a 'window.online' input, then transition to 'online' and make a change in the UI" (and yes, I'm intentionally making our FSM handle some concerns it shouldn't… you'll see why).

Take this, for example:

State Input Next State Output
Offline window.online Online UI Change to Reflect Online
Offline applicationCache.downloading Online UI Change to Reflect Online
Offline sendHttpRequest Offline Queue Up HTTP Request
Online window.offline Offline UI Change to Reflect Offline
Online applicationCache.error Offline UI Change to Reflect Offline
Online sendHttpRequest Online Send HTTP Request

The above table helps us to:

  • determine what kinds of input we want each state to handle
  • indicate whether or not the input should result in a transition
  • describe any output that may be a result of handling the input.

To put that in plain English, let's take the first row of data in the table: If we're in the offline state and we get a window.online event, then our FSM will transition to online and the FSM will cause a change in the UI to indicate that we're now online.

More Scope Creep

But wait - remember how the boss said the users wanted a "Go Offline/Go Online" button? Here's what gets added:

State Input Next State Output
Offline go online Online UI Change to Reflect Online
Online go offline Offline UI Change to Reflect Offline

Not a bad change, right? We just added two new inputs. However, wait until the boss's next conversation. Our blurred concerns are about to be painfully obvious…

Enter, your boss:

Boss: "Hey remember how we wanted to keep our users' requests from being ignored when they were offline - so we built this fancy date machine..."

You: "State machine."

Boss: "Right, state machine. Anyway, so we were queueing up their requests and were just resubmitting them when they came back online, right?"

You: "Riiiight….?"

Boss: Well. Marketing wants to be able to say that this app is real time, so we need to add websockets to the picture.

You: …

Boss: …

You: …

Boss: …

You: …

Boss: "Well, I can see you might need a minute to process this. You did a great job adding websockets to our last app, so this should be simple, right?"

You: …

"Stay calm. BREATHE!" (you tell yourself). We can just update our state transition table, right? Sure, but keep in mind that the app is technically online if the websocket backend is available, but the HTTP services aren't, and vice versa. As you try to untangle all the possibilities, your state transition table begins to look like this:

State Input Next State Output
Offline socket.connect OnlineWithWebSocket UI Change to Reflect Online With WebSockets
Offline window.online OnlineWithHTTP? UI Change to Reflect Online (but HTTP or WebSockets?)
Offline applicationCache.downloading OnlineWithHTTP? UI Change to Reflect Online (but HTTP or WebSockets?)
Offline sendRequest Offline Queue Up Request
OnlineWithHTTP window.offline Offline UI Change to Reflect Offline
OnlineWithHTTP applicationCache.error Offline UI Change to Reflect Offline
OnlineWithHTTP sendRequest OnlineWithHTTP Send Request Over HTTP
OnlineWithHTTP socket.connect ?! ??!
OnlineWithWebSocket socket.error Offline UI Change to Reflect Offline
OnlineWithWebSocket sendRequest OnlineWithWebSocket Send Request Over Socket

The need for a websocket vs HTTP heartbeat check becomes apparent. Even worse, you realize that this table doesn't even help you determine where the heartbeat checks should happen. Are they polling continually? Should there be a specific state for running heartbeat checks? What "online" state is the preferred one - HTTP or websockets? Once we're online with HTTP or websockets, when - if ever - should we switch to the other "online" state? Oh, and you forgot for a moment that this was a Cordova app - so you need to add input for the deviceready and resume events - but this could result in six new rows being added to the table. Why? If these events need to be handled in all three states, then it's one row for deviceready and one for resume in each state that exists. Each row we add to the table means more code in the FSM - not necessarily a bad thing on its own of course - but if it's behavior that shouldn't be in the FSM to begin with, then we have a recipe for disaster in the making!

The nice thing about using state transition tables is that they can reveal problems with an FSM before you write any code. When your FSM is concerned with managing more than one kind of state, adding new input or output can cause an exponential growth in complexity (as well as code). Conclusion? This FSM is doing too much.

Gotta Keep 'em Separated

After you recover from the migraine casued by the effort above, you realize that the concept of "online/offline" is separate from "what should the app do with a request?". AND - what the app does with a request is separate from "how should the UI report connectivity state?". So, let's start with what we know: we need an FSM that reports to us what the connectivity state is, and it needs to take our heartbeat check(s) into account. For now, let's focus on HTTP connectivity and leave websockets out (we'll add it later):

State Input Next State Output
Probing heartbeat Online Event Indicating We're Online
Probing no-heartbeat Disconnected Event Indicating We're Disconnected
Probing go.offline Offline Event Indicating We're Offline
Online window.offline Probing Event Indicating We're Probing
Online appCache.error Probing Event Indicating We're Probing
Online request.timeout Probing Event Indicating We're Probing
Online go.offline Offline Event Indicating We're Offline
Disconnected go.online Probing Event Indicating We're Probing
Disconnected window.online Probing Event Indicating We're Probing
Disconnected appCache.downloading Probing Event Indicating We're Probing
Disconnected go.offline Offline Event Indicating We're Offline
Offline go.online Probing Event Indicating We're Probing

Using the above table, you can see:

  • We've added a state that is specifically for running our heartbeat check: probing (which, ironically, is something Developer Advocates become more familiar with as they travel and interact more with the TSA, but I digress…). Anytime we enter the probing state, we'll have an "entry action" that kicks off the heartbeat check (more on this in a moment).
  • We have two kinds of offline states: offline (for when the user deliberately chooses to work offline) and disconnected (for "Oops, we lost connectivity!").
  • We've changed how we react to (for example) window offline and online events. Before we were just blindly trusting an offline event to mean that we were really-undoubtedly-one-hundred-percent-I-promise offline. However, what if the user is on a commuter train that just passed through a tunnel? They might have lost connectivity for only a few seconds or less! With the introduction of the probing state, we can now express our well-founded paranoia about inherently unreliable APIs in code! No more blind trust - if we are in an online state and our FSM receives input that indicates we might be offline, the FSM transitions to probing to find out for real. In the same way, if we are in the disconnected state and our FSM receives input that indicates we might be online, the FSM transitions to probing to really find out.

So let's see our freshly minted HTTP-focused connectivity FSM using machina.js (be sure to read the comments in the snippet below as well!):

    // We're getting a customized FSM constructor which we
    // can use later to create an instance. Please note that
    // I'm leaving off any "wrapper methods" at the top of
    // the FSM (methods that would wrap the "handle" call).
    // For the sake of keeping this example somewhat simple, 
    // we'll just use the handle call directly...
    var HttpConnectivityFsm = machina.Fsm.extend({
    
        // we'll assume we're offline and let the app
        // determine when to try and probe for state
        initialState : "offline",

        // The initialize method is invoked as soon as the
        // constructor has completed execution. Here we
        // set up listeners to the various events that
        // could indicate changes in connectivity
        initialize : function () {
            var self = this;
            
            // The "stethoscope" is simply an object that can make a
            // request to a pre-determined HTTP endpoint and emit a
            // heartbeat event if the request is successful, or a
            // no-heartbeat event if it fails.
            _.each( ['heartbeat', 'no-heartbeat'], function ( eventName ) {
                self.stethoscope.on( eventName, function () {
                    self.handle.call( self, eventName );
                } );
            } );
            
            $( window ).bind( "online", function () {
                self.handle( "window.online" );
            });

            $( window ).bind( "offline", function () {
                self.handle( "window.offline" );
            });

            $( window.applicationCache ).bind( "error", function () {
                self.handle( "appCache.error" );
            });

            $( window.applicationCache ).bind( "downloading", function () {
                self.handle( "appCache.downloading" );
            });
            
            $( document ).on( "resume", function () {
                self.handle( "device.resume" );
            });
        },

        states : {
            probing : {
            
                // the "_onEnter" handler is a special machina
                // feature that gets executed as soon as you
                // enter the state. This is our "entry action".
                _onEnter : function () {
                    this.stethoscope.checkHeartbeat();
                },
                
                // We're using a shortcut feature of machina here.
                // If the only action of an input handler is to
                // transition to a new state, then the value of the
                // handler can be the string name of the state to
                // which we should transition, instead of a function.
                heartbeat      : "online",
                "no-heartbeat" : "disconnected",
                "go.offline"   : "offline",
                
                // the "*" is a special handler in machina that will
                // be invoked for any input that's not explicitly
                // handled by another handler in the same state.
                "*" : function () {
                    this.deferUntilTransition();
                }
            },

            online : {
                "window.offline"  : "probing",
                "appCache.error"  : "probing",
                // the request.timeout event could be hooked into some
                // customization of $.ajax that causes this input to be
                // passed to the FSM when *any* HTTP request times out.
                "request.timeout" : "probing",
                "device.resume"   : "probing",
                "go.offline"      : "offline"
            },

            disconnected : {
                "window.online"        : "probing",
                "appCache.downloading" : "probing",
                "go.online"            : "probing",
                "device.resume"        : "probing",
                "go.offline"           : "offline"
            },

            offline : {
                "go.online" : "probing"
            }
        }
    });
    
    // somewhere in your code, when you need it (assuming
    // you already have an httpStethoscope instance):
    var httpConnectivity = new HttpConnectivityFsm({
        stethoscope: httpStethoscope
    });

OK - so the highlights from above:

  • We start in offline, and we'll let our app decide when to invoke httpConnectivity.handle('go.online');. This could be done when the deviceready event (or a DOM ready event) fires.
  • The events that can notify us of connectivity state changes are wired up in initialize, so the vast majority of the input to this FSM will be automatically driven by events! The user can still manually trigger go.online and go.offline, etc.
  • machina FSMs are event-emitters - so we can emit events internally wherever we need to (in addition to the events machina already provides out of the box). Each state's _onEnter handler is emitting an event matching the name of the state, so it's easy for consumers to subscribe via: httpConnectivity.on('online', callback);, etc.
  • By the way...wondering what an event emitter is? If you're familiar with the Observer pattern (a.k.a. - "events"), the "emitter" is the subject - the thing capable of generating events to which other components can subscribe. Event emitters typically have an API with calls like on(), off() and trigger() (or emit()). If you've ever subscribed a callback to listen to, for example, a click event in the DOM, then you've used an event emitter...
  • As mentioned in the code comments above, when the only thing your input handler does is call this.transition("someState");, machina provides a shortcut syntax where the handler value is just the string name of the state to which you want to transition, instead of a function. You can see this, for example, above in the probing state – the heartbeat input handler value is the string online, rather than a function that causes a transition to online. This helps reduce function boilerplate, making the code a bit more readable & terse where possible.

Finally - Consuming the FSM!

UI Changes

The httpConnectivity FSM above can now act as a "single source of truth" for our HTTP connectivity state. Since it's an event emitter, it's quite simple to have other components in your app subscribe and react to state changes. Let's pretend our app is using a Backbone view to manage a piece of the DOM where the user is presented with connection status information:

var StatusView = Backbone.View.extend({
    el: "#conn-status",
    
    initialize: function() {
        var self = this;
        httpConnectivity.on("transition", function( data ) {
            self.$el.removeClass( data.fromState ).addClass( data.toState );
        });
    },
    
    // render implemention, other methods, etc…
});

Communications Management

In Part 2 of this series, we had our "ConnectivityManager" queueing up HTTP requests while we were offline, and then replaying them when we were online. Of course, the real world is hardly ever so naive, but this simplicity makes it easier to convey the key concepts. We're going to take that behavior - which used to be part of our connectivity FSM - and place it in its own "AjaxManagement" FSM. The "AjaxManagement" FSM will then be able to subscribe to the httpConnectivity FSM's events and adjust how it manages HTTP requests as the connectivity state changes. Despite the naïveté, this will give you an idea of how you could begin to tackle sophisticated communications management for offline vs online.

// helper function used by the FSM below
var sendAction = function(options) {
    $.ajax(options).done( function () {
        this.emit( "http-success", options);
    }).fail( function () {
        this.emit( "http-error", options);
    });
};

var AjaxManagementFsm = machina.Fsm.extend({

    initialize: function() {
        var self = this;
        // we cause this FSM to transition based on the
        // state reported by the httpConnectivityFsm
        self.httpConnectivityFsm.on("transition", function( data ) {
            if( data.toState === "online" ) {
                self.handle( "transportAvailable" );
            } else if ( data.toState === "offline" || 
                        data.toState === "probing" ||
                        data.toState === "disconnected" ) {
                self.handle( "transportUnavailable" );
            }
        });
        self.initialState = (self.httpConnectivityFsm.state === "online") ? 
            "sending" : 
            "queueing";
    },
    
    initialState: "queueing",
    
    states: {
        queueing: {
            sendHttpRequest: function() {
                this.deferUntilTransition('sending');
            },
            transportAvailable: "sending"
        },
        sending: {
            sendHttpRequest: function( options ) {
                sendAction.call(this, options);
            },
            transportUnavailable: "queueing"
        }
    },
    
    mayJax : function( options ) {
        this.handle( "sendHttpRequest", options );
    }
});

// And later, when you need it:
var ajaxManager = new AjaxManagementFsm({
    httpConnectivityFsm: httpConnectivityFsm
});

// make a 'safe' AJAX request
ajaxManager.mayJax({
    type     : "GET",
    url      : "some/API",
    dataType : "json",
    timeout  : 5000
});

From the above code, you can see that our ajaxManager is listening to the events emitted by our httpConnectivityFsm. If the httpConnectivityFsm reports that we're online, then the ajaxManager allows requests to be sent via $.ajax();, otherwise, it transitions to queueing and queues up any sendHttpRequest input until it transitions into sending again.

Where Are We?!

We've gone from a thorny problem and bunch of disparate APIs (discussed in Part 1), to a spaghetti-code-nightmare-yet-beginnings-of-a-state-machine (in Part 2), finally arriving at a place where we have one clean FSM monitoring HTTP connectivity state and another managing how HTTP requests get handled. Let's examine some of the benefits we've picked up:

  • Separation of Concerns! UI changes, connectivity monitoring and communications 'management' are now handled by separate components
  • Greater testability due to better separation
  • Greater extensibility - our httpConnectivityFsm can easily be extended to take new events into account that could indicate a change in connectivity state
  • Less brittle - due to better separation, we can now change how our ajaxManager and our StatusView react to changes in connectivity state without ever touching the httpConnectivityFsm (& vice versa)
  • Reusability - since our httpConnectivityFsm is an event emitter, other components in the app (or future ones) can consume the events it produces. No need to litter the app with connectivity-focused code.

"WAIT A SECOND! WHAT ABOUT ADDING WEBSOCKETS INTO THIS?!"

Right, you caught me. Because I just had to go and open that can of worms, I'm going to save that for Part 4 - and then try to not add any more parts to this series…

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.