Question

Using AngularJs, I wrote a factory aiming to handle a WebSocket connection.
Here's its code:

.factory('WebSocketConnection', function () {
        var service = {};
        service.callbacks = [];
        service.connect = function() {
            if(service.ws) 
              return;
            var ws = new WebSocket("ws://localhost:9000/ws");
            ws.onmessage = function (message) {
                angular.forEach(service.callbacks, function(callback){
                    callback(message);
                });
            };
            service.ws = ws;
        };

        service.send = function(message) {
            service.ws.send(message);
        };

        service.subscribe = function(callback) {
          service.callbacks.push(callback);
        };
        return service;
});

Basically, it allows each Angular components like controller or factory/service to register a specific callback, in order to handle messages; hence the callbacks array.
Here's the interesting excerpt of a listening controller:

WebSocketConnection.subscribe(function (message) {  
      $scope.$apply(function () {
         var data = angular.fromJson(message.data);
         $scope.notifications.push(data);
      });
});

So the callbacks array would contain this function.

But...what if I won't need this controller any more at some time? (for instance when I navigate to other page based on another controller)
I would be forced to delete the callback item (this function) from the array each time usage of the controller is left, to avoid useless, maybe conflicting, process of this callback as long as any further messages are handled.
Not handy...

I thought about a way to broadcast event from the $rootScope from the factory, so that a specific controller doesn't have to manage his listeners/subscriptions itself.
But I don't want to involve all the scope tree, including all scopes that are not concerned.

What would be a good practice?

Note: I need to achieve a relation 1-N where 1 is the WebSocket handler (factory) and N, any parallel alive Angular components, each needing to listen to messages.

Was it helpful?

Solution 2

Thanks to @calebboyd who made remind me the existence of the $scope's destroy event, I think I have found a good way to achieve my requirement.

A semi-automatic way to let the controller unsubscribe itself would be to add this piece of code:

$scope.$on("$destroy",function() {
  WebSocketConnection.unsubscribe($scope.$id);      
});

Subscribing mechanism would look like this:

WebSocketConnection.subscribe($scope.$id, function (message) {   //note the $scope.$id parameter
      $scope.$apply(function () {
         var data = angular.fromJson(message.data);
         $scope.notifications.push(data);
      });
});

Therefore, the full factory code would be:

.factory('WebSocketConnection', function () {
        var service = {};
        service.callbacks = {}; //note the object declaration, not Array
        service.connect = function() {
            if(service.ws) 
              return;
            var ws = new WebSocket("ws://localhost:9000/ws");
            ws.onmessage = function (message) {
                angular.forEach(service.callbacks, function(callback){
                    callback(message);
                });
            };
            service.ws = ws;
        };

        service.send = function(message) {
            service.ws.send(message);
        };

        service.subscribe = function(concernedScopeId, callback) {
          service.callbacks[concernedScopeId] = callback;
        };

        service.unsubscribe = function(concernedScopeId) {
          delete service.callbacks[concernedScopeId];
        };

        return service;
});

And that does the trick: each useless callback acting as listener can then be detected and deleted.

OTHER TIPS

I would suggest maintaining your model in a service or factory object. This would enable you to interact with the data as it exists there and not be dependent on the state of your application (what controllers exist) when a message is recieved. It can also allow you to enforce the concept of:

$scope has model instead of $scope as model

That might look something like this:

ws.onmessage = function(event) {
    $rootScope.$apply(function(){
        Service.notifications.push(event.data);
    }
}

and

angular.module('MyApp').controller('MyCtrl', ['$scope', 'Service', 
function($scope, Service) {
    $scope.notifications = Service.notifications; //references are ===
}])

To enable flexibility you can use data contained in the message to determine what injectable/methods need to be updated, then use the $injector.

Licensed under: CC-BY-SA with attribution
Not affiliated with StackOverflow
scroll top