From 269bbe0e7d20d40c1f89f69d017360f82e94def3 Mon Sep 17 00:00:00 2001 From: Michael Giambalvo Date: Tue, 16 May 2017 18:03:25 -0700 Subject: [PATCH] fix(upgrade): call setInterval outside the Angular zone This wraps the $interval service when using upgrade to run the $interval() call outside the Angular zone. However, the callback is invoked within the Angular zone, so changes still propagate to downgraded components. --- packages/upgrade/src/common/angular1.ts | 6 +++ packages/upgrade/src/common/constants.ts | 1 + packages/upgrade/src/static/upgrade_module.ts | 29 ++++++++++++++- .../static/integration/testability_spec.ts | 37 +++++++++++++++++++ 4 files changed, 72 insertions(+), 1 deletion(-) diff --git a/packages/upgrade/src/common/angular1.ts b/packages/upgrade/src/common/angular1.ts index 3e45a23caf..24f612253f 100644 --- a/packages/upgrade/src/common/angular1.ts +++ b/packages/upgrade/src/common/angular1.ts @@ -158,6 +158,12 @@ export interface IInjectorService { has(key: string): boolean; } +export interface IIntervalService { + (func: Function, delay: number, count?: number, invokeApply?: boolean, + ...args: any[]): Promise; + cancel(promise: Promise): boolean; +} + export interface ITestabilityService { findBindings(element: Element, expression: string, opt_exactMatch?: boolean): Element[]; findModels(element: Element, expression: string, opt_exactMatch?: boolean): Element[]; diff --git a/packages/upgrade/src/common/constants.ts b/packages/upgrade/src/common/constants.ts index 87e60f3a19..3f4cb03ff9 100644 --- a/packages/upgrade/src/common/constants.ts +++ b/packages/upgrade/src/common/constants.ts @@ -11,6 +11,7 @@ export const $CONTROLLER = '$controller'; export const $DELEGATE = '$delegate'; export const $HTTP_BACKEND = '$httpBackend'; export const $INJECTOR = '$injector'; +export const $INTERVAL = '$interval'; export const $PARSE = '$parse'; export const $PROVIDE = '$provide'; export const $ROOT_SCOPE = '$rootScope'; diff --git a/packages/upgrade/src/static/upgrade_module.ts b/packages/upgrade/src/static/upgrade_module.ts index 634f7669c3..70c9313e0c 100644 --- a/packages/upgrade/src/static/upgrade_module.ts +++ b/packages/upgrade/src/static/upgrade_module.ts @@ -9,7 +9,7 @@ import {Injector, NgModule, NgZone, Testability, ɵNOT_FOUND_CHECK_ONLY_ELEMENT_INJECTOR as NOT_FOUND_CHECK_ONLY_ELEMENT_INJECTOR} from '@angular/core'; import * as angular from '../common/angular1'; -import {$$TESTABILITY, $DELEGATE, $INJECTOR, $PROVIDE, INJECTOR_KEY, UPGRADE_MODULE_NAME} from '../common/constants'; +import {$$TESTABILITY, $DELEGATE, $INJECTOR, $INTERVAL, $PROVIDE, INJECTOR_KEY, UPGRADE_MODULE_NAME} from '../common/constants'; import {controllerKey} from '../common/util'; import {angular1Providers, setTempInjectorRef} from './angular1_providers'; @@ -190,6 +190,33 @@ export class UpgradeModule { } ]); } + + if ($injector.has($INTERVAL)) { + $provide.decorator($INTERVAL, [ + $DELEGATE, + (intervalDelegate: angular.IIntervalService) => { + // Wrap the $interval service so that setInterval is called outside NgZone, + // but the callback is still invoked within it. This is so that $interval + // won't block stability, which preserves the behavior from AngularJS. + let wrappedInterval = + (fn: Function, delay: number, count?: number, invokeApply?: boolean, + ...pass: any[]) => { + return this.ngZone.runOutsideAngular(() => { + return intervalDelegate((...args: any[]) => { + // Run callback in the next VM turn - $interval calls + // $rootScope.$apply, and running the callback in NgZone will + // cause a '$digest already in progress' error if it's in the + // same vm turn. + setTimeout(() => { this.ngZone.run(() => fn(...args)); }); + }, delay, count, invokeApply, ...pass); + }); + }; + + (wrappedInterval as any)['cancel'] = intervalDelegate.cancel; + return wrappedInterval; + } + ]); + } } ]) diff --git a/packages/upgrade/test/static/integration/testability_spec.ts b/packages/upgrade/test/static/integration/testability_spec.ts index ada37784e4..7fc46c8f2d 100644 --- a/packages/upgrade/test/static/integration/testability_spec.ts +++ b/packages/upgrade/test/static/integration/testability_spec.ts @@ -73,5 +73,42 @@ export function main() { expect(ng2Stable).toEqual(true); }); })); + + it('should not wait for $interval', fakeAsync(() => { + const ng1Module = angular.module('ng1', []); + const element = html('
'); + + bootstrap(platformBrowserDynamic(), Ng2Module, element, ng1Module).then((upgrade) => { + + const ng2Testability: Testability = upgrade.injector.get(Testability); + const $interval: angular.IIntervalService = upgrade.$injector.get('$interval'); + + let ng2Stable = false; + let intervalDone = false; + + const id = $interval((arg: string) => { + // should only be called once + expect(intervalDone).toEqual(false); + + intervalDone = true; + expect(NgZone.isInAngularZone()).toEqual(true); + expect(arg).toEqual('passed argument'); + }, 200, 0, true, 'passed argument'); + + ng2Testability.whenStable(() => { ng2Stable = true; }); + + tick(100); + + expect(intervalDone).toEqual(false); + expect(ng2Stable).toEqual(true); + + tick(200); + expect(intervalDone).toEqual(true); + expect($interval.cancel(id)).toEqual(true); + + // Interval should not fire after cancel + tick(200); + }); + })); }); }