Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

fix(upgrade): call setInterval outside the Angular zone #16836

Merged
merged 1 commit into from
Jun 6, 2017
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
6 changes: 6 additions & 0 deletions packages/upgrade/src/common/angular1.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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<any>;
cancel(promise: Promise<any>): boolean;
}

export interface ITestabilityService {
findBindings(element: Element, expression: string, opt_exactMatch?: boolean): Element[];
findModels(element: Element, expression: string, opt_exactMatch?: boolean): Element[];
Expand Down
1 change: 1 addition & 0 deletions packages/upgrade/src/common/constants.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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';
Expand Down
29 changes: 28 additions & 1 deletion packages/upgrade/src/static/upgrade_module.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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';
Expand Down Expand Up @@ -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;
}
]);
}
}
])

Expand Down
37 changes: 37 additions & 0 deletions packages/upgrade/test/static/integration/testability_spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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('<div></div>');

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);
});
}));
});
}