13

I am new in angular and encounter a catch-22:

Facts:

  1. I have a service that logs my stuff (my-logger).

  2. I have replaced the $ExceptionHandler (of angular), with my own implementation which forwards uncaught exceptions to my-logger service

  3. I have another service, pusher-service, that needs to be notified whenever a fatal message is to be logged somewhere in my application using 'my-logger'.

Problem:

I can't have 'my-logger' be depend on 'pusher' since it will create circular dependency (as 'pusher' uses $http. The circle: $ExceptionHandler -> my-logger -> pusher -> $http -> $ExceptionHandler...)

My attempts:

In order to make these 2 services communicate with each other, I wanted to use $watch on the pusher-service: watches a property on $rootscope that will be updated in my-logger. But, when trying to consume $rootScope in 'my-logger', in order to update the property on which the 'pusher' "watches", I fail on circular dependency as it turns out that $rootscope depends on $ExceptionHandler (the circle: $ExceptionHandler -> my-logger -> $rootScope -> $ExceptionHandler).

Tried to find an option to get, at runtime, the scope object that in its context 'my-logger' service works. can't find such an option.

Can't use broadcast as well, as it requires my-logger to get access to the scope ($rootScope) and that is impossible as seen above.

My Question:

Is there an angular way to have two services communicate through a 3rd party entity ?

Any idea how this can be solved ?

aviad cohen
  • 637
  • 1
  • 6
  • 16

5 Answers5

10

Use a 3rd service that acts as a notification/pubsub service:

.factory('NotificationService', [function() {
    var event1ServiceHandlers = [];
    return {
        // publish
        event1Happened: function(some_data) {
            angular.forEach(event1ServiceHandlers, function(handler) {
                handler(some_data);
            });
        },
        // subscribe
        onEvent1: function(handler) {
            event1ServiceHandlers.push(handler);
        }
    };
}])

Above, I only show one event/message type. Each additional event/message would need its own array, publish method, and subscribe method.

.factory('Service1', ['NotificationService',
function(NotificationService) {
    // event1 handler
    var event1Happened = function(some_data) {
        console.log('S1', some_data);
        // do something here
    }
    // subscribe to event1
    NotificationService.onEvent1(event1Happened);
    return {
        someMethod: function() {
           ...
           // publish event1
           NotificationService.event1Happened(my_data);
        },
    };
}])

Service2 would be coded similarly to Service1.

Notice how $rootScope, $broadcast, and scopes are not used with this approach, because they are not needed with inter-service communication.

With the above implementation, services (once created) stay subscribed for the life of the app. You could add methods to handle unsubscribing.

In my current project, I use the same NotificationService to also handle pubsub for controller scopes. (See Updating "time ago" values in Angularjs and Momentjs if interested).

Community
  • 1
  • 1
Mark Rajcok
  • 362,217
  • 114
  • 495
  • 492
  • A plunker for this would be much appreciated if possible.I am having trouble understanding this completly – Ashkan Hovold Feb 09 '16 at 12:56
  • I've created a question about this at. http://stackoverflow.com/questions/35293132/pub-sub-design-pattern-angularjs-service please take a look if you have the time. Basicly I would like to see a working plunker example of this to better be able to understand it. Cheers – Ashkan Hovold Feb 09 '16 at 15:03
6

Yes, use events and listeners.

In your 'my-logger' you can broadcast an event when new log is captured:

$rootScope.$broadcast('new_log', log); // where log is an object containing information about the error.

and than listen for that event in your 'pusher':

$rootScope.$on('new_log', function(event, log) {... //

This way you don't need to have any dependencies.

Stewie
  • 60,366
  • 20
  • 146
  • 113
  • No, I cannot. 'my-logger' can't use $rootScope as $rootScope on its own depends on $ExceptionHandler which will lead to the circular dependency ($ExceptionHandler -> 'my-logger' -> $rootScope -> $ExceptionHandler). As stated at the original message, couldn't use any of the scope's capabilities: broadcast, emit, watch... as 'my-logger' can't have any access to it due to the circular dependency. – aviad cohen Mar 11 '13 at 15:17
1

I have partially succeeded to solve the case: I have created the dependency between 'my-logger' and 'pusher' using the $injector. I used $injector in 'my-logger' and injected at "runtime" (means right when it is about to be used and not at the declaration of the service) the pusher service upon fatal message arrival. This worked well only when I have also injected at "runtime" the $http to the 'pusher' right before the sending is to happen.

My question is why it works with injector in "runtime" and not with the dependencies declared at the head of the service ?

I have only one guess: its a matter of timing: When service is injected at "runtime", if its already exists (means was already initialized else where) then there is no need to fetch and get all its dependencies and thus the circle is never discovered and never halts the execution.

Am I correct ?

aviad cohen
  • 637
  • 1
  • 6
  • 16
0

This is an easy way to publish/subscribe to multiple events between services and controllers

.factory('$eventQueue', [function() {
  var listeners = [];
  return {
    // publish
    send: function(event_name, event_data) {
        angular.forEach(listeners, function(handler) {
          if (handler['event_name'] === event_name) {
            handler['callback'](event_data);
          }                
        });
    },
    // subscribe
    onEvent: function(event_name,handler) {
      listeners.push({'event_name': event_name, 'callback': handler});
    }
  };
}])

consumers and producers

.service('myService', [ '$eventQueue', function($eventQueue) {
  return {

    produce: function(somedata) {
     $eventQueue.send('any string you like',data);
    }

  }
}])

.controller('myController', [ '$eventQueue', function($eventQueue) {
  $eventQueue.onEvent('any string you like',function(data) {
    console.log('got data event with', data);
}])

.service('meToo', [ '$eventQueue', function($eventQueue) {
  $eventQueue.onEvent('any string you like',function(data) {
    console.log('I also got data event with', data);
}])
Damian Hamill
  • 83
  • 1
  • 6
0

You can make your own generic event publisher service, and inject it into each service.

Here's an example (I have not tested it but you get the idea):

        .provider('myPublisher', function myPublisher($windowProvider) {
            var listeners = {},
                $window = $windowProvider.$get(),
                self = this;

            function fire(eventNames) {
                var args = Array.prototype.slice.call(arguments, 1);

                if(!angular.isString(eventNames)) {
                    throw new Error('myPublisher.on(): argument one must be a string.');
                }

                eventNames = eventNames.split(/ +/);
                eventNames = eventNames.filter(function(v) {
                    return !!v;
                });

                angular.forEach(eventNames, function(eventName) {
                    var eventListeners = listeners[eventName];

                    if(eventListeners && eventListeners.length) {
                        angular.forEach(eventListeners, function(listener) {
                            $window.setTimeout(function() {
                                listener.apply(listener, args);
                            }, 1);
                        });
                    }
                });

                return self;
            }
            function on(eventNames, handler) {
                if(!angular.isString(eventNames)) {
                    throw new Error('myPublisher.on(): argument one must be a string.');
                }

                if(!angular.isFunction(handler)) {
                    throw new Error('myPublisher.on(): argument two must be a function.');
                }

                eventNames = eventNames.split(/ +/);
                eventNames = eventNames.filter(function(v) {
                    return !!v;
                });

                angular.forEach(eventNames, function(eventName) {
                    if(listeners[eventName]) {
                        listeners[eventName].push(handler);
                    }
                    else {
                        listeners[eventName] = [handler];
                    }
                });

                return self;
            }
            function off(eventNames, handler) {
                if(!angular.isString(eventNames)) {
                    throw new Error('myPublisher.off(): argument one must be a string.');
                }

                if(!angular.isFunction(handler)) {
                    throw new Error('myPublisher.off(): argument two must be a function.');
                }

                eventNames = eventNames.split(/ +/);
                eventNames = eventNames.filter(function(v) {
                    return !!v;
                });

                angular.forEach(eventNames, function(eventName) {
                    if(listeners[eventName]) {
                        var index = listeners[eventName].indexOf(handler);
                        if(index > -1) {
                            listeners[eventName].splice(index, 1);
                        }
                    }
                });

                return self;
            }

            this.fire = fire;
            this.on = on;
            this.off = off;
            this.$get = function() {
                return self;
            };
        });
Jerinaw
  • 5,260
  • 7
  • 41
  • 54