BoJoe22
BoJoe22

Reputation: 95

Angular Service asynchronous singleton for UI-Router

I am using UI-Router for an Angular app. I have data which I am pulling through an $http asynchronous call that is used to create a connection object of sorts. My goal is to have some way of creating this object and making it available as a singleton. Although the chances of it happening are rare, I am looking to avoid timing issues where the socket property in the Service is assigned asynchronously. When a different state in the router wants to get or create another connection object, it will do so through the singleton so that the same connection can be used instead of creating another connection.

Below is a skeleton of some code that I am attempting to get to work. I'm not sure if something can be done with the resolve of the state.

Any thoughts or suggestions on how this can be done?

var myapp = angular.module('me', ['ui.router'])
.service('connectionService', ['$http', function($http) {
    var socket;

    // Somehow call and store result of $http in a singleton
    // Would the $http.get get put in some kind of function? If so, how should it get called 
    // and returned so that a user of the service can get the 'socket' object synchronously?
        $http.get("something/that/returns/data")
            .success(function (result) {
                this.socket= createConnection(result.data);
            })
            .error(function (err) {
                //handle error
            })

    var getSocket= function() {
        if (!this.socket) {
            return createNewSocket();
        } else {
            return this.socket;
        }
    }
}])

myapp.config(function($stateProvider, $urlRouterProvider) {
    $urlRouterProvider.otherwise("home");

    $stateProvider
        .state('home', {
            url: "/home",
            templateUrl: "home.html",
            resolve: {
                socket: // ??? Can something be done here ???
            },
            controller: function(socket) {
                // I can haz socket?
            }
        })
        .state('nextState', {
            url: "/next",
            templateUrl: "next.html",
            resolve: {
                socket: // Should be the same socket object as from the home state
            },
            controller: function(socket) {
                // I can haz same socket?
            }
        })
})

Update

In the process of trying to explain the issue better, I have changed the original code a couple of times. The comments people have left corrected definite errors I was having. I have reverted the above code to the way it originally was, and have added new code below. The issue that I am still having is that the resolve on the ui-router state isn't resolving before entering the controller. So, when calling an emit() on the socket object that is supposed to be resolved, I an error of "Cannot read property 'emit' of undefined". I also still want the socket objects to be the same object between the states. I would appreciate any other suggestions or comments to steer me in the right direction.

var myapp = angular.module('me', ['ui.router'])
.service('connectionService', ['$http', function($http) {
    var socket;

    // Somehow call and store result of $http in a singleton
    // Would the $http.get get put in some kind of function? If so, how should it get called 
    // and returned so that a user of the service can get the 'socket' object synchronously?
    $http.get("something/that/returns/data")
        .then(function (result) {
            socket = createConnection(result.data);
        }, function (err) {
            // handle error
        });

    var getSocket = function() {
        if (!socket) {
            // handle error
        } else {
            return socket;
        }
    }
}])

myapp.config(function($stateProvider, $urlRouterProvider) {
    $urlRouterProvider.otherwise("home");

    $stateProvider
        .state('home', {
            url: "/home",
            templateUrl: "home.html",
            resolve: {
                socket: function(connectionService) {
                    return connectionService.getSocket();
                }
            },
            controller: function(socket) {
                socket.emit("some msg");
            }
        })
        .state('nextState', {
            url: "/next",
            templateUrl: "next.html",
            resolve: {
                socket: function(connectionService) {
                    return connectionService.getSocket();
                }
            },
            controller: function(socket) {
                socket.emit("some other msg");
            }
        })
})

Update 2

Working solution, but this is not as elegant as Muli Yulzari's answer that has been accepted as the correct answer.

I looked into creating my own promise and returning that as something for ui-router to resolve. Based on all of my debugging and stack traces, there is only one connection created among the states and the entering into the controller is deferred until the connection is resolved. So, I believe this is a working solution. I also have a version of the code that handles an onConnect event on the connection before resolving the promise, but I have left it out for the sake of brevity.

var myapp = angular.module('me', ['ui.router'])
.service('connectionService', ['$http', '$q', function($http, $q) {
    var socket;

    this.getSocket = function() {
        if (!socket) {
            var deferred = $q.defer();

            $http.get("something/that/returns/data")
                .then(function (result) {
                    socket = createConnection(result.data);
                    deferred.resolve(socket);
                }, function (err) {
                    // handle error
                    deferred.reject(socket);
                });

            return deferred.promise;
        } else {
            return socket;
        }
    }
}])

myapp.config(function($stateProvider, $urlRouterProvider) {
    $urlRouterProvider.otherwise("home");

    $stateProvider
        .state('home', {
            url: "/home",
            templateUrl: "home.html",
            resolve: {
                socket: function(connectionService) {
                    return connectionService.getSocket();
                }
            },
            controller: function(socket) {
                socket.emit("some msg");
            }
        })
        .state('nextState', {
            url: "/next",
            templateUrl: "next.html",
            resolve: {
                socket: function(connectionService) {
                    return connectionService.getSocket();
                }
            },
            controller: function(socket) {
                socket.emit("some other msg");
            }
        })
})

Upvotes: 1

Views: 351

Answers (1)

Muli Yulzary
Muli Yulzary

Reputation: 2569

To my understanding, you want to initialize the socket according to data from $http.get everytime the socket instance is invalid but do so asynchronously while the app loads.

.service('connectionService', ['$http', '$q', function($http, $q) {
    //For caching the socket
    var socket;

    //Do the first get
    tryGetData();

    //tryGetData executes $http.get and returns a promise that when fulfilled -
    //returns the new socket instance.
    function tryGetData(){
     return $http.get("something/that/returns/data")
        .then(function (result) {
            return socket = createConnection(result.data);
        }, function (err) {
            // handle error
        });
    }

    //Whenever getSocket gets called, it first checks if the socket is instantiated. if not -
    //assumes the $http.get failed and tries again to instantiate the socket calling
    //$http.get again in the process.
    //this function returns a promise for controllers to rely on.
    this.getSocket = function(){
     if(socket) return $q.resolve(socket);
     return tryGetData();
    }
}]);

The rest of your code looks fine.

Upvotes: 1

Related Questions