In this post I will show a potential pitfall from combining ng-upgrade with long running intervals.

Zones

Angular provides us with an execution context in the form of zones.

A key feature of “zones” involves patching asynchronous events like setInterval and XHR requests to be aware of Angular's change detection.

The main benefit is that we no longer have to manually trigger change detection when updating bound properties from native browser events.

In AngularJS we relied on wrapper services like $interval and $timeout to ensure proper change detection. With zones we no longer have to worry about this.

Zones are great for the most part. There are however a few things to be aware of when using zones in hybrid AngularJS-Angular applications.

Macro tasks

Zone categorizes events like setTimeout and XHR as macro tasks. Whenever the application fires one of these events, it triggers some bookkeeping in zone.js.

This bookkeeping involves keeping track of when the application has stabilized.

“Stable” here basically means that there are no pending events. An example of a pending event is an active setInterval. For as long as the interval is firing, the event is considered “pending”.

But what if we have a never ending setInterval?

Unfortunately never ending setIntervals will cause the application to never stabilize since the interval never leaves “pending” state.

Pending events don't seem to matter to the application in general, but you may encounter issues during E2E testing.

Protractor has built-in timing checks to wait for Angular events. This is very helpful when writing E2E tests, but the timing checks are tied in with the app stability check.

In the case of never ending intervals we have a problem since the app never stabilizes. During a test, Protractor will wait forever, but not tell us why!

Demo

Let's build a simple ng-upgrade application to illustrate the problem.

In the AngularJS part of the application we have created a clock using the $interval service. The interval runs for as long as the application is active. Behind the scenes $interval is backed by window.setTimeout, so when adding zone.js, we inherit the patched window.setTimeout version downstream.

Since the interval is never cleared, the application is never considered stable by zone.js.

Here is the AngularJS code:

angular.module('angular-legacy', []); angular.module('angular-legacy').controller('timeController', function($interval){ var vm = this; vm.title = 'Current Time:'; $interval(function(){ vm.time = new Date().toLocaleString(); }, 500); });

When we load up the application everything works as expected. However, when we try to E2E tests this, even the most trivial test fails. Protractor is stuck waiting for zone.js to declare the application as stable!

How can we fix it?

The solution is to smuggle the clock interval past the watchful eye of zone.js.

At this point zone.js is “all knowing”. It knows everything about any asynchronous event we may trigger in the application. However, it only applies the pending macro task check to the main Angular zone.

It turns out we can escape the pending check by running the clock interval in its own zone.

To make this happen let's create a new zone, and pass the interval as a delegate as seen below:

angular.module('angular-legacy', []); angular.module('angular-legacy').controller('timeController', function($interval){ var vm = this; vm.title = 'Current Time:'; // DOES NOT WORK // Zone.current.fork({}).run(function(){ // $interval(function(){ // vm.time = new Date().toLocaleString(); // }, 500); // }); new Zone().run(function(){ $interval(function(){ vm.time = new Date().toLocaleString(); }, 500); }); });

It appears we have to create a brand new zone. Forking the zone does not seem to work since it inherits the problematic pending event check from the parent.

Check out the code on Github if you want to give it a try.