feat: move NgZone to Stream/Observable-based callback API

BREAKING CHANGES:
- deprecates these methods in NgZone: overrideOnTurnStart, overrideOnTurnDone, overrideOnEventDone, overrideOnErrorHandler
- introduces new API in NgZone that may shadow other API used by existing applications.
This commit is contained in:
Yegor Jbanov
2015-10-19 14:41:15 -07:00
parent a7c95ade2e
commit 491e1fdd2c
9 changed files with 1111 additions and 119 deletions

View File

@ -35,6 +35,17 @@ class WrappedTimer implements Timer {
bool get isActive => _timer.isActive;
}
/**
* Stores error information; delivered via [NgZone.onError] stream.
*/
class NgZoneError {
/// Error object thrown.
final error;
/// Either long or short chain of stack traces.
final List stackTrace;
NgZoneError(this.error, this.stackTrace);
}
/**
* A `Zone` wrapper that lets you schedule tasks after its private microtask queue is exhausted but
* before the next "VM turn", i.e. event loop iteration.
@ -56,6 +67,12 @@ class NgZone {
ZeroArgFunction _onEventDone;
ErrorHandlingFn _onErrorHandler;
final _onTurnStartCtrl = new StreamController.broadcast(sync: true);
final _onTurnDoneCtrl = new StreamController.broadcast(sync: true);
final _onEventDoneCtrl = new StreamController.broadcast(sync: true);
final _onErrorCtrl =
new StreamController<NgZoneError>.broadcast(sync: true);
// Code executed in _mountZone does not trigger the onTurnDone.
Zone _mountZone;
// _innerZone is the child of _mountZone. Any code executed in this zone will trigger the
@ -103,18 +120,43 @@ class NgZone {
* Sets the zone hook that is called just before Angular event turn starts.
* It is called once per browser event.
*/
@Deprecated('Use onTurnStart Stream instead')
void overrideOnTurnStart(ZeroArgFunction onTurnStartFn) {
_onTurnStart = onTurnStartFn;
}
void _notifyOnTurnStart() {
this._onTurnStartCtrl.add(null);
}
/**
* Notifies subscribers just before Angular event turn starts.
*
* Emits an event once per browser task that is handled by Angular.
*/
Stream get onTurnStart => _onTurnStartCtrl.stream;
/**
* Sets the zone hook that is called immediately after Angular processes
* all pending microtasks.
*/
@Deprecated('Use onTurnDone Stream instead')
void overrideOnTurnDone(ZeroArgFunction onTurnDoneFn) {
_onTurnDone = onTurnDoneFn;
}
/**
* Notifies subscribers immediately after the Angular zone is done processing
* the current turn and any microtasks scheduled from that turn.
*
* Used by Angular as a signal to kick off change-detection.
*/
Stream get onTurnDone => _onTurnDoneCtrl.stream;
void _notifyOnTurnDone() {
this._onTurnDoneCtrl.add(null);
}
/**
* Sets the zone hook that is called immediately after the last turn in
* an event completes. At this point Angular will no longer attempt to
@ -123,6 +165,7 @@ class NgZone {
*
* This hook is useful for validating application state (e.g. in a test).
*/
@Deprecated('Use onEventDone Stream instead')
void overrideOnEventDone(ZeroArgFunction onEventDoneFn,
[bool waitForAsync = false]) {
_onEventDone = onEventDoneFn;
@ -136,15 +179,55 @@ class NgZone {
}
}
/**
* Notifies subscribers immediately after the final `onTurnDone` callback
* before ending VM event.
*
* This event is useful for validating application state (e.g. in a test).
*/
Stream get onEventDone => _onEventDoneCtrl.stream;
void _notifyOnEventDone() {
this._onEventDoneCtrl.add(null);
}
/**
* Whether there are any outstanding microtasks.
*/
bool get hasPendingMicrotasks => _pendingMicrotasks > 0;
/**
* Whether there are any outstanding timers.
*/
bool get hasPendingTimers => _pendingTimers.isNotEmpty;
/**
* Whether there are any outstanding asychnronous tasks of any kind that are
* scheduled to run within Angular zone.
*
* Useful as a signal of UI stability. For example, when a test reaches a
* point when [hasPendingAsyncTasks] is `false` it might be a good time to run
* test expectations.
*/
bool get hasPendingAsyncTasks => hasPendingMicrotasks || hasPendingTimers;
/**
* Sets the zone hook that is called when an error is uncaught in the
* Angular zone. The first argument is the error. The second argument is
* the stack trace.
*/
@Deprecated('Use onError Stream instead')
void overrideOnErrorHandler(ErrorHandlingFn errorHandlingFn) {
_onErrorHandler = errorHandlingFn;
}
/**
* Notifies subscribers whenever an error happens within the zone.
*
* Useful for logging.
*/
Stream get onError => _onErrorCtrl.stream;
/**
* Runs `fn` in the inner zone and returns whatever it returns.
*
@ -194,6 +277,7 @@ class NgZone {
void _maybeStartVmTurn(ZoneDelegate parent) {
if (!_hasExecutedCodeInInnerZone) {
_hasExecutedCodeInInnerZone = true;
parent.run(_innerZone, _notifyOnTurnStart);
if (_onTurnStart != null) {
parent.run(_innerZone, _onTurnStart);
}
@ -209,19 +293,25 @@ class NgZone {
_nestedRun--;
// If there are no more pending microtasks and we are not in a recursive call, this is the end of a turn
if (_pendingMicrotasks == 0 && _nestedRun == 0 && !_inVmTurnDone) {
if (_onTurnDone != null && _hasExecutedCodeInInnerZone) {
if (_hasExecutedCodeInInnerZone) {
// Trigger onTurnDone at the end of a turn if _innerZone has executed some code
try {
_inVmTurnDone = true;
parent.run(_innerZone, _onTurnDone);
_notifyOnTurnDone();
if (_onTurnDone != null) {
parent.run(_innerZone, _onTurnDone);
}
} finally {
_inVmTurnDone = false;
_hasExecutedCodeInInnerZone = false;
}
}
if (_pendingMicrotasks == 0 && _onEventDone != null) {
runOutsideAngular(_onEventDone);
if (_pendingMicrotasks == 0) {
_notifyOnEventDone();
if (_onEventDone != null) {
runOutsideAngular(_onEventDone);
}
}
}
}
@ -248,9 +338,14 @@ class NgZone {
// Called by Chain.capture() on errors when long stack traces are enabled
void _onErrorWithLongStackTrace(error, Chain chain) {
if (_onErrorHandler != null) {
if (_onErrorHandler != null || _onErrorCtrl.hasListener) {
final traces = chain.terse.traces.map((t) => t.toString()).toList();
_onErrorHandler(error, traces);
if (_onErrorCtrl.hasListener) {
_onErrorCtrl.add(new NgZoneError(error, traces));
}
if (_onErrorHandler != null) {
_onErrorHandler(error, traces);
}
} else {
throw error;
}
@ -258,8 +353,13 @@ class NgZone {
// Outer zone handleUnchaughtError when long stack traces are not used
void _onErrorWithoutLongStackTrace(error, StackTrace trace) {
if (_onErrorHandler != null) {
_onErrorHandler(error, [trace.toString()]);
if (_onErrorHandler != null || _onErrorCtrl.hasListener) {
if (_onErrorHandler != null) {
_onErrorHandler(error, [trace.toString()]);
}
if (_onErrorCtrl.hasListener) {
_onErrorCtrl.add(new NgZoneError(error, [trace.toString()]));
}
} else {
throw error;
}

View File

@ -1,19 +1,22 @@
import {ListWrapper, StringMapWrapper} from 'angular2/src/core/facade/collection';
import {normalizeBlank, isPresent, global} from 'angular2/src/core/facade/lang';
import {ObservableWrapper, EventEmitter} from 'angular2/src/core/facade/async';
import {wtfLeave, wtfCreateScope, WtfScopeFn} from '../profile/profile';
export interface NgZoneZone extends Zone {
/** @internal */
_innerZone: boolean;
}
export interface ZeroArgFunction {
(): void;
}
export interface ZeroArgFunction { (): void; }
export interface ErrorHandlingFn {
(error: any, stackTrace: any): void;
export interface ErrorHandlingFn { (error: any, stackTrace: any): void; }
/**
* Stores error information; delivered via [NgZone.onError] stream.
*/
export class NgZoneError {
constructor(public error: any, public stackTrace: any) {}
}
/**
@ -109,6 +112,15 @@ export class NgZone {
/** @internal */
_onErrorHandler: ErrorHandlingFn;
/** @internal */
_onTurnStartEvents: EventEmitter;
/** @internal */
_onTurnDoneEvents: EventEmitter;
/** @internal */
_onEventDoneEvents: EventEmitter;
/** @internal */
_onErrorEvents: EventEmitter;
// Number of microtasks pending from _innerZone (& descendants)
/** @internal */
_pendingMicrotasks: number = 0;
@ -146,6 +158,10 @@ export class NgZone {
this._disabled = true;
this._mountZone = null;
}
this._onTurnStartEvents = new EventEmitter(false);
this._onTurnDoneEvents = new EventEmitter(false);
this._onEventDoneEvents = new EventEmitter(false);
this._onErrorEvents = new EventEmitter(false);
}
/**
@ -155,11 +171,24 @@ export class NgZone {
* The hook is called once per browser task that is handled by Angular.
*
* Setting the hook overrides any previously set hook.
*
* @deprecated this API will be removed in the future. Use `onTurnStart` instead.
*/
overrideOnTurnStart(onTurnStartHook: ZeroArgFunction): void {
this._onTurnStart = normalizeBlank(onTurnStartHook);
}
/**
* Notifies subscribers just before Angular event turn starts.
*
* Emits an event once per browser task that is handled by Angular.
*/
get onTurnStart(): /* Subject */ any { return this._onTurnStartEvents; }
_notifyOnTurnStart(parentRun): void {
parentRun.call(this._innerZone, () => { this._onTurnStartEvents.next(null); });
}
/**
* Sets the zone hook that is called immediately after Angular zone is done processing the current
* task and any microtasks scheduled from that task.
@ -169,11 +198,25 @@ export class NgZone {
* The hook is called once per browser task that is handled by Angular.
*
* Setting the hook overrides any previously set hook.
*
* @deprecated this API will be removed in the future. Use `onTurnDone` instead.
*/
overrideOnTurnDone(onTurnDoneHook: ZeroArgFunction): void {
this._onTurnDone = normalizeBlank(onTurnDoneHook);
}
/**
* Notifies subscribers immediately after Angular zone is done processing
* the current turn and any microtasks scheduled from that turn.
*
* Used by Angular as a signal to kick off change-detection.
*/
get onTurnDone() { return this._onTurnDoneEvents; }
_notifyOnTurnDone(parentRun): void {
parentRun.call(this._innerZone, () => { this._onTurnDoneEvents.next(null); });
}
/**
* Sets the zone hook that is called immediately after the `onTurnDone` callback is called and any
* microstasks scheduled from within that callback are drained.
@ -184,6 +227,8 @@ export class NgZone {
* This hook is useful for validating application state (e.g. in a test).
*
* Setting the hook overrides any previously set hook.
*
* @deprecated this API will be removed in the future. Use `onEventDone` instead.
*/
overrideOnEventDone(onEventDoneFn: ZeroArgFunction, opt_waitForAsync: boolean = false): void {
var normalizedOnEventDone = normalizeBlank(onEventDoneFn);
@ -198,15 +243,51 @@ export class NgZone {
}
}
/**
* Notifies subscribers immediately after the final `onTurnDone` callback
* before ending VM event.
*
* This event is useful for validating application state (e.g. in a test).
*/
get onEventDone() { return this._onEventDoneEvents; }
_notifyOnEventDone(): void {
this.runOutsideAngular(() => { this._onEventDoneEvents.next(null); });
}
/**
* Whether there are any outstanding microtasks.
*/
get hasPendingMicrotasks(): boolean { return this._pendingMicrotasks > 0; }
/**
* Whether there are any outstanding timers.
*/
get hasPendingTimers(): boolean { return this._pendingTimeouts.length > 0; }
/**
* Whether there are any outstanding asychnronous tasks of any kind that are
* scheduled to run within Angular zone.
*
* Useful as a signal of UI stability. For example, when a test reaches a
* point when [hasPendingAsyncTasks] is `false` it might be a good time to run
* test expectations.
*/
get hasPendingAsyncTasks(): boolean { return this.hasPendingMicrotasks || this.hasPendingTimers; }
/**
* Sets the zone hook that is called when an error is thrown in the Angular zone.
*
* Setting the hook overrides any previously set hook.
*
* @deprecated this API will be removed in the future. Use `onError` instead.
*/
overrideOnErrorHandler(errorHandler: ErrorHandlingFn) {
this._onErrorHandler = normalizeBlank(errorHandler);
}
get onError() { return this._onErrorEvents; }
/**
* Executes the `fn` function synchronously within the Angular zone and returns value returned by
* the function.
@ -257,10 +338,10 @@ export class NgZone {
var errorHandling;
if (enableLongStackTrace) {
errorHandling = StringMapWrapper.merge(Zone.longStackTraceZone,
{onError: function(e) { ngZone._onError(this, e); }});
errorHandling = StringMapWrapper.merge(
Zone.longStackTraceZone, {onError: function(e) { ngZone._notifyOnError(this, e); }});
} else {
errorHandling = {onError: function(e) { ngZone._onError(this, e); }};
errorHandling = {onError: function(e) { ngZone._notifyOnError(this, e); }};
}
return zone.fork(errorHandling)
@ -271,6 +352,7 @@ export class NgZone {
ngZone._nestedRun++;
if (!ngZone._hasExecutedCodeInInnerZone) {
ngZone._hasExecutedCodeInInnerZone = true;
ngZone._notifyOnTurnStart(parentRun);
if (ngZone._onTurnStart) {
parentRun.call(ngZone._innerZone, ngZone._onTurnStart);
}
@ -285,18 +367,24 @@ export class NgZone {
// to run()).
if (ngZone._pendingMicrotasks == 0 && ngZone._nestedRun == 0 &&
!this._inVmTurnDone) {
if (ngZone._onTurnDone && ngZone._hasExecutedCodeInInnerZone) {
if (ngZone._hasExecutedCodeInInnerZone) {
try {
this._inVmTurnDone = true;
parentRun.call(ngZone._innerZone, ngZone._onTurnDone);
ngZone._notifyOnTurnDone(parentRun);
if (ngZone._onTurnDone) {
parentRun.call(ngZone._innerZone, ngZone._onTurnDone);
}
} finally {
this._inVmTurnDone = false;
ngZone._hasExecutedCodeInInnerZone = false;
}
}
if (ngZone._pendingMicrotasks === 0 && isPresent(ngZone._onEventDone)) {
ngZone.runOutsideAngular(ngZone._onEventDone);
if (ngZone._pendingMicrotasks === 0) {
ngZone._notifyOnEventDone();
if (isPresent(ngZone._onEventDone)) {
ngZone.runOutsideAngular(ngZone._onEventDone);
}
}
}
}
@ -340,17 +428,22 @@ export class NgZone {
}
/** @internal */
_onError(zone, e): void {
if (isPresent(this._onErrorHandler)) {
_notifyOnError(zone, e): void {
if (isPresent(this._onErrorHandler) || ObservableWrapper.hasSubscribers(this._onErrorEvents)) {
var trace = [normalizeBlank(e.stack)];
while (zone && zone.constructedAtException) {
trace.push(zone.constructedAtException.get());
zone = zone.parent;
}
this._onErrorHandler(e, trace);
if (ObservableWrapper.hasSubscribers(this._onErrorEvents)) {
ObservableWrapper.callNext(this._onErrorEvents, new NgZoneError(e, trace));
}
if (isPresent(this._onErrorHandler)) {
this._onErrorHandler(e, trace);
}
} else {
console.log('## _onError ##');
console.log('## _notifyOnError ##');
console.log(e.stack);
throw e;
}