Integrating AngularJS and SignalR: The Connection Provider

When I began the migration of FunnelFire’s frontend application from Knockout to AngularJS, I was majorly lacking in code organizational practice. Knockout, an amazing and fast framework for data binding with an easy-to-use syntax was very beginner friendly and did not force you to modularize and organize your code in a logical manner. The hardest part I found in swapping to AngularJS was understanding how to properly use SignalR, a real-time connection manager between the client’s browser and our web servers. I solved this with the creation of a connection provider and separating my client-server communication into services.

Naturally, I integrated SignalR into our new angular website wrong about five times before I found a pattern that seemed to be perfect for Angular by leveraging Angular’s $q in order to ensure I was primarily dealing with one type of promise throughout my project.

In order to properly integrating these two is to understand the differences between providers, services, factories, and so on. There are many better blog posts on this topic, my favorite so far being this one.

The goal of writing a connection provider is simple:

  1. Be able to configure SignalR before it is loaded, such as adding to the query string and setting max delays for reconnect and such
  2. Not have to care about the state of the Signalr connection (started, disconnected, reconnected) inside of controller / service calls.
  3. Be able to rely on native angular promises while making SignalR server calls.
  4. Having a very organized, AngularJS-style ecosystem.

Using a Provider to Configure SignalR

In Angular, the use of a provider allows us to configure a service (be that an angular.service or angular.factory service) before it is injected into another service, controller, directive, or whatnot.

Before setting up a provider, let’s declare an app for the purpose of this demo

var myApp = angular.module("myApp", []);  

We then create a provider for our connection:

myApp.provider('connection', function Connection() {  
    var connection = $.connection,
        reconnectDelay = 1500,
        maxReconnectDelay = 60000;

    // allows you to set logging on before the connection runs
    this.showLogging = function () {
        $.connection.hub.logging = true;
    };

    // used to override the default values
    this.setup = function (queryString, delay, maxDelay) {
        reconnectDelay = delay || reconnectDelay;
        maxReconnectDelay = maxDelay || maxReconnectDelay;

        $.connection.hub.qs = queryString;
    };

    // Used to get the connection to add client callbacks, if so desired, in the config stage
    this.getConnection = function () {
        return connection;
    };

    // This is what is returned when we inject 'connection'
    this.$get = ['$q', '$timeout',
            function connectionService($q, $timeout) {
            var self = this,
                failedConnectionAttempts = 0,
                loadedDefered = $q.defer(),
                isLoaded = loadedDefered.promise,
                loading = false,
                initialized = false;

            function whenReady(serverCall) {
                return isLoaded.then(function () {
                    return $q.when(serverCall());
                });
            }

            function init() {
                // if we are currently loading, abort
                if (loading) {
                    return;
                }

                // if we have not yet been initialized (ie: we are reconnecting)
                // then we need to setup the disconnection event
                if (!initialized) {
                    initialized = true;

                    connection.hub.disconnected(function () {
                        loadedDefered = $q.defer();
                        isLoaded = loadedDefered.promise;

                        loading = false;
                        var newDelay = reconnectDelay * ++failedConnectionAttempts;
                        $timeout(init, Math.min(Math.max(reconnectDelay, newDelay), maxReconnectDelay));
                    });
                }

                // set loading to be true now that we're handling the connection start
                loading = true;

                connection.hub.start().done(function () {
                    loading = false;
                    // resolve the loading defered so that 
                    loadedDefered.resolve();
                    failedConnectionAttempts = 0;
                }).fail(function () {
                    /// <summary>Panics; figure out what to do here later</summary>
                    loadedDefered = $q.defer();
                    isLoaded = loadedDefered.promise;
                });
            }

            init();

            // so that we can say `connection.ready().then(function() { return myServerCall(); });`
            self.ready = whenReady;
            self.hubs = connection;

            return self;
    }];

    return this;
});

Using a provider allows us to configure the provider before it is injected into services and controllers.

// In your main.js file, or wherever you setup the providers
myApp.config(["connectionProvider",  
    function (connectionProvider) {
        if (window.location.hostname === "localhost") connectionProvider.showLogging();
        connectionProvider.setup("?accessor=browser", 1500, 60000);
}]);

In order to keep the data-manipulation and the views separate, we place our back-and-forth between the server and client in a service.

// An example of moving your client-to-server data into a service
myApp.service('myData', ['connection',  
    function (connection) {
        var self = this,
            ready = connection.ready,
            myHubServer = connection.hubs.myHub.server;

        this.getMyData = function () {
            return ready(function () {
                return myHubServer.getMyData();
            });
        };

        return this;
    }]);</pre>

Which we then call in the controller:

// Which we may then use in the controller
myApp.controller('MyController', ['$scope', 'myData',  
    function ($scope, myData) {
        $scope.someDataArray = [];

        myData.getMyData().then(function (allMyThings) {
            $scope.someDataArray = allMyThings;
        });
    }]);

language-javascript

Calling myData.getMyData() is returning a $q promise, so you are only working with one type of promise due to the wrapper in the connection.ready call.

All code from this post is contained within this gist.

Happy Coding!


Currently Drinking: Guatemalan El Tambor from Toby’s Estate, made in a Bodum Brazil 3 cup French Press. It’s very tasty, light, and beginner friendly; heavy cream brings out a slightly hot-chocolate feel to it; absolutely delicious.