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 service:
angular.module('app')
.factory('socket', ['$rootScope', function ($rootScope) {
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 () {
socket.disconnect();
},
socket: socket
};
}]);
Controller:
angular.module('app')
.controller('Controller', ['$scope', 'socket', function ($scope, socket) {
socket.emit('register')
socket.on('connect', function () {
console.log('Socket connected');
});
socket.on('disconnect', function () {
console.log('Socket disconnected');
});
socket.on('register', function (reginfo) {
console.log('Register: %s, cname=%s', reginfo.ok, reginfo.cname);
socket.disconnect(); // <-- this line throw Error
});
socket.on('last', updateSnapshot);
socket.on('state', updateSnapshot);
function updateSnapshot(snapshot) { ... }
}]);
But when I try to disconnect use this method I catch Error:
Error: $apply already in progress
at Error (<anonymous>)
at beginPhase (http://localhost:4000/scripts/vendor/angular.js:8182:15)
at Object.$get.Scope.$apply (http://localhost:4000/scripts/vendor/angular.js:7984:11)
at SocketNamespace.on (http://localhost:4000/scripts/services/socket.js:10:32)
at SocketNamespace.EventEmitter.emit [as $emit] (http://localhost:4000/socket.io/socket.io.js:633:15)
at Socket.publish (http://localhost:4000/socket.io/socket.io.js:1593:19)
at Socket.onDisconnect (http://localhost:4000/socket.io/socket.io.js:1970:14)
at Socket.disconnect (http://localhost:4000/socket.io/socket.io.js:1836:12)
at SocketNamespace.<anonymous> (http://localhost:4000/scripts/controllers/controller.js:38:34)
at on (http://localhost:4000/scripts/services/socket.js:11:34)
And I don't understand where to dig…
[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
};
}]);
If you love us? You can donate to us via Paypal or buy me a coffee so we can maintain and grow! Thank you!
Donate Us With