Improve this AngularJS factory to use with socket.io - angularjs

I want to use socket.io in AngularJS.
I found the following factory:
app.factory('socket', 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);
}
});
})
}
};
and it is used in the controller like:
function MyCtrl($scope, socket) {
socket.on('message', function(data) {
...
});
};
the problem is that each time the controller is visited another listener is added, so when a message is received it is handled multiple times.
what can be a better strategy to integrate socket.io with AngularJS ?
EDIT: I know that I can return nothing in the factory and do the listening there, then use $rootScope.$broadcast and $scope.$on in the controllers, but it doesn't look like a good solution.
EDIT2: added to the factory
init: function() {
socket.removeAllListeners();
}
and call it at the beginning of each controller that use socket.io.
still doesn't feel like the best solution.

Remove the socket listeners whenever the controller is destroyed.
You will need to bind the $destroy event like this:
function MyCtrl($scope, socket) {
socket.on('message', function(data) {
...
});
$scope.$on('$destroy', function (event) {
socket.removeAllListeners();
// or something like
// socket.removeListener(this);
});
};
For more information check the angularjs documentation.

You might be able to handle this with a minimal amount of work by wrapping up a Scope and watching for $destroy to be broadcast, and when it is, only removing from the socket the listeners that were added in the context of that Scope. Be warned: what follows hasn't been tested--I'd treat it more like pseudocode than actual code. :)
// A ScopedSocket is an object that provides `on` and `emit` methods,
// but keeps track of all listeners it registers on the socket.
// A call to `removeAllListeners` will remove all listeners on the
// socket that were created via this particular instance of ScopedSocket.
var ScopedSocket = function(socket, $rootScope) {
this.socket = socket;
this.$rootScope = $rootScope;
this.listeners = [];
};
ScopedSocket.prototype.removeAllListeners = function() {
// Remove each of the stored listeners
for(var i = 0; i < this.listeners.length; i++) {
var details = this.listeners[i];
this.socket.removeListener(details.event, details.fn);
};
};
ScopedSocket.prototype.on = function(event, callback) {
var socket = this.socket;
var $rootScope = this.$rootScope;
var wrappedCallback = function() {
var args = arguments;
$rootScope.$apply(function() {
callback.apply(socket, args);
});
};
// Store the event name and callback so we can remove it later
this.listeners.push({event: event, fn: wrappedCallback});
socket.on(event, wrappedCallback);
};
ScopedSocket.prototype.emit = function(event, data, callback) {
var socket = this.socket;
var $rootScope = this.$rootScope;
socket.emit(event, data, function() {
var args = arguments;
$rootScope.$apply(function() {
if (callback) {
callback.apply(socket, args);
}
});
});
};
app.factory('Socket', function($rootScope) {
var socket = io.connect();
// When injected into controllers, etc., Socket is a function
// that takes a Scope and returns a ScopedSocket wrapping the
// global Socket.IO `socket` object. When the scope is destroyed,
// it will call `removeAllListeners` on that ScopedSocket.
return function(scope) {
var scopedSocket = new ScopedSocket(socket, $rootScope);
scope.$on('$destroy', function() {
scopedSocket.removeAllListeners();
});
return scopedSocket;
};
});
function MyController($scope, Socket) {
var socket = Socket($scope);
socket.on('message', function(data) {
...
});
};

I would add a comment to the accepted answer, but i can't. So, i'll write a reply.
I had the same problem and the easiest and simplest answer i found is the one that you can find here, on another post, provided by michaeljoser.
I'll copy it below for convenience:
You have to add removeAllListeners to your factory (see below) and have the following code in each of your controllers:
$scope.$on('$destroy', function (event) {
socket.removeAllListeners();
});
Updated socket factory:
var socket = io.connect('url');
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);
}
});
})
},
removeAllListeners: function (eventName, callback) {
socket.removeAllListeners(eventName, function() {
var args = arguments;
$rootScope.$apply(function () {
callback.apply(socket, args);
});
});
}
};
});
It saved my day, i hope it will be useful to someone else!

create function in your service or factory like below.
unSubscribe: function(listener) {
socket.removeAllListeners(listener);
}
and then call in your controller under the "$destroy" event like below.
$scope.$on('$destroy', function() {
yourServiceName.unSubscribe('eventName');
});
that's solve

I just solved a similar problem before I read this. I did it all in the Service.
.controller('AlertCtrl', ["$scope", "$rootScope", "Socket", function($scope, $rootScope, Socket) {
$scope.Socket = Socket;
}])
// this is where the alerts are received and passed to the controller then to the view
.factory('Socket', ["$rootScope", function($rootScope) {
var Socket = {
alerts: [],
url: location.protocol+'//'+location.hostname+(location.port ? ':'+location.port: ''),
// io is coming from socket.io.js which is coming from Node.js
socket: io.connect(this.url)
};
// set up the listener once
// having this in the controller was creating a
// new listener every time the contoller ran/view loaded
// has to run after Socket is created since it refers to itself
(function() {
Socket.socket.on('get msg', function(data) {
if (data.alert) {
Socket.alerts.push(data.alert);
$rootScope.$digest();
}
});
}());
return Socket;
}])

I tried different ways but nothing worked as expected.
In my app I'm using a socket factory in both the MainController and a GameController. When the user switches to a different view I only want to remove the duplicates events generated by the GameController and leave the MainController running so I cannot use a removeAllListeners function. Instead I discovered a better way to just avoid creating duplicates inside my socket factory:
app.factory('socket', function ($rootScope) {
var socket = io.connect();
function on(eventName, callback) {
socket.on(eventName, function () {
var args = arguments;
$rootScope.$apply(function () {
callback.apply(socket, args);
});
});
// Remove duplicate listeners
socket.removeListener(eventName, callback);
}
function emit(eventName, data, callback) {
socket.emit(eventName, data, function () {
var args = arguments;
$rootScope.$apply(function () {
if (callback) {
callback.apply(socket, args);
}
});
});
// Remove duplicate listeners
socket.removeListener(eventName, callback);
}
return {
on: on,
emit: emit
};
}

Instead of doing app.factory, create a service (singleton) like so:
var service = angular.module('socketService', []);
service.factory('$socket', function() {
// Your factory logic
});
You can then simply inject the service to your app and use it in controllers as you would $rootScope.
Here is a more complete example of how I have this set up:
// App module
var app = angular.module('app', ['app.services']);
// services
var services = angular.module('app.services', []);
// Socket service
services.factory('$socket', ['$rootScope', function(rootScope) {
// Factory logic here
}]);
// Controller
app.controller('someController', ['$scope', '$socket', function(scope, socket) {
// Controller logic here
}]);

Expanding on Brandon's answer above, I've created a service that should additionally 1) strip angular tags like .$$hashKey that gets left on elements, and 2) allows for namespaced sockets like socketsof('..').on('..'
(function (window, app, undefined) {
'use strict';
var ScopedSocket = function (socket, $rootScope) {
this.socket = socket;
this.$rootScope = $rootScope;
this.listeners = [];
this.childSockets = [];
};
ScopedSocket.prototype.removeAllListeners = function () {
var i;
for (i = 0; i < this.listeners.length; i++) {
var details = this.listeners[i];
this.socket.removeListener(details.event, details.fn);
}
for (i = 0; i < this.childSockets.length; i++) {
this.childSockets[i].removeAllListeners();
}
};
ScopedSocket.prototype.on = function (event, callback) {
var socket = this.socket;
var $rootScope = this.$rootScope;
this.listeners.push({event: event, fn: callback});
socket.on(event, function () {
var args = arguments;
$rootScope.$apply(function () {
callback.apply(socket, args);
});
});
};
ScopedSocket.prototype.emit = function (event, data, callback) {
var socket = this.socket;
var $rootScope = this.$rootScope;
socket.emit(event, angular.fromJson(angular.toJson(data)), function () {
var args = arguments;
$rootScope.$apply(function () {
if (callback) {
callback.apply(socket, args);
}
});
});
};
ScopedSocket.prototype.of = function (channel) {
var childSocket = new ScopedSocket(this.socket.of(channel), this.$rootScope);
this.childSockets.push(childSocket);
return childSocket;
};
app.factory('Socket', ['$rootScope', function ($rootScope) {
var socket = $rootScope.socket;
return function(scope) {
var scopedSocket = new ScopedSocket(socket, $rootScope);
scope.$on('$destroy', function() {
scopedSocket.removeAllListeners();
});
return scopedSocket;
};
}]);
})(window, window.app);

I use something like the code below.
socketsService is only instantiated once and I believe Angular takes care of GC the $on's
If you don't like $broadcast/$on, there are some slightly more solid Message Bus implementations for Angular available...
app.service('socketsService', ['$rootScope', function ($rootScope) {
var socket = window.io.connect();
socket.on('info', function(data) {
$rootScope.$broadcast("info_received", data);
});
socket.emit('ready', "Hello");
}]);
app.controller("infoController",['$scope',
function ($scope) {
$scope.$root.$on("info_received", function(e,data){
console.log(data);
});
//...
}]);
app.run(
['socketsService',
function (socketsService) {
//...
}]);

I solved this problem by checking whether listener already exists. If you have multiple controllers that are all loaded at the same time (think of different page modules that all utilize socketIO), removing all registered listeners on $destroy would break the functionality of both the destroyed controller and all the controllers that are still loaded.
app.factory("SocketIoFactory", function ($rootScope) {
var socket = null;
var nodePath = "http://localhost:12345/";
function listenerExists(eventName) {
return socket.hasOwnProperty("$events") && socket.$events.hasOwnProperty(eventName);
}
return {
connect: function () {
socket = io.connect(nodePath);
},
connected: function () {
return socket != null;
},
on: function (eventName, callback) {
if (!listenerExists(eventName)) {
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);
}
});
})
}
};
});
This could be further improved by tracking which listeners were registered by which controller and removing only listeners that belong to destroyed controllers to clean up the memory.

I am doing this to avoid duplicated listeners and works pretty well.
on: function (eventName, callback) {
//avoid duplicated listeners
if (listeners[eventName] != undefined) return;
socket.on(eventName, function () {
var args = arguments;
$rootScope.$apply(function () {
callback.apply(socket, args);
});
listeners[eventName] = true;
});
},

I was having the exact same problem of duplicate events after a browser refresh. I was using a 'factory', but switched to use a 'service'. Here's my socket.io wrapper:
myApp.service('mysocketio',['$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);
}
});
});
}
}
}]);
I use this service inside my controller and listen for events:
myApp.controller('myController', ['mysocketio', function(mysocketio)
{
mysocketio.on( 'myevent', function(msg)
{
console.log('received event: ' + msg );
}
}]);
Once I switched from using a factory to using a service, I don't receive duplicates after a browser refresh.

I tried with the above code in my AngularApp and found the events duplicating.
With the same Example from #pootzko using the SocketIoFactory
I have added a unSubscribe(even_name) inside the $destroy of the Controller, that will remove/clear the socketEventListner
var app = angular.module("app", []);
..
..
..
//Create a SocketIoFactory
app.service('SocketIoFactory', function($rootScope){
console.log("SocketIoFactory....");
//Creating connection with server
var protocol = 'ws:',//window.location.protocol,
host = window.location.host,
port = 80,
socket = null;
var nodePath = protocol+'//'+host+':'+port+'/';
function listenerExists(eventName) {
return socket.hasOwnProperty("$events") && socket.$events.hasOwnProperty(eventName);
}
return {
connect: function () {
socket = io.connect(nodePath);
console.log('SOCKET CONNECTION ... ',nodePath);
},
connected: function () {
return socket != null;
},
on: function (eventName, callback) {
if (!listenerExists(eventName)) {
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);
}
});
})
},
unSubscribe: function(listener) {
socket.removeAllListeners(listener);
}
};
});
..
..
..
//Use in a controller
app.controller("homeControl", ['$scope', 'SocketIoFactory', function ($scope, SocketIoFactory) {
//Bind the events
SocketIoFactory.on('<event_name>', function (data) {
});
//On destroy remove the eventListner on socketConnection
$scope.$on('$destroy', function (event) {
console.log('[homeControl] destroy...');
SocketIoFactory.unSubscribe('<event_name>');
});
}]);

Related

What am I doing wrong when setting up socket.io in angular

So i am trying to use socket.io with my controller, but for some reason I cannot get it setup I tried many different things, but cant seem to find that sweet spot. Heres my setup:
var main = angular.module("main", []);
main.factory('socket', 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);
}
});
})
}
};
});
main.controller('Triv', ['$scope', 'socket','$http','$interval',
'$timeout', function ($scope, $http,$interval,socket, $timeout) {
...... })]);
I had similar problems with gulp managed bower installed socket.io.
What I did is I created a symlink in my script source directory
socket.io.js -> ../../bower_components/socket.io-client/socket.io.js
which forced inclusion of socket.io.js at the end of script list

Prevent additional connections to hub in SignalR JS client with Angular

I have an angular resource that looks like this
angular.module('hubProxy', ['ngResource'])
.factory('eventStream', ['$rootScope', function ($rootScope) {
'use strict';
return {
on: function (eventName, callback) {
var connection = $.hubConnection();
var hubProxy = connection.createHubProxy('mainHub');
hubProxy.on(eventName, function () {
var args = arguments;
$rootScope.$apply(function () {
callback.apply(hubProxy, args);
});
});
connection.start(function() { console.log('connection started!'); });
}
};
}]);
I need to call this resource multiple times, but I don't want to keep making new connections
So I either need to disconnect each time, or recycle the existing connection
I think, you need recycle the existing connection.
Try this
angular.module('hubProxy', ['ngResource'])
.factory('eventStream', ['$rootScope', function ($rootScope) {
'use strict';
var connection = $.hubConnection();
var hubProxy = connection.createHubProxy('mainHub');
connection.start(function() { console.log('connection started!'); });
return {
on: function (eventName, callback) {
hubProxy.on(eventName, function () {
var args = arguments;
$rootScope.$apply(function () {
callback.apply(hubProxy, args);
});
});
}
};
}]);

How to avoid asynchronous in service angular - socket io

I tried using debugger to see whats been goin in my factory-angular, i found out whats been causing the error in my socket variable the whole time is the asynchronous behavior of the JS, is there a way to initialize first my socket variable via http-get before loading the functions('on' and 'emit') inside my factory?...
'use strict';
app.factory('getgameip', ['$http', '$location', function($http,$rootScope, $location) {
return {
getGameip: getGameip
};
function getGameip() {
debugger
return $http.get('/getip/test');
}
}]);
app.factory('socket', function ($rootScope,$http,$location,getgameip) {
debugger
getgameip.getGameip().then(function(data) {//1st step
debugger
//3rd step
//this socket variable would be used in the 'on function' and 'emit function' below
var socket = io.connect('http://'+data.data[0].ip+':'+data.data[0].port);
});
return {
on: function(eventName, callback) {
//2nd step
debugger
socket.on(eventName, function () { //i got error on socket io because it has not been initialized yet
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);
}
});
});
}
};
});

Angular promise resolves inside function but not outside

I have a recursive function checking for some data every half second or so. The function returns a promise. Once I find the data, I want to resolve the promise and pass the data as the resolution. The problem is, the promise won't call .then() outside of the function. Here's the fiddle: http://jsfiddle.net/btyg1u0g/1/.
Here's the fiddle code:
Service:
myApp.factory('myService', function($q, $timeout) {
var checkStartTime = false;
var checkTimeout = 30000;
function checkForContent() {
var deferred = $q.defer();
// simulating an $http request here
$timeout(function () {
console.log("Checking...");
if (!checkStartTime) checkStartTime = new Date();
// this would normally be 'if (data)'
if ((new Date()) - checkStartTime > 3000) {
deferred.resolve("Finished check");
checkStartTime = false; // reset the timeout start time
} else {
// if we didn't find data, wait a bit and check again
$timeout(function () {
checkForContent();
}, 400);
}
}, 5);
// then is called in here when we find data
deferred.promise.then(function(message) {
console.log("Resolved inside checkForContent");
});
return deferred.promise;
}
return {
runCheck: function() {
return checkForContent()
}
}
});
Controller:
myApp.controller('MyCtrl', function ($scope, myService) {
$scope.name = 'Superhero';
// then is never called
myService.runCheck()
.then(function (message) {
console.log("Resolved outside checkForContent");
});
});
Check out this fiddle.
The outter $timeout function is named so it can be called from within itself.
$timeout(function inner() {
// ...
Then it is called recursively like this:
$timeout(function () {
inner();
}, 400);

Using WebSockets with AngularJS and $broadcast

I've setup an AngularJS app using websockets and it seems to be working. Here is a summary of whats going on:
var app = angular.module('websocketApp',[]);
app.factory('WebSocket',function($rootScope) {
var websocket = new WebSocket(websocket_url);
var items = [];
websocket.onmessage = function(msg) {
items.push(JSON.parse(msg.data));
$rootScope.$broadcast('new_message');
}
return {
fetchItems: function() {
return items;
}
}
});
app.controller('ItemsCtrl',function($scope,WebSocket) {
$scope.$on('new_message',function() {
$scope.$apply(function() {
$scope.items = WebSocket.fetchItems();
});
});
});
My question is if anyone else has setup an Angular app using websockets and if this implementation is the correct way to go about it or if there is a better solution. I've read many cons on using $broadcast but this seems to be the correct usage of the $broadcast functionality.
The way you have done it seems fine. An alternative way I do it though is to store an event/callback array, and register the events on it that I want to receive specifically.
For example:
angular.module('myapp.services.socket', [])
.factory('io', ['$rootScope', 'globals', function ($rootScope, globals) {
var socket;
var curChannel;
var eventCache = [];
function isConnected() {
return socket && socket.socket.connected;
}
function on(eventName, callback) {
socket.on(eventName, function () {
var args = arguments;
$rootScope.$apply(function () {
callback.apply(socket, args);
});
});
}
function emit(eventName, data, callback) {
socket.emit(eventName, data, function () {
var args = arguments;
$rootScope.$apply(function () {
if (callback) {
callback.apply(socket, args);
}
});
});
}
return {
registerEvent: function(eventName, callback) {
eventCache.push({ name: eventName, cb: callback });
if(isConnected()){
on(eventName, callback);
}
},
emit: function (eventName, data, callback) {
// firstly check that the socket is connected
if(isConnected()){
emit(eventName, data, callback);
}else{
// connect to the server and subscribe upon connection
socket = io.connect(globals.api + ':80');
socket.on('connect', function(){
emit(eventName, data, callback);
// add the events from the cache
for(var i in eventCache){
on(eventCache[i].name, eventCache[i].cb);
}
});
}
}
};
}]);
This way, you can simply register event callbacks whenever you want, by injecting this service, and running:
io.registerEvent('some_event', function(){ /* some logic */ });

Resources