1

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");
            }
        })
})
8
  • In angular, services are singletons, so this would indeed be what you want. However, you have some issues with the way that your service is built; specifically, your use of .this inside the callback won't work as you intend, because .this refers to the callback, not the parent service. Also, you should consider switching to .then instead of the deprecated .success. Commented Jun 13, 2016 at 20:24
  • Removed deprecated .success and switched to .then promises. Commented Jun 13, 2016 at 20:45
  • 1
    you are still using .this inside the promise, which is the main part that won't work. Commented Jun 13, 2016 at 20:46
  • In angular services and factory are singleton. Commented Jun 13, 2016 at 20:50
  • If you are new to Javascript, then you will want to spend some time reading about how this works, as it is a common source of bugs. Commented Jun 13, 2016 at 20:50

1 Answer 1

1

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.

Sign up to request clarification or add additional context in comments.

6 Comments

This is close, but the tryGetData() does not return a promise that the ui-router's resolve can handle. I have a solution that works where I create a $q deferred object and return the promise of that deferred object. I will post that solution, but want to give you credit for your help.
Yes, $http does return a promise. But the way it is written here, the tryGetData() would not return that promise because .then() is already called. Not sure how to explain it with proper terminology. Also, I needed a way to process the data coming back from $http.get before returning the promise result. Hence I needed to catch the $http promise and rethrow another one. At least that was my observation. I could be completely wrong...
Also it is worth noting that since promises are monads this is exactly why you can perform chaining like this.
Awesome. I don't know if I neglected to reload my code or something when I tried this earlier. It's definitely working now, and certainly cuts out all the extra stuff that I added. I've certainly learned a lot. Thank you for all the help!
|

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service and acknowledge you have read our privacy policy.

Start asking to get answers

Find the answer to your question by asking.

Ask question

Explore related questions

See similar questions with these tags.