I use nodejs with socket.io and angularjs on client. I picked up angular-socketio example from the Internet and added disconnect
method to It.
Socket se
[Update]
$$phase
is an internal, private variable to Angular, and thus you should not really depend on it for things like this. Igor describes, in another answer, some suggestions for handling this which should be used instead (I hear he knows a thing or two about Angular. ;)
When models change and events fire from within the Angular framework, Angular can do dirty tracking as necessary and update any necessary views. When you want to interact with code outside of Angular, you have to wrap the necessary function calls in the $apply
method of a scope, so that Angular knows something is happening. That's why the code reads
$rootScope.$apply(function () {
callback.apply(socket, args);
});
and so forth. It's telling Angular, "take this code that normally wouldn't trigger Angular view updates, and treat it like it should."
The problem is when you call $apply
when you're already in an $apply
call. For example, the following would throw an $apply already in progress
error:
$rootScope.$apply(function() {
$rootScope.$apply(function() {
// some stuff
});
});
Based on your stack trace, it looks like some call to emit
(which already uses $apply
) triggered a call to on
(which also uses $apply
). To fix this problem, we need to only call $apply
if an $apply
is not already in progress. Thankfully, there is a property on the scope called $$phase
that can tell us if a dirty check is in progress.
We can easily build a function that takes a scope and a function to run, and then runs the function with $apply
only if one isn't already in progress:
var safeApply = function(scope, fn) {
if (scope.$$phase) {
fn(); // digest already in progress, just run the function
} else {
scope.$apply(fn); // no digest in progress, run the function with $apply
}
};
Now we can replace calls to
$rootScope.$apply(function...);
to
safeApply($rootScope, function...);
For example, to modify the code you have above,
angular.module('app')
.factory('socket', ['$rootScope', function ($rootScope) {
var safeApply = function(scope, fn) {
if (scope.$$phase) {
fn(); // digest already in progress, just run the function
} else {
scope.$apply(fn); // no digest in progress, run with $apply
}
};
var socket = io.connect();
return {
on: function (eventName, callback) {
socket.on(eventName, function () {
var args = arguments;
safeApply($rootScope, function () {
callback.apply(socket, args);
});
});
},
emit: function (eventName, data, callback) {
socket.emit(eventName, data, function () {
var args = arguments;
safeApply($rootScope, function () {
if (callback) {
callback.apply(socket, args);
}
});
})
},
disconnect: function () {
socket.disconnect();
},
socket: socket
};
}]);
The core of the problem in this (just like in most of the other cases) is that the on
method is called asynchronously most of the time (good!) but also synchronously in some cases (bad!).
When you call socket.disconnect()
from your application (from within a controller which lives in the "angular context") it synchronously fires the disconnect event which then propagates into the on
method which is designed to open the boundary into the angular context. But since you are already in the angular context, angular complains with the error you mentioned.
Since this issue is specific to the disconnect call the best options here are to
Example code:
angular.module('app')
.factory('socket', ['$rootScope', function ($rootScope, $timeout) {
var socket = io.connect();
return {
on: function (eventName, callback) {
socket.on(eventName, function () {
var args = arguments;
$rootScope.$apply(function () {
callback.apply(socket, args);
});
});
},
emit: function (eventName, data, callback) {
socket.emit(eventName, data, function () {
var args = arguments;
$rootScope.$apply(function () {
if (callback) {
callback.apply(socket, args);
}
});
})
},
disconnect: function () {
$timeout(socket.disconnect, 0, false);
},
socket: socket
};
}]);
or
angular.module('app')
.factory('socket', ['$rootScope', function ($rootScope) {
var socket = io.connect(),
disconnecting = false;
return {
on: function (eventName, callback) {
socket.on(eventName, function () {
var args = arguments;
if (!disconnecting) {
$rootScope.$apply(function () {
callback.apply(socket, args);
});
} else {
callback.apply(socket, args);
}
});
},
emit: function (eventName, data, callback) {
socket.emit(eventName, data, function () {
var args = arguments;
$rootScope.$apply(function () {
if (callback) {
callback.apply(socket, args);
}
});
})
},
disconnect: function () {
disconnecting = true;
socket.disconnect();
},
socket: socket
};
}]);