Sonic1015 Sonic1015 - 3 months ago 7
AngularJS Question

AngularJS multiple Controllers with one Service: Asynchronous errors

I am getting some asynchronous errors when executing a function on my controllers near-simultaneously. Each controller takes some data, and calls a method in a service for testing. The service returns a promise to the controller, manipulates the data passed in, then resolves the promise. The code outline for the service looks like so:

<!-- language: lang-js -->
//Service that our controller can access
app.service("testing", function($timeout, $q) {

//Test function which takes a group, and returns a promise with the result
this.Test = function(resultsLocation, testList, testFunction) {
//promise we are returning
var deferred = $q.defer();
var i = 0;

//TestCallback loop
TestCallBack = function(testList) {
if (i < testList.length) {

//perform a test on one item of the list
testFunction(testList[i]).then(function() {
//push result back to controller
resultsLocation.push(testList[i].result);
i++;

//show result of that one item with scope update.
//also looks visually pleasing to see test come in
//one at a time
$timeout(function() {
TestCallBack(testList);
}, 100);
});
} else {
//we are done. Resolve promise
deferred.resolve("Done");
}
};

//initiate loop
TestCallBack(testList);

//return promise
return deferred.promise;
};

});//testing Service


And then I have a few controllers that roughly look like this:

<!-- language: lang-js -->
//Peripheral
app.controller("peripheral#", function($scope, testing) {
//self stuff
$scope.Title = "Peripheral#";
$scope.Summary = "";
$scope.Results = new Array();

//initial lin tests
var DiagnosticsList = [
//test1
//test2
//etc...
];

//Tests routine
$scope.Testing = function() {
//reset results
$scope.Results = new Array();
$scope.Summary = "Testing...";

//Do Tests
testing.Test($scope.Results, DiagnosticsList, CustomTestingFunction1).then(
function(result) {
$scope.Summary = "Testing...";
},
function(error) {
console.log("Error testing Peripheral1");
}
);
};
});


"Testing" is called on a button press in the html. The problem is if controller1 calls "Testing", then controller2 calls "Testing", the promise will never be resolved in controller1. Worse still, some of the test results are pushed into controller 2's results.

Perhaps I'm missing something, but I could've sworn I read somewhere that a service will be it's own instance when a controller has it.

Anyways, here's a plunker demonstrating the behavior: https://plnkr.co/edit/fE5OD35LaXHWrhv0ohq2?p=preview

Pressing "Test" individually is fine, but if you press "Test" while the other controller is testing, you'll get odd behavior such as values being mixed up, and the first controller will never finish testing.

Answer

The problem is that the service put the testCallBack function on global scope. Instead declare the function on the service function scope.

app.service("testing", function($timeout, $q) {

    //Test function which takes a group, and returns a promise with the result
    this.Test = function(resultsLocation, testList, testFunction) {
        //promise we are returning
        var deferred = $q.defer();
        var i = 0;

        //TestCallback loop
    //DONT use global scope
    //TestCallBack = function(testList) {
    //INSTEAD use function scope
    function TestCallBack(testlist) {
            if (i < testList.length) {

The DEMO on PLNKR


So just to clarify, declaring a function as a var puts it on the global scope even when declared inside a function?

The error was setting a value to an undeclared variable.

Assigning a value to an undeclared variable implicitly creates it as a global variable (it becomes a property of the global object) when the assignment is executed. The differences between declared and undeclared variables are:

  1. Declared variables are constrained in the execution context in which they are declared. Undeclared variables are always global.

  2. Declared variables are created before any code is executed. Undeclared variables do not exist until the code assigning to them is executed.

  3. Declared variables are a non-configurable property of their execution context (function or global). Undeclared variables are configurable (e.g. can be deleted).

Because of these three differences, failure to declare variables will very likely lead to unexpected results. Thus it is recommended to always declare variables, regardless of whether they are in a function or global scope. And in ECMAScript 5 strict mode, assigning to an undeclared variable throws an error.1

In this case, when the service.Test function was called the second time, the global value TestCallBack was being replaced by a reference to the anonymous function in the second instantiation of the service.Test function. The anonymous TestCallBack function refers to a closure2 of the service.Test function. A closure is a special kind of object that combines two things: a function, and the environment in which that function was created. So functions scheduled by the first instantiation were getting their i and resultsLocation switched to the closure of the second instantiation of the service.Test function.

Comments