2

I'm developing a Chrome Extension using AngularJS (so it is running in CSP mode). amServices.js contains a service that deals with Chrome native messaging. So at js/core/am/amServices.js:268:20 the relevant code is as follows:

chrome.runtime.onMessage.addListener(
  function (message, sender, sendResponse) {
    $rootScope.$apply(function () {
      if (message.type == 'login' && message.state == 'ok') {
//huge if/else if here for every kind of message

My understanding is that as all the code inside here is getting called asynchronously and can trigger modifications in most of the application views, $rootScope.$apply is mandatory. However, in what seems a totally random way, I sometimes get these in the console:

Error: [$rootScope:inprog] http://errors.angularjs.org/1.2.13/$rootScope/inprog?p0=%24apply
    at Error (native)
    at chrome-extension://hbfchfkepmidgcdfcpnodlnmfjhekcom/lib/angular/angular.min.js:6:450
    at n (chrome-extension://hbfchfkepmidgcdfcpnodlnmfjhekcom/lib/angular/angular.min.js:98:34)
    at h.$apply (chrome-extension://hbfchfkepmidgcdfcpnodlnmfjhekcom/lib/angular/angular.min.js:104:195)
    at chrome-extension://hbfchfkepmidgcdfcpnodlnmfjhekcom/js/core/am/amServices.js:268:20
    at Function.target.(anonymous function) (extensions::SafeBuiltins:19:14)
    at Event.dispatchToListener (extensions::event_bindings:394:22)
    at Event.dispatch_ (extensions::event_bindings:378:27)
    at Event.dispatch (extensions::event_bindings:400:17)
    at messageListener (extensions::messaging:192:31) 

No what bugs me about it is the fact that, unlike what is explained here:

Why doesn't Angular ignore subsequent $digest invocations?

In my stack trace I'm not seeing two $apply calls, so I have no way to know where the conflic comes from. Additionally, I can't run AngularJS Batarang debug tool, as it doesn't work with CSP mode.

I'm ignoring these errors without any apparent consequences, but I'm unsure wheter it really is safe to ignore them. Any ideas on how to know which two apply calls triggered the conflict?

bluehallu
  • 10,205
  • 9
  • 44
  • 61
  • 1
    One thing I would do is to use the non-minified version of angular and see what its internal state is. It can get complex in there but I've gotten a few debugging ideas that way. – David Pope Mar 29 '14 at 13:49
  • The error is stating that there is already a $digest/$apply in progress. This kind of thing happens when listeners are FIRING digests vs subscribing to the digest. Angular only does work in a $digest, so if something is happening in your angular app, a $digest/$apply is already inprog. You can likely accomplish what you are trying to do with $watch and/or $broadcast/$emit. Tough to advise further without knowing more. – J.Wells Mar 29 '14 at 13:58
  • also - $apply calls $digest (in case i didn't clarify properly). – J.Wells Mar 29 '14 at 13:59
  • So - from $rootScope you can do `$rootScope.$broadcast('someEventName', {data:here});` then in any other non-isolated scope derived from that rootScope, you can subscribe to that event `$scope.on('someEventName', function(data){ /*do something with data*/});` --- which should roll those events into angular's $digest loop, handling those events every time they fire. – J.Wells Mar 29 '14 at 14:06
  • 2
    Do you guys even read the questions you comment on? I know what the error means and I know why it's supposed to happen. The question is about why the stack trace isn't showing two different apply calls. – bluehallu Mar 29 '14 at 17:54
  • 1
    Oh man - I'm sorry we answered a question that you went back and edited. So - to answer the question in your comment, here's $apply `$apply: function(expr) { try { beginPhase('$apply'); return this.$eval(expr); } catch (e) { $exceptionHandler(e); } finally { clearPhase(); try { $rootScope.$digest(); } catch (e) { $exceptionHandler(e); throw e; } } }` that's why, genius. – J.Wells Mar 29 '14 at 18:37
  • @J.Wells I edited it make the obvious more obvious. Look at the edit history and re-read the original question. – bluehallu Mar 29 '14 at 18:45
  • @Hallucynogenyc Why would you expect the initial `$apply` call to appear in your _asynchronous_ stacktrace? You're calling `$apply` in response to an event, which understandably could try to call `$apply` when a digest is already in progress (this would be random). However, your event isn't tied to the rest of your stack, so I don't see how you'd expect to see that anyway. –  Mar 31 '14 at 15:51
  • @lunchmeat317 All chrome extension components run in a single process and single thread. Therefore the handler for the listener in my code cannot fire while a random digest is happening, it must wait until any current execution finishes. My understanding is that somehow my own code is calling a second $apply somehow. Correct me if I'm mistaken :p – bluehallu Apr 01 '14 at 09:33
  • This is not a legitimate question. AngularJS doesn't ignore subsequent $digest calls because they don't and it was their decision. What I can tell you is that they know that people aren't happy about it and I read that in the next version of Angular 2.0 they will be doing away with ever needing to call $apply again. – btm1 Apr 01 '14 at 17:32
  • @btm1 I never said they should ignore them. In fact, to me, it makes a lot of sense that you have to call $apply. What I'm asking is how am I supposed to debug this kind of situation. – bluehallu Apr 02 '14 at 10:13
  • Could it be that some part of your code inside $apply is calling a angular function that calls apply or digest? You say in your comment that is a huge if/else statement, could we see all of it? – Wawy Apr 02 '14 at 15:30
  • @Wawy all it does is call a different function for each case, I've already tried to trace every single of them to check for an $apply without much luck. Again the question is not where my particular conflict comes from but how am I supposed to debug this kind of problem. – bluehallu Apr 03 '14 at 07:30
  • Usually what you need to check for is anything that could mean you are in the digest loop when you call $apply. Or also anything that could try to trigger a digest (directly or indirectly) when you are inside your apply call. It doesn't need to be $apply, it could be $timeout, $evalAsync or even $digest. It also doesn't need to be anything specific to your code instead it might be related to an angular method/function that internally does it when called. – Wawy Apr 03 '14 at 10:47
  • @Wawy Yeah I know that, but as it's the case here, it is impossible in practice for me to check every line of code under that first $apply. In the answer I referenced in the question the guy says I should be seeing 2 $apply calls in the stack and use that to debug the situation, but I'm only seeing one. – bluehallu Apr 03 '14 at 11:09
  • Yes, but that is not entirely true, since you could be inside a watch function and be calling $apply inside it which will fail but you would not see 2 $apply in the trace, that's why I said you can't rely solely on seeing $apply in your trace. Example: $scope.watch('var', function() { $scope.apply(console.log('I'm doing something silly'); }); – Wawy Apr 03 '14 at 11:24
  • @Wawy Submit an answer explaining in which situations I won't see 2 $apply in the stack trace and the bounty is yours ;) – bluehallu Apr 03 '14 at 12:54

2 Answers2

3

If your question is why? and you are expecting the error would only happen if you had more than one $apply()

Well, as @J.Wells mentions in the comment on your question, is probably because other angular directives triggered a $scope.$apply and the callback from the chrome.runtime happens while the angular internals are in a $$phase.

Take a look at the source code for ng-click, angularjs internals use the same $scope.$apply available to developers.

So just consider that angular directives such as ng-click, ng-change evaluate expression using a $scope.$apply, which starts an $apply phase, and directives such as ng-if and ng-hide evaluate expressions using a $scope.$watch, which is inside a $digest phase.

Bad theory, because as @Wawy suggests, javascript is single threaded so the callback cannot be executed while in a digest (left so that the comments make sense): If it happens when you click a Login button, with an ng-click directive, it could be because chrome binds and responds too fast before the $digest phase of the ng-click is finished, so the onMessage callback is executed.

guzart
  • 3,700
  • 28
  • 23
  • A $digest is global to the app, not specific to one directive. It's also a synchronous operation (and as far as I'm concerned javascript is single threaded), hence I don't think it's possible that the callback can happen while the code inside $digest is being executed. – Wawy Apr 04 '14 at 08:13
  • The $rootScope.$digest is global to the app, but every $scope has their own $digest. – guzart Apr 07 '14 at 21:22
2

To answer this question we need to have a look at the implementation of $digest and $apply:

$digest: function() {
    //Simplified code

    beginPhase('$digest'); 
    /* $$phase = 'digest' 
       From now on any code that calls $apply will cause an exception.
    */

    lastDirtyWatch = null;

    do { // "while dirty" loop
      dirty = false;
      current = target;

      //Running any $evalAsync your code might have called
      while(asyncQueue.length) {
        try {
          asyncTask = asyncQueue.shift();
           // If any $evalAsync expression calls $apply will cause an exception.
          asyncTask.scope.$eval(asyncTask.expression);
        } catch (e) {
          clearPhase();
          $exceptionHandler(e);
        }
        lastDirtyWatch = null;
      }

      traverseScopesLoop:
      do { // "traverse the scopes" loop
        if ((watchers = current.$$watchers)) {
          // process our watches
          // If any $watch expression calls $apply it will cause an exception.
          ...
        }
      } while ((current = next));

    } while (dirty || asyncQueue.length);
}

$apply:

$apply: function(expr) {
        try {
          beginPhase('$apply');
          //Any code inside expr that calls $apply or any function that does it will cause an exception.
          return this.$eval(expr);
        } catch (e) {
          $exceptionHandler(e);
        } finally {
          clearPhase();
          try {
            $rootScope.$digest();
          } catch (e) {
            $exceptionHandler(e);
            throw e;
          }
        }
      },

After analysing the two methods we find 3 places where you could cause an exception:

$evalAsync expression, $watch expression and $apply expression. So any code inside those expression that directly or indirectly calls $apply will trigger an exception.

Worth mentioning that angular internally also calls $apply so you shouldn't be calling $apply if you are inside angular 'realms'.

Wawy
  • 6,259
  • 2
  • 23
  • 23