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

Now Where Were We?

I highly recommend you go back and read the first three parts of the series, otherwise, this may not make much sense! When we ended Part 3, we had created an FSM to act as the single-source-of-truth for HTTP connectivity state in our application, and we had another FSM managing communications - making AJAX calls when HTTP connectivity was available, and naively queueing the requests up when the app was offline. However - remember our boss wanted us to throw websockets into the mix. The implication being, if the client supported websockets, and the backend socket connection was available, prefer it over HTTP.

Now our application has "two ways of being online". Sigh.

Our HTTP Connectivity FSM had finally reached the point to where it only focused on a targeted concern. We don't want to bake the web socket connectivity concerns into it, since (as we observed in Part 3) it would likely result in dramatic growth in the number of states and input handlers - a sure sign that our FSM is doing too much. But what can we do?

FSM All the Things

tweet

Whatever Taylor. This is happening.

OK, not really all the things - but all the websocket connectivity things. We can create an FSM that will act as the single-source-of-truth for websocket connectivity, just as our HTTP Connectivity FSM does for HTTP. Once we have that, we'll explore how these FSMs can interact - enabling our application to model more complex behavior, while keeping the concerns tightly focused inside each abstraction.

Web Socket FSM

Let's steal the basic structure of our HTTP Connectivity FSM, but of course alter it to be websocket specific. For the sake of this example, I'm assuming that our web socket stack is using node.js and socket.io:

var SocketConnectivityFsm = 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 
        // send a message to a web socket endpoint & emit a
        // heartbeat event if a reply is received within a
        // specific time frame, or a no-heartbeat otherwise.
        _.each(['heartbeat', 'no-heartbeat'], function (eventName) {
            self.stethoscope.on(eventName, function () {
                self.handle.call(self, eventName);
            });
        });

        var needsProbingFn = function () {
            self.handle("needs-probing");
        };

        // Any time our socket emits one of these events, we're going
        // to play it super-safe and transition to probing to make
        // another heartbeat check (instead of polling for one)
        _.each(
            ['connect', 'connecting', 'connect_failed', 'disconnect',
            'error', 'reconnect', 'reconnect_failed', 'reconnecting'],
            function (eventName) {
                self.socket.on(eventName, needsProbingFn);
            }
        );

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

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

        $(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",
            "device.resume"  : "probing",
            "needs-probing"  : "probing",
            "go.offline"     : "offline"
        },

        disconnected: {
            "window.online" : "probing",
            "go.online"     : "probing",
            "device.resume" : "probing",
            "needs-probing" : "probing",
            "go.offline"    : "offline"
        },

        offline: {
            "go.online" : "probing"
        }
    }
});

// somewhere later, when you need it (assuming you already 
// have a socketConnection & socketStethoscope instance):
var socketConnectivity = new SocketConnectivityFsm({
    socket: socketConnection,
    stethoscope: socketStethoscope
});

Let's Break That Down:

  • Much like our HTTP Connectivity FSM, we have a "stethoscope", whose job is to reach out to the other end of the socket and ask for a reply to verify that the connection is truly active. If we get one within the specified time limit (presumably set when the stethoscope instance is created), a heartbeat event is emitted. Otherwise, a no-heartbeat event is emitted.
  • In our FSM's initialize method, we subscribe to listen for a list of connection-related events that the socket can emit – and if any of those are emitted, we have the FSM handle a needs-probing input (which results in transitioning to probing state).
  • Otherwise - it's very much like the HTTP Connectivity FSM when it comes to the kinds of input each state handles, and how it responds.

Putting it All Together

{Rubs hands together maniacally.} Now we're getting to the fun stuff! While we have connectivity management FSMs for both websockets and HTTP, we need to replace our AJAX Management FSM (created in part 3) with a "Communications Manager" that takes both transports into account. We'll assume that websockets is the preferred transport (after all, marketing and your boss made a big deal about it in Part 3), so we'll use it instead of HTTP if it's available, otherwise we'll fall back to HTTP. If neither are available, we'll blindly (gasp!) queue up everything like before:

// let's assume we have access to both our
// httpConnectivityFsm & our socketConnectivityFsm
// by the time we reach this point….
var CommManagementFsm = machina.Fsm.extend({

    initialize: function () {
        var self = this;
        // Now we listen to the transition event on each of our
        // "connectivity" FSMs. We take the name of whatever state
        // that the FSM is transitioning into, and provide it as
        // input to this FSM. So an http connectivity FSM transition
        // to "probing" would cause this FSM to handle "http.probing".
        self.httpConnectivityFsm.on("transition", function (data) {
            self.handle("http." + data.toState);
        });
        self.socketConnectivityFsm.on("transition", function (data) {
            self.handle("websockets." + data.toState);
        });
    },

    initialState: "queueing",

    states: {
        queueing: {
            _onEnter: function () {
                // We'll check to see if either FSM is online, and 
                // transition if so.
                if (this.socketConnectivityFsm.state === 'online') {
                    this.transition("websockets");
                } else if (this.httpConnectivityFsm.state === 'online') {
                    this.transition("http");
                }
            },
            sendRequest: function () {
                this.deferUntilTransition();
            },
            "websockets.online" : "websockets",
            "http.online"   : "http"
        },
        websockets: {
            sendRequest: function (options) {
                this.socket.emit(options.url, options);
            },
            // a transition to the state that the FSM is already
            // in is ignored - it's a no-op. But we have these
            // inputs marked below, since we want to be explicit
            // that we stay in websockets if these input actions
            // happen. Then, the "*" handler can safely assume
            // that ANY other event means we go back to queueing
            "http.online" : "websockets",
            "websockets.online" : "websockets",
            "*" : "queueing"
        },
        http: {
            sendRequest: function (options) {
                $.ajax(options);
            },
            "http.online" : "http",
            "websockets.online" : "websockets",
            "*" : "queueing"
        }
    },

    // Top level wrapper method so that consumers don't have
    // to call "handle('sendRequest', options)"
    sendRequest: function (options) {
        this.handle("sendRequest", options);
    }
});

It's Alive!

In the initialize method above, you can see that we're listening to both connectivity FSMs' transition events - and we prefix the toState name (which comes on the event data argument) with either "websockets." or "http." and pass it to our CommManagmentFsm instance as input. In other words, if our socketConnectivityFsm transitions to online, then our CommManagementFsm will handle a websockets.online input. Likewise, if our httpConnectivityFsm transitions to probing, our CommManagementFsm will handle a http.probing input.

Based on what we're doing in initialize, the CommManagementFsm is dependent on the httpConnectivityFsm and socketConnectivityFsm instances. In a sense they are in a hierarchy, with the CommManagementFsm being the top node in a tree, with the other two as children that bubble information up to the parent. FSMs are a powerful abstraction on their own, but once you have them working in a hierarchy, amazing potential opens up! The example above keeps it fairly simple - with our CommManagementFsm having 3 possible states: queueing, websockets and http.

anchorman

This FSMs purpose is to answer the question of "How are we communicating with the server at the moment?". It starts in the queueing state, and we can see that upon entering queueing, it checks to see if our socketConnectivityFsm is online. If it is, then our FSM transitions to websockets and any communications are written to the websocket. If not, we check to see if our httpConnectivityFsm is online and, if so, transition to http, with communications being handled via $.ajax. If neither transport is available, we stay in queueing until one of them reports being online.

Taking Advantage of Some machina Features

To Queue or Not to Queue

In the queueing state, if a sendRequest input occurs, we queue it up to be replayed in either http or websockets by calling this.deferUntilTransition(). The only input(s) that can cause a transition in this state are websockets.online and http.online. Once we're in the websockets state, sendRequest input is routed over the socket (and anything that's been queued will get replayed when we transition).

Transitions to Nowhere & Upgrades

You'll notice, too, that our websockets state can handle a http.online and websockets.online input - both are telling machina to transition to websockets (the state we're ALREADY in!). With machina, attempting to transition into the state you are already in results in a no-op (it gets ignored). But WHY did we add those two handlers, then? The answer: because this lets us make the * handler (which handles ANY input not explicitly handled by name already) assume that we need to transition to queueing. So ANY input to the FSM while we're in websockets – other than sendRequest, http.online and websockets.online – results in a transition to queueing. This works almost the exact same way in the http state, with one difference: if a websockets.online input is handled in the http state, we 'upgrade' our transport from HTTP to websockets by transitioning to the websockets state.

Using this is as easy as:

// assuming we have our connectivity FSM instances
var commManager = new CommManagementFsm({
    httpConnectivityFsm : httpConnectivityFsm,
    socketConnectivityFsm : socketConnectivityFsm
});
// and later when we want to make a request… &
// assuming requestData is an object containing
// the necessary info to make a request
commManager.sendRequest(requestData);

The UI Doesn't Have to be Lonely

In Part 3 we had a Backbone view that was listening to our httpConnectivityFsm and swapping out CSS classes to cause a UI change when we were online or offline, etc. If you aren't a Backbone afficianado, don't worry. Many people aren't. It's simply a way for us to manage our view (the HTML currently being displayed) in a very organized fashion.

We can update that Backbone view to listen to our commManager instance if we want:

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

The only other change we'd need to make for the above tweak to work is to make sure we have CSS defined for "queueing", "websockets" and "http" classes.

Fringe Benefits

This setup with our three FSMs working together is pretty flexible. It would be quite simple to add the ability for the user to deliberately turn one transport off (let's say they wanted websockets or nothing, so they want HTTP turned off). We'd simply transition the httpConnectivityFsm to offline and leave it there. Our commManager would react appropriately without any changes to the code. In addition to being flexible, this setup is testable in isolation (each component), in partial isolation (for ex. - a mocked view instance of our Backbone view, with real instances of our FSMs), and/or as a whole (complete with verifying that the DOM changed accordingly as different classes were applied). One other benefit – while I typically loathe heavily subjective arguments about code, I would also argue that this setup is readable. Of course, the reader would need to grasp state machines, and know the basics of machina's API - but once those key details are in place, it doesn't take much to look at any one of these FSMs and clearly understand what's happening. We've also already seen what it looks like to try to handle state on your own. In less you are a fan of giant convoluted switch statements, Machina makes this code a whole lot more manageble.

Tying the Bow

So that's where I'm going to leave it. It was a fun journey through these four blog posts! I expect future versions of machina to provide more direct support for hierarchical FSMs, so keep an eye out for updates if you're interested in that kind of thing. Regardless of what library you use, my biggest goal through this series was to provide an overview of how an abstraction like a finite state machine can help you tie the available (and often incomplete) tools together with your own in order to solve complex problems without spaghettifying your code or sacrificing features. I'd love to hear from you if you think it's been helpful!

You're ready to bend the chaos of managing connectivity to your will using these new found tools and approaches, so fire up Icenium and start building a mobile app today.


About the Author

Jim Cowart

Jim Cowart 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. 

Related Posts

Comments

Comments are disabled in preview mode.