diff --git a/karma-js.conf.js b/karma-js.conf.js index 01426c1834..d91b4f223c 100644 --- a/karma-js.conf.js +++ b/karma-js.conf.js @@ -12,6 +12,12 @@ module.exports = function(config) { // Loaded through the es6-module-loader, in `test-main.js`. {pattern: 'dist/js/dev/es5/**', included: false, watched: false}, + // Promise monkey patch & zone should be included first + 'zone/es6-promise.js', + 'zone/zone.js', + 'zone/inner-zone.js', + 'zone/long-stack-trace-zone.js', + 'node_modules/traceur/bin/traceur-runtime.js', 'node_modules/es6-module-loader/dist/es6-module-loader-sans-promises.src.js', // Including systemjs because it defines `__eval`, which produces correct stack traces. @@ -20,9 +26,6 @@ module.exports = function(config) { 'node_modules/systemjs/lib/extension-cjs.js', 'node_modules/rx/dist/rx.js', 'node_modules/reflect-metadata/Reflect.js', - 'node_modules/zone.js/zone.js', - 'node_modules/zone.js/long-stack-trace-zone.js', - 'tools/build/file2modulename.js', 'test-main.js' ], diff --git a/modules/angular2/src/core/zone/vm_turn_zone.dart b/modules/angular2/src/core/zone/vm_turn_zone.dart index a1c787fb4e..76fb769bdf 100644 --- a/modules/angular2/src/core/zone/vm_turn_zone.dart +++ b/modules/angular2/src/core/zone/vm_turn_zone.dart @@ -1,6 +1,6 @@ library angular.zone; -import 'dart:async' as async; +import 'dart:async'; import 'package:stack_trace/stack_trace.dart' show Chain; /** @@ -15,49 +15,97 @@ import 'package:stack_trace/stack_trace.dart' show Chain; * The wrapper maintains an "inner" and "outer" `Zone`. The application code will executes * in the "inner" zone unless `runOutsideAngular` is explicitely called. * - * A typical application will create a singleton `VmTurnZone` whose outer `Zone` is the root `Zone` - * and whose default `onTurnDone` runs the Angular digest. + * A typical application will create a singleton `VmTurnZone`. The outer `Zone` is a fork of the root + * `Zone`. The default `onTurnDone` runs the Angular change detection. */ class VmTurnZone { Function _onTurnStart; Function _onTurnDone; - Function _onScheduleMicrotask; Function _onErrorHandler; - async.Zone _outerZone; - async.Zone _innerZone; + // Code executed in _outerZone does not trigger the onTurnDone. + Zone _outerZone; + // _innerZone is the child of _outerZone. Any code executed in this zone will trigger the + // onTurnDone hook at the end of the current VM turn. + Zone _innerZone; - int _nestedRunCounter; + // Number of microtasks pending from _outerZone (& descendants) + int _pendingMicrotasks = 0; + // Whether some code has been executed in the _innerZone (& descendants) in the current turn + bool _hasExecutedCodeInInnerZone = false; + // Whether the onTurnStart hook is executing + bool _inTurnStart = false; + // _outerRun() call depth. 0 at the end of a macrotask + // zone.run(() => { // top-level call + // zone.run(() => {}); // nested call -> in-turn + // }); // we should only check for the end of a turn once the top-level run ends + int _nestedRun = 0; /** * Associates with this * - * - an "outer" `Zone`, which is the one that created this. - * - an "inner" `Zone`, which is a child of the outer `Zone`. + * - an "outer" [Zone], which is a child of the one that created this. + * - an "inner" [Zone], which is a child of the outer [Zone]. * * @param {bool} enableLongStackTrace whether to enable long stack trace. They should only be * enabled in development mode as they significantly impact perf. */ VmTurnZone({bool enableLongStackTrace}) { - _nestedRunCounter = 0; - _outerZone = async.Zone.current; - _innerZone = _createInnerZoneWithErrorHandling(enableLongStackTrace); + // The _outerZone captures microtask scheduling so that we can run onTurnDone when the queue + // is exhausted and code has been executed in the _innerZone. + if (enableLongStackTrace) { + _outerZone = Chain.capture( + () { + return Zone.current.fork( + specification: new ZoneSpecification( + scheduleMicrotask: _scheduleMicrotask, + run: _outerRun, + runUnary: _outerRunUnary, + runBinary: _outerRunBinary + ), + zoneValues: {'_name': 'outer'} + ); + }, onError: _onErrorWithLongStackTrace); + } else { + _outerZone = Zone.current.fork( + specification: new ZoneSpecification( + scheduleMicrotask: _scheduleMicrotask, + run: _outerRun, + runUnary: _outerRunUnary, + runBinary: _outerRunBinary, + handleUncaughtError: (Zone self, ZoneDelegate parent, Zone zone, error, StackTrace trace) => + _onErrorWithoutLongStackTrace(error, trace) + ), + zoneValues: {'_name': 'outer'} + ); + } + + // Instruments the inner [Zone] to detect when code is executed in this (or a descendant) zone. + // Also runs the onTurnStart hook the first time this zone executes some code in each turn. + _innerZone = _outerZone.fork( + specification: new ZoneSpecification( + run: _innerRun, + runUnary: _innerRunUnary, + runBinary: _innerRunBinary + ), + zoneValues: {'_name': 'inner'}); } /** * Initializes the zone hooks. * + * The given error handler should re-throw the passed exception. Otherwise, exceptions will not + * propagate outside of the [VmTurnZone] and can alter the application execution flow. + * Not re-throwing could be used to help testing the code or advanced use cases. + * * @param {Function} onTurnStart called before code executes in the inner zone for each VM turn * @param {Function} onTurnDone called at the end of a VM turn if code has executed in the inner zone - * @param {Function} onScheduleMicrotask * @param {Function} onErrorHandler called when an exception is thrown by a macro or micro task */ - initCallbacks({Function onTurnStart, Function onTurnDone, - Function onScheduleMicrotask, Function onErrorHandler}) { - this._onTurnStart = onTurnStart; - this._onTurnDone = onTurnDone; - this._onScheduleMicrotask = onScheduleMicrotask; - this._onErrorHandler = onErrorHandler; + void initCallbacks({Function onTurnStart, Function onTurnDone, Function onErrorHandler}) { + _onTurnStart = onTurnStart; + _onTurnDone = onTurnDone; + _onErrorHandler = onErrorHandler; } /** @@ -76,7 +124,12 @@ class VmTurnZone { * } * ``` */ - dynamic run(fn()) => _innerZone.run(fn); + dynamic run(fn()) { + // Using runGuarded() is required when executing sync code with Dart otherwise handleUncaughtError() + // would not be called on exceptions. + // see https://code.google.com/p/dart/issues/detail?id=19566 for details. + return _innerZone.runGuarded(fn); + } /** * Runs `fn` in the outer zone and returns whatever it returns. @@ -97,81 +150,94 @@ class VmTurnZone { * } * ``` */ - dynamic runOutsideAngular(fn()) => _outerZone.run(fn); - - async.Zone _createInnerZoneWithErrorHandling(bool enableLongStackTrace) { - if (enableLongStackTrace) { - return Chain.capture(() { - return _createInnerZone(async.Zone.current); - }, onError: this._onErrorWithLongStackTrace); - } else { - return async.runZoned(() { - return _createInnerZone(async.Zone.current); - }, onError: this._onErrorWithoutLongStackTrace); - } + dynamic runOutsideAngular(fn()) { + return _outerZone.runGuarded(fn); } - async.Zone _createInnerZone(async.Zone zone) { - return zone.fork( - specification: new async.ZoneSpecification( - run: _onRun, - runUnary: _onRunUnary, - scheduleMicrotask: _onMicrotask)); + // Executes code in the [_innerZone] & trigger the onTurnStart hook when code is executed for the + // first time in a turn. + dynamic _innerRun(Zone self, ZoneDelegate parent, Zone zone, fn()) { + _maybeStartVmTurn(parent, zone); + return parent.run(zone, fn); } - dynamic _onRunBase( - async.Zone self, async.ZoneDelegate delegate, async.Zone zone, fn()) { - _nestedRunCounter++; - try { - if (_nestedRunCounter == 1 && _onTurnStart != null) delegate.run( - zone, _onTurnStart); - return fn(); - } catch (e, s) { - if (_onErrorHandler != null && _nestedRunCounter == 1) { - _onErrorHandler(e, [s.toString()]); - } else { - rethrow; + dynamic _innerRunUnary(Zone self, ZoneDelegate parent, Zone zone, fn(arg), arg) { + _maybeStartVmTurn(parent, zone); + return parent.runUnary(zone, fn, arg); + } + + dynamic _innerRunBinary(Zone self, ZoneDelegate parent, Zone zone, fn(arg1, arg2), arg1, arg2) { + _maybeStartVmTurn(parent, zone); + return parent.runBinary(zone, fn, arg1, arg2); + } + + void _maybeStartVmTurn(ZoneDelegate parent, Zone zone) { + if (!_hasExecutedCodeInInnerZone) { + _hasExecutedCodeInInnerZone = true; + if (_onTurnStart != null) { + _inTurnStart = true; + parent.run(zone, _onTurnStart); } + } + } + + dynamic _outerRun(Zone self, ZoneDelegate parent, Zone zone, fn()) { + try { + _nestedRun++; + return parent.run(zone, fn); } finally { - _nestedRunCounter--; - if (_nestedRunCounter == 0 && _onTurnDone != null) _finishTurn( - zone, delegate); + _nestedRun--; + // If there are no more pending microtasks, we are at the end of a VM turn (or in onTurnStart) + // _nestedRun will be 0 at the end of a macrotasks (it could be > 0 when there are nested calls + // to _outerRun()). + if (_pendingMicrotasks == 0 && _nestedRun == 0) { + if (_onTurnDone != null && !_inTurnStart && _hasExecutedCodeInInnerZone) { + // Trigger onTurnDone at the end of a turn if _innerZone has executed some code + try { + parent.run(_innerZone, _onTurnDone); + } finally { + _hasExecutedCodeInInnerZone = false; + } + } + } + _inTurnStart = false; } } - dynamic _onRun(async.Zone self, async.ZoneDelegate delegate, async.Zone zone, - fn()) => _onRunBase(self, delegate, zone, () => delegate.run(zone, fn)); + dynamic _outerRunUnary(Zone self, ZoneDelegate parent, Zone zone, fn(arg), arg) => + _outerRun(self, parent, zone, () => fn(arg)); - dynamic _onRunUnary(async.Zone self, async.ZoneDelegate delegate, - async.Zone zone, fn(args), args) => - _onRunBase(self, delegate, zone, () => delegate.runUnary(zone, fn, args)); + dynamic _outerRunBinary(Zone self, ZoneDelegate parent, Zone zone, fn(arg1, arg2), arg1, arg2) => + _outerRun(self, parent, zone, () => fn(arg1, arg2)); - void _finishTurn(zone, delegate) { - delegate.run(zone, _onTurnDone); + void _scheduleMicrotask(Zone self, ZoneDelegate parent, Zone zone, fn) { + _pendingMicrotasks++; + var microtask = () { + try { + fn(); + } finally { + _pendingMicrotasks--; + } + }; + parent.scheduleMicrotask(zone, microtask); } - _onMicrotask( - async.Zone self, async.ZoneDelegate delegate, async.Zone zone, fn) { - if (this._onScheduleMicrotask != null) { - _onScheduleMicrotask(fn); - } else { - delegate.scheduleMicrotask(zone, fn); - } - } - - _onErrorWithLongStackTrace(exception, Chain chain) { - final traces = chain.terse.traces.map((t) => t.toString()).toList(); - _onError(exception, traces, chain.traces[0]); - } - _onErrorWithoutLongStackTrace(exception, StackTrace trace) { - _onError(exception, [trace.toString()], trace); - } - - _onError(exception, List traces, StackTrace singleTrace) { + // Called by Chain.capture() on errors when long stack traces are enabled + void _onErrorWithLongStackTrace(error, Chain chain) { if (_onErrorHandler != null) { - _onErrorHandler(exception, traces); + final traces = chain.terse.traces.map((t) => t.toString()).toList(); + _onErrorHandler(error, traces); } else { - _outerZone.handleUncaughtError(exception, singleTrace); + throw error; + } + } + + // Outer zone handleUnchaughtError when long stack traces are not used + void _onErrorWithoutLongStackTrace(error, StackTrace trace) { + if (_onErrorHandler != null) { + _onErrorHandler(error, [trace.toString()]); + } else { + throw error; } } } diff --git a/modules/angular2/src/core/zone/vm_turn_zone.es6 b/modules/angular2/src/core/zone/vm_turn_zone.es6 index ea3a966de7..f33d6d0914 100644 --- a/modules/angular2/src/core/zone/vm_turn_zone.es6 +++ b/modules/angular2/src/core/zone/vm_turn_zone.es6 @@ -7,8 +7,8 @@ import {normalizeBlank, isPresent, global} from 'angular2/src/facade/lang'; * The wrapper maintains an "inner" and "outer" `Zone`. The application code will executes * in the "inner" zone unless `runOutsideAngular` is explicitely called. * - * A typical application will create a singleton `VmTurnZone` whose outer `Zone` is the root `Zone` - * and whose default `onTurnDone` runs the Angular digest. + * A typical application will create a singleton `VmTurnZone`. The outer `Zone` is a fork of the root + * `Zone`. The default `onTurnDone` runs the Angular change detection. * * @exportedAs angular2/core */ @@ -20,25 +20,30 @@ export class VmTurnZone { _onTurnDone:Function; _onErrorHandler:Function; - _nestedRunCounter:number; - /** * Associates with this * - * - an "outer" zone, which is the one that created this. + * - an "outer" zone, which is a child of the one that created this. * - an "inner" zone, which is a child of the outer zone. * * @param {bool} enableLongStackTrace whether to enable long stack trace. They should only be * enabled in development mode as they significantly impact perf. */ constructor({enableLongStackTrace}) { - this._nestedRunCounter = 0; this._onTurnStart = null; this._onTurnDone = null; this._onErrorHandler = null; + Zone._hasExecutedInnerCode = false; - this._outerZone = global.zone; + this._outerZone = global.zone.fork({ + _name: 'outer', + beforeTurn: () => { this._beforeTurn(); }, + afterTurn: () => { this._afterTurn(); } + }); this._innerZone = this._createInnerZone(this._outerZone, enableLongStackTrace); + + // TODO('remove'); + Zone.debug = true; } /** @@ -46,10 +51,9 @@ export class VmTurnZone { * * @param {Function} onTurnStart called before code executes in the inner zone for each VM turn * @param {Function} onTurnDone called at the end of a VM turn if code has executed in the inner zone - * @param {Function} onScheduleMicrotask * @param {Function} onErrorHandler called when an exception is thrown by a macro or micro task */ - initCallbacks({onTurnStart, onTurnDone, onScheduleMicrotask, onErrorHandler} = {}) { + initCallbacks({onTurnStart, onTurnDone, onErrorHandler} = {}) { this._onTurnStart = normalizeBlank(onTurnStart); this._onTurnDone = normalizeBlank(onTurnDone); this._onErrorHandler = normalizeBlank(onErrorHandler); @@ -62,10 +66,10 @@ export class VmTurnZone { * Angular's auto digest mechanism. * * ``` - * var zone: VmTurnZone = ; + * var zone: VmTurnZone = [ref to the application zone]; * * zone.run(() => { - * // auto-digest will run after this function is called from JS + * // the change detection will run after this function and the microtasks it enqueues have executed. * }); * ``` */ @@ -80,7 +84,7 @@ export class VmTurnZone { * auto-digest mechanism. * * ``` - * var zone: VmTurnZone = ; + * var zone: VmTurnZone = [ref to the application zone]; * * zone.runOusideAngular(() => { * element.onClick(() => { @@ -111,23 +115,39 @@ export class VmTurnZone { }; } - return zone.fork(errorHandling).fork({ - beforeTask: () => {this._beforeTask()}, - afterTask: () => {this._afterTask()} - }); + return zone + .fork(errorHandling) + .fork({ + '$run': function (parentRun) { + return function () { + if (!Zone._hasExecutedInnerCode) { + // Execute the beforeTurn hook when code is first executed in the inner zone in the turn + Zone._hasExecutedInnerCode = true; + var oldZone = global.zone; + global.zone = this; + this.beforeTurn(); + global.zone = oldZone; + } + + return parentRun.apply(this, arguments); + } + }, + _name: 'inner' + }); } - _beforeTask(){ - this._nestedRunCounter ++; - if(this._nestedRunCounter === 1 && this._onTurnStart) { - this._onTurnStart(); - } + _beforeTurn() { + this._onTurnStart && this._onTurnStart(); } - _afterTask(){ - this._nestedRunCounter --; - if(this._nestedRunCounter === 0 && this._onTurnDone) { - this._onTurnDone(); + _afterTurn() { + if (this._onTurnDone) { + if (Zone._hasExecutedInnerCode) { + // Execute the onTurnDone hook in the inner zone so that microtasks are enqueued there + // The hook gets executed when code has runned in the inner zone during the current turn + this._innerZone.run(this._onTurnDone, this); + Zone._hasExecutedInnerCode = false; + } } } diff --git a/modules/angular2/src/test_lib/test_lib.dart b/modules/angular2/src/test_lib/test_lib.dart index 889508f652..3d652e142d 100644 --- a/modules/angular2/src/test_lib/test_lib.dart +++ b/modules/angular2/src/test_lib/test_lib.dart @@ -225,3 +225,5 @@ String elementText(n) { return DOM.getText(n); } + +String getCurrentZoneName() => Zone.current['_name']; diff --git a/modules/angular2/src/test_lib/test_lib.es6 b/modules/angular2/src/test_lib/test_lib.es6 index 68a6e60776..d68b2d35aa 100644 --- a/modules/angular2/src/test_lib/test_lib.es6 +++ b/modules/angular2/src/test_lib/test_lib.es6 @@ -135,11 +135,15 @@ function _it(jsmFn, name, fn) { if (!(fn instanceof FunctionWithParamTokens)) { fn = inject([], fn); } - inIt = true; - fn.execute(injector); - inIt = false; - if (!async) done(); + // Use setTimeout() to run tests in a macrotasks. + // Without this, tests would be executed in the context of a microtasks (a promise .then). + setTimeout(() => { + inIt = true; + fn.execute(injector); + inIt = false; + if (!async) done(); + }, 0); }); } @@ -351,3 +355,7 @@ function elementText(n) { return DOM.getText(n); } + +function getCurrentZoneName(): string { + return zone._name; +} diff --git a/modules/angular2/test/core/zone/vm_turn_zone_spec.js b/modules/angular2/test/core/zone/vm_turn_zone_spec.js index fe76da3c25..dd078e20d2 100644 --- a/modules/angular2/test/core/zone/vm_turn_zone_spec.js +++ b/modules/angular2/test/core/zone/vm_turn_zone_spec.js @@ -9,171 +9,622 @@ import { it, xdescribe, xit, + Log } from 'angular2/test_lib'; -import {Log, once} from 'angular2/test_lib'; + import {PromiseWrapper} from 'angular2/src/facade/async'; +import {ListWrapper} from 'angular2/src/facade/collection'; import {BaseException} from 'angular2/src/facade/lang'; + import {VmTurnZone} from 'angular2/src/core/zone/vm_turn_zone'; +// Schedules a macrotask (using a timer) +// The code is executed in the outer zone to properly detect VM turns - in Dart VM turns could not be properly detected +// in the root zone because scheduleMicrotask() is not overriden. +function macroTask(fn: Function): void { + _zone.runOutsideAngular(() => PromiseWrapper.setTimeout(fn, 0)); +} + +// Schedules a microtasks (using a resolved promise .then()) +function microTask(fn: Function): void { + PromiseWrapper.resolve(null).then((_) => { fn(); }); +} + +var _log; +var _errors; +var _traces; +var _zone; + +function logError(error, stackTrace) { + ListWrapper.push(_errors, error); + ListWrapper.push(_traces, stackTrace); +} + export function main() { describe("VmTurnZone", () => { - var log, zone; + + function createZone(enableLongStackTrace) { + var zone = new VmTurnZone({enableLongStackTrace: enableLongStackTrace}); + zone.initCallbacks({ + onTurnStart: _log.fn('onTurnStart'), + onTurnDone: _log.fn('onTurnDone') + }); + return zone; + } beforeEach(() => { - log = new Log(); - zone = new VmTurnZone({enableLongStackTrace: true}); - zone.initCallbacks({ - onTurnStart: log.fn('onTurnStart'), - onTurnDone: log.fn('onTurnDone') - }); + _log = new Log(); + _errors = []; + _traces = []; }); - describe("run", () => { - it('should call onTurnStart and onTurnDone', () => { - zone.run(log.fn('run')); - - expect(log.result()).toEqual('onTurnStart; run; onTurnDone'); - }); - - it('should return the body return value from run', () => { - expect(zone.run(() => 6)).toEqual(6); - }); - - it('should not run onTurnStart and onTurnDone for nested Zone.run', () => { - zone.run(() => { - zone.run(log.fn('run')); - }); - expect(log.result()).toEqual('onTurnStart; run; onTurnDone'); - }); - - - it('should call onTurnStart and onTurnDone before and after each top-level run', () => { - zone.run(log.fn('run1')); - zone.run(log.fn('run2')); - - expect(log.result()).toEqual('onTurnStart; run1; onTurnDone; onTurnStart; run2; onTurnDone'); - }); - - - it('should call onTurnStart and onTurnDone before and after each turn', inject([AsyncTestCompleter], (async) => { - var a = PromiseWrapper.completer(); - var b = PromiseWrapper.completer(); - - zone.run(() => { - log.add('run start'); - a.promise.then((_) => log.add('a then')); - b.promise.then((_) => log.add('b then')); - }); - - a.resolve("a"); - b.resolve("b"); - - PromiseWrapper.all([a.promise, b.promise]).then((_) => { - expect(log.result()).toEqual('onTurnStart; run start; onTurnDone; onTurnStart; a then; onTurnDone; onTurnStart; b then; onTurnDone'); - async.done(); - }); - })); - }); - - describe("runOutsideAngular", () => { - it("should run a function outside of the angular zone", () => { - zone.runOutsideAngular(log.fn('run')); - - expect(log.result()).toEqual('run'); - }); - }); - - describe("exceptions", () => { - var trace, exception, saveStackTrace; + describe('long stack trace', () => { beforeEach(() => { - trace = null; - exception = null; - saveStackTrace = (e, t) => { - exception = e; - trace = t; - }; + _zone = createZone(true); }); - it('should call the on error callback when it is defined', () => { - zone.initCallbacks({onErrorHandler: saveStackTrace}); + commonTests(); - zone.run(() => { - throw new BaseException('aaa'); - }); + it('should produce long stack traces', inject([AsyncTestCompleter], + (async) => { + macroTask(() => { + _zone.initCallbacks({onErrorHandler: logError}); + var c = PromiseWrapper.completer(); - expect(exception).toBeDefined(); - }); - - it('should rethrow exceptions from the body when no callback defined', () => { - expect(() => { - zone.run(() => { - throw new BaseException('bbb'); - }); - }).toThrowError('bbb'); - }); - - it('should produce long stack traces', inject([AsyncTestCompleter], (async) => { - zone.initCallbacks({onErrorHandler: saveStackTrace}); - - var c = PromiseWrapper.completer(); - - zone.run(function () { - PromiseWrapper.setTimeout(function () { - PromiseWrapper.setTimeout(function () { - c.resolve(null); - throw new BaseException('ccc'); + _zone.run(() => { + PromiseWrapper.setTimeout(() => { + PromiseWrapper.setTimeout(() => { + c.resolve(null); + throw new BaseException('ccc'); + }, 0); }, 0); - }, 0); - }); + }); - c.promise.then((_) => { - // then number of traces for JS and Dart is different - expect(trace.length).toBeGreaterThan(1); - async.done(); + c.promise.then((_) => { + expect(_traces.length).toBe(1); + expect(_traces[0].length).toBeGreaterThan(1); + async.done(); + }); }); })); - it('should produce long stack traces (when using promises)', inject([AsyncTestCompleter], (async) => { - zone.initCallbacks({onErrorHandler: saveStackTrace}); + it('should produce long stack traces (when using microtasks)', inject( + [AsyncTestCompleter], (async) => { + macroTask(() => { + _zone.initCallbacks({onErrorHandler: logError}); + var c = PromiseWrapper.completer(); - var c = PromiseWrapper.completer(); - - zone.run(function () { - PromiseWrapper.resolve(null).then((_) => { - return PromiseWrapper.resolve(null).then((__) => { - c.resolve(null); - throw new BaseException("ddd"); + _zone.run(() => { + microTask(() => { + microTask(() => { + c.resolve(null); + throw new BaseException("ddd"); + }); }); }); - }); - c.promise.then((_) => { - // then number of traces for JS and Dart is different - expect(trace.length).toBeGreaterThan(1); - async.done(); + c.promise.then((_) => { + expect(_traces.length).toBe(1); + expect(_traces[0].length).toBeGreaterThan(1); + async.done(); + }); }); })); + }); + + describe('short stack trace', () => { + beforeEach(() => { + _zone = createZone(false); + }); + + commonTests(); it('should disable long stack traces', inject([AsyncTestCompleter], (async) => { - var zone = new VmTurnZone({enableLongStackTrace: false}); - zone.initCallbacks({onErrorHandler: saveStackTrace}); + macroTask(() => { + _zone.initCallbacks({onErrorHandler: logError}); + var c = PromiseWrapper.completer(); - var c = PromiseWrapper.completer(); - - zone.run(function () { - PromiseWrapper.setTimeout(function () { - PromiseWrapper.setTimeout(function () { - c.resolve(null); - throw new BaseException('ccc'); + _zone.run(() => { + PromiseWrapper.setTimeout(() => { + PromiseWrapper.setTimeout(() => { + c.resolve(null); + throw new BaseException('ccc'); + }, 0); }, 0); - }, 0); - }); + }); - c.promise.then((_) => { - expect(trace.length).toEqual(1); - async.done(); + c.promise.then((_) => { + expect(_traces.length).toBe(1); + expect(_traces[0].length).toEqual(1); + async.done(); + }); }); })); }); }); } + +function commonTests() { + describe("run", () => { + it('should return the body return value from run', inject([AsyncTestCompleter], (async) => { + macroTask(() => { + expect(_zone.run(() => { + return 6; + })).toEqual(6); + }); + + macroTask(() => { + async.done(); + }); + })); + + it('should call onTurnStart and onTurnDone', inject([AsyncTestCompleter], (async) => { + macroTask(() => { + _zone.run(_log.fn('run')); + }); + + macroTask(() => { + expect(_log.result()).toEqual('onTurnStart; run; onTurnDone'); + async.done(); + }); + })); + + it('should call onTurnStart once before a turn and onTurnDone once after the turn', + inject([AsyncTestCompleter], (async) => { + + macroTask(() => { + _zone.run(() => { + _log.add('run start'); + microTask(() => { _log.add('async'); }); + _log.add('run end'); + }); + }); + + macroTask(() => { + // The microtask (async) is executed after the macrotask (run) + expect(_log.result()).toEqual('onTurnStart; run start; run end; async; onTurnDone'); + async.done(); + }); + })); + + it('should not run onTurnStart and onTurnDone for nested Zone.run', + inject([AsyncTestCompleter], (async) => { + macroTask(() => { + _zone.run(() => { + _log.add('start run'); + _zone.run(() => { + _log.add('nested run'); + microTask(() => _log.add('nested run microtask')); + }); + _log.add('end run'); + }); + }); + + macroTask(() => { + expect(_log.result()).toEqual('onTurnStart; start run; nested run; end run; nested run microtask; onTurnDone'); + async.done(); + }); + })); + + it('should call onTurnStart and onTurnDone before and after each top-level run', + inject([AsyncTestCompleter], (async) => { + macroTask(() => { + _zone.run(_log.fn('run1')); + _zone.run(_log.fn('run2')); + }); + + macroTask(() => { + _zone.run(_log.fn('run3')); + }); + + macroTask(() => { + expect(_log.result()).toEqual('onTurnStart; run1; run2; onTurnDone; onTurnStart; run3; onTurnDone'); + async.done(); + }); + })); + + it('should call onTurnStart and onTurnDone before and after each turn', + inject([AsyncTestCompleter], (async) => { + var a; + var b; + + macroTask(() => { + a = PromiseWrapper.completer(); + b = PromiseWrapper.completer(); + _zone.run(() => { + _log.add('run start'); + a.promise.then((_) => { + return _log.add('a then'); + }); + b.promise.then((_) => { + return _log.add('b then'); + }); + }); + }); + + macroTask(() => { + a.resolve('a'); + b.resolve('b'); + }); + + macroTask(() => { + expect(_log.result()).toEqual('onTurnStart; run start; onTurnDone; onTurnStart; a then; b then; onTurnDone'); + async.done(); + }); + })); + + it('should run a function outside of the angular zone', inject([AsyncTestCompleter], (async) => { + macroTask(() => { + _zone.runOutsideAngular(_log.fn('run')); + }); + + macroTask(() => { + expect(_log.result()).toEqual('run'); + async.done() + }); + })); + + it('should call onTurnStart and onTurnDone when an inner microtask is scheduled from outside angular', + inject([AsyncTestCompleter], (async) => { + var completer; + + macroTask(() => { + _zone.runOutsideAngular(() => { + completer = PromiseWrapper.completer(); + }); + }); + + macroTask(() => { + _zone.run(() => { + completer.promise.then((_) => { + _log.add('executedMicrotask'); + }); + }); + }); + + macroTask(() => { + _zone.runOutsideAngular(() => { + _log.add('scheduling a microtask'); + completer.resolve(null); + }); + }); + + macroTask(() => { + expect(_log.result()).toEqual( + // First VM turn => setup Promise then + 'onTurnStart; onTurnDone; ' + + // Second VM turn (outside of anguler) + 'scheduling a microtask; ' + + // Third VM Turn => execute the microtask (inside angular) + 'onTurnStart; executedMicrotask; onTurnDone' + ); + async.done(); + }); + })); + + it('should not call onTurnStart and onTurnDone when an outer microtask is scheduled from inside angular', + inject([AsyncTestCompleter], (async) => { + var completer; + + macroTask(() => { + _zone.runOutsideAngular(() => { + completer = PromiseWrapper.completer(); + completer.promise.then((_) => { + _log.add('executedMicrotask'); + }); + }); + }); + + macroTask(() => { + _zone.run(() => { + _log.add('scheduling a microtask'); + completer.resolve(null); + }); + }); + + macroTask(() => { + expect(_log.result()).toEqual( + 'onTurnStart; scheduling a microtask; executedMicrotask; onTurnDone' + ); + async.done(); + }); + })); + + it('should call onTurnStart before executing a microtask scheduled in onTurnDone as well as ' + + 'onTurnDone after executing the task', inject([AsyncTestCompleter], (async) => { + var ran = false; + _zone.initCallbacks({ + onTurnStart: _log.fn('onTurnStart'), + onTurnDone: () => { + _log.add('onTurnDone(begin)'); + if (!ran) { + microTask(() => { + ran = true; + _log.add('executedMicrotask');}); + } + + _log.add('onTurnDone(end)'); + }}); + + macroTask(() => { + _zone.run(() => { + _log.add('run'); + }); + }); + + macroTask(() => { + expect(_log.result()).toEqual( + // First VM turn => 'run' macrotask + 'onTurnStart; run; onTurnDone(begin); onTurnDone(end); ' + + // Second VM Turn => microtask enqueued from onTurnDone + 'onTurnStart; executedMicrotask; onTurnDone(begin); onTurnDone(end)' + ); + async.done(); + }); + })); + + it('should call onTurnStart and onTurnDone for a scheduleMicrotask in onTurnDone triggered by ' + + 'a scheduleMicrotask in run', inject([AsyncTestCompleter], (async) => { + var ran = false; + _zone.initCallbacks({ + onTurnStart: _log.fn('onTurnStart'), + onTurnDone: () => { + _log.add('onTurnDone(begin)'); + if (!ran) { + _log.add('onTurnDone(scheduleMicrotask)'); + microTask(() => { + ran = true; + _log.add('onTurnDone(executeMicrotask)'); + }); + } + _log.add('onTurnDone(end)'); + }}); + + macroTask(() => { + _zone.run(() => { + _log.add('scheduleMicrotask'); + microTask(() => { _log.add('run(executeMicrotask)'); }); + }); + }); + + macroTask(() => { + expect(_log.result()).toEqual( + // First VM Turn => a macrotask + the microtask it enqueues + 'onTurnStart; scheduleMicrotask; run(executeMicrotask); onTurnDone(begin); onTurnDone(scheduleMicrotask); onTurnDone(end); ' + + // Second VM Turn => the microtask enqueued from onTurnDone + 'onTurnStart; onTurnDone(executeMicrotask); onTurnDone(begin); onTurnDone(end)' + ); + async.done(); + + }); + })); + + it('should execute promises scheduled in onTurnStart before promises scheduled in run', + inject([AsyncTestCompleter], (async) => { + var donePromiseRan = false; + var startPromiseRan = false; + + _zone.initCallbacks({ + onTurnStart: () => { + _log.add('onTurnStart(begin)'); + if (!startPromiseRan) { + _log.add('onTurnStart(schedulePromise)'); + microTask(() => { _log.add('onTurnStart(executePromise)'); }); + startPromiseRan = true; + } + _log.add('onTurnStart(end)'); + }, + onTurnDone: () => { + _log.add('onTurnDone(begin)'); + if (!donePromiseRan) { + _log.add('onTurnDone(schedulePromise)'); + microTask(() => { _log.add('onTurnDone(executePromise)'); }); + donePromiseRan = true; + } + _log.add('onTurnDone(end)'); + }}); + + macroTask(() => { + _zone.run(() => { + _log.add('run start'); + PromiseWrapper.resolve(null) + .then((_) => { + _log.add('promise then'); + PromiseWrapper.resolve(null).then((_) => { _log.add('promise foo'); }); + return PromiseWrapper.resolve(null); + }) + .then((_) => { + _log.add('promise bar'); + }); + _log.add('run end'); + }); + }); + + macroTask(() => { + expect(_log.result()).toEqual( + // First VM turn: enqueue a microtask in onTurnStart + 'onTurnStart(begin); onTurnStart(schedulePromise); onTurnStart(end); ' + + // First VM turn: execute the macrotask which enqueues microtasks + 'run start; run end; ' + + // First VM turn: execute enqueued microtasks + 'onTurnStart(executePromise); promise then; promise foo; promise bar; ' + + // First VM turn: onTurnEnd, enqueue a microtask + 'onTurnDone(begin); onTurnDone(schedulePromise); onTurnDone(end); ' + + // Second VM turn: execute the microtask from onTurnEnd + 'onTurnStart(begin); onTurnStart(end); onTurnDone(executePromise); onTurnDone(begin); onTurnDone(end)' + ); + async.done(); + }); + })); + + it('should call onTurnStart and onTurnDone before and after each turn, respectively', + inject([AsyncTestCompleter], (async) => { + var completerA, completerB; + + macroTask(() => { + _zone.run(() => { + completerA = PromiseWrapper.completer(); + completerB = PromiseWrapper.completer(); + completerA.promise.then((_) => _log.add('a then')); + completerB.promise.then((_) => _log.add('b then')); + _log.add('run start'); + }); + }); + + macroTask(() => { + _zone.run(() => { + completerA.resolve(null); + }); + }); + + + macroTask(() => { + _zone.run(() => { + completerB.resolve(null); + }); + }); + + macroTask(() => { + expect(_log.result()).toEqual( + // First VM turn + 'onTurnStart; run start; onTurnDone; ' + + // Second VM turn + 'onTurnStart; a then; onTurnDone; ' + + // Third VM turn + 'onTurnStart; b then; onTurnDone'); + async.done(); + }); + })); + + it('should call onTurnStart and onTurnDone before and after (respectively) all turns in a chain', + inject([AsyncTestCompleter], (async) => { + macroTask(() => { + _zone.run(() => { + _log.add('run start'); + microTask(() => { + _log.add('async1'); + microTask(() => { + _log.add('async2'); + }); + }); + _log.add('run end'); + }); + }); + + macroTask(() => { + expect(_log.result()).toEqual('onTurnStart; run start; run end; async1; async2; onTurnDone'); + async.done(); + }); + })); + + it('should call onTurnStart and onTurnDone for promises created outside of run body', + inject([AsyncTestCompleter], (async) => { + var promise; + + _zone.initCallbacks({ + onTurnStart: _log.fn('onTurnStart'), + onTurnDone: _log.fn('onTurnDone') + }); + + macroTask(() => { + _zone.runOutsideAngular(() => { + promise = PromiseWrapper.resolve(4).then((x) => PromiseWrapper.resolve(x)); + }); + + _zone.run(() => { + promise.then((_) => { + _log.add('promise then'); + }); + _log.add('zone run'); + }); + }); + + macroTask(() => { + expect(_log.result()).toEqual('onTurnStart; zone run; promise then; onTurnDone'); + async.done(); + }); + })); + }); + + describe('exceptions', () => { + it('should call the on error callback when it is defined', inject([AsyncTestCompleter], (async) => { + macroTask(() => { + _zone.initCallbacks({onErrorHandler: logError}); + + var exception = new BaseException('sync'); + + _zone.run(() => { + throw exception; + }); + + expect(_errors.length).toBe(1); + expect(_errors[0]).toBe(exception); + async.done(); + }); + })); + + it('should call onError for errors from microtasks', inject([AsyncTestCompleter], (async) => { + _zone.initCallbacks({onErrorHandler: logError}); + + var exception = new BaseException('async'); + + macroTask(() => { + _zone.run(() => { + microTask(() => { throw exception; }); + }); + }); + + macroTask(() => { + expect(_errors.length).toBe(1); + expect(_errors[0]).toEqual(exception); + async.done(); + }); + })); + + it('should call onError when onTurnDone throws and the zone is sync', + inject([AsyncTestCompleter], (async) => { + var exception = new BaseException('fromOnTurnDone'); + + _zone.initCallbacks({ + onErrorHandler: logError, + onTurnDone: () => { throw exception; } + }); + + macroTask(() => { + _zone.run(() => { }); + }); + + macroTask(() => { + expect(_errors.length).toBe(1); + expect(_errors[0]).toEqual(exception); + async.done(); + }); + })); + + it('should call onError when onTurnDone throws and the zone is async', + inject([AsyncTestCompleter], (async) => { + var asyncRan = false; + + var exception = new BaseException('fromOnTurnDone'); + + _zone.initCallbacks({ + onErrorHandler: logError, + onTurnDone: () => { throw exception; }}); + + macroTask(() => { + _zone.run(() => { + microTask(() => { + asyncRan = true; + }); + }); + }); + + macroTask(() => { + expect(asyncRan).toBe(true); + expect(_errors.length).toBe(1); + expect(_errors[0]).toEqual(exception); + async.done(); + }); + })); + }); +} diff --git a/tools/broccoli/html-replace/SCRIPTS.html b/tools/broccoli/html-replace/SCRIPTS.html index 4b10ee2d32..73ed94f0da 100644 --- a/tools/broccoli/html-replace/SCRIPTS.html +++ b/tools/broccoli/html-replace/SCRIPTS.html @@ -1,7 +1,8 @@ - - + + + diff --git a/tools/broccoli/html-replace/SCRIPTS_benchmarks.html b/tools/broccoli/html-replace/SCRIPTS_benchmarks.html index f5cd484536..e256e49be9 100644 --- a/tools/broccoli/html-replace/SCRIPTS_benchmarks.html +++ b/tools/broccoli/html-replace/SCRIPTS_benchmarks.html @@ -1,8 +1,9 @@ + + + - - diff --git a/tools/broccoli/html-replace/SCRIPTS_benchmarks_external.html b/tools/broccoli/html-replace/SCRIPTS_benchmarks_external.html index 33a99e9eda..d9d3f91e17 100644 --- a/tools/broccoli/html-replace/SCRIPTS_benchmarks_external.html +++ b/tools/broccoli/html-replace/SCRIPTS_benchmarks_external.html @@ -1,9 +1,10 @@ + + + - - diff --git a/tools/broccoli/trees/browser_tree.ts b/tools/broccoli/trees/browser_tree.ts index 7fd04d925d..7557f8d4ed 100644 --- a/tools/broccoli/trees/browser_tree.ts +++ b/tools/broccoli/trees/browser_tree.ts @@ -78,9 +78,10 @@ module.exports = function makeBrowserTree(options, destinationPath) { var vendorScriptsTree = flatten(new Funnel('.', { files: [ + 'zone/es6-promise.js', + 'zone/zone.js', + 'zone/long-stack-trace-zone.js', 'node_modules/es6-module-loader/dist/es6-module-loader-sans-promises.src.js', - 'node_modules/zone.js/zone.js', - 'node_modules/zone.js/long-stack-trace-zone.js', 'node_modules/systemjs/dist/system.src.js', 'node_modules/systemjs/lib/extension-register.js', 'node_modules/systemjs/lib/extension-cjs.js', diff --git a/zone/es6-promise.js b/zone/es6-promise.js new file mode 100644 index 0000000000..d914a1c514 --- /dev/null +++ b/zone/es6-promise.js @@ -0,0 +1,978 @@ +/*! + * @overview es6-promise - a tiny implementation of Promises/A+. + * @copyright Copyright (c) 2014 Yehuda Katz, Tom Dale, Stefan Penner and contributors (Conversion to ES6 API by Jake Archibald) + * @license Licensed under MIT license + * See https://raw.githubusercontent.com/jakearchibald/es6-promise/master/LICENSE + * @version 2.1.1 + */ + +(function() { + "use strict"; + function lib$es6$promise$utils$$objectOrFunction(x) { + return typeof x === 'function' || (typeof x === 'object' && x !== null); + } + + function lib$es6$promise$utils$$isFunction(x) { + return typeof x === 'function'; + } + + function lib$es6$promise$utils$$isMaybeThenable(x) { + return typeof x === 'object' && x !== null; + } + + var lib$es6$promise$utils$$_isArray; + if (!Array.isArray) { + lib$es6$promise$utils$$_isArray = function (x) { + return Object.prototype.toString.call(x) === '[object Array]'; + }; + } else { + lib$es6$promise$utils$$_isArray = Array.isArray; + } + + var lib$es6$promise$utils$$isArray = lib$es6$promise$utils$$_isArray; + + var lib$es6$promise$utils$$_global; + + function lib$es6$promise$utils$$getGlobal() { + if (!lib$es6$promise$utils$$_global) { + if (typeof global !== 'undefined') { + lib$es6$promise$utils$$_global = global; + } else if (typeof self !== 'undefined') { + lib$es6$promise$utils$$_global = self; + } else { + try { + lib$es6$promise$utils$$_global = Function('return this')(); + } catch (e) { + throw new Error('polyfill failed because global object is unavailable in this environment'); + } + } + } + + return lib$es6$promise$utils$$_global; + } + + function lib$es6$promise$utils$$getCurrentZone() { + return lib$es6$promise$utils$$getGlobal().zone; + } + + function lib$es6$promise$utils$$setCurrentZone(zone) { + lib$es6$promise$utils$$getGlobal().zone = zone; + } + function lib$es6$promise$asap$microtask$$asap(callback, arg) { + lib$es6$promise$utils$$getCurrentZone().scheduleMicrotask(function() { + callback(arg); + }); + } + var lib$es6$promise$asap$microtask$$default = lib$es6$promise$asap$microtask$$asap; + + function lib$es6$promise$$internal$$noop() {} + + var lib$es6$promise$$internal$$PENDING = void 0; + var lib$es6$promise$$internal$$FULFILLED = 1; + var lib$es6$promise$$internal$$REJECTED = 2; + var lib$es6$promise$$internal$$INZONE = 3; + + var lib$es6$promise$$internal$$GET_THEN_ERROR = new lib$es6$promise$$internal$$ErrorObject(); + + function lib$es6$promise$$internal$$selfFullfillment() { + return new TypeError("You cannot resolve a promise with itself"); + } + + function lib$es6$promise$$internal$$cannotReturnOwn() { + return new TypeError('A promises callback cannot return that same promise.'); + } + + function lib$es6$promise$$internal$$getThen(promise) { + try { + return promise.then; + } catch(error) { + lib$es6$promise$$internal$$GET_THEN_ERROR.error = error; + return lib$es6$promise$$internal$$GET_THEN_ERROR; + } + } + + function lib$es6$promise$$internal$$tryThen(then, value, fulfillmentHandler, rejectionHandler) { + try { + then.call(value, fulfillmentHandler, rejectionHandler); + } catch(e) { + return e; + } + } + + function lib$es6$promise$$internal$$handleForeignThenable(promise, thenable, then) { + lib$es6$promise$asap$microtask$$default(function(promise) { + var sealed = false; + var error = lib$es6$promise$$internal$$tryThen(then, thenable, function(value) { + if (sealed) { return; } + sealed = true; + if (thenable !== value) { + lib$es6$promise$$internal$$resolve(promise, value); + } else { + lib$es6$promise$$internal$$fulfill(promise, value); + } + }, function(reason) { + if (sealed) { return; } + sealed = true; + + lib$es6$promise$$internal$$reject(promise, reason); + }, 'Settle: ' + (promise._label || ' unknown promise')); + + if (!sealed && error) { + sealed = true; + lib$es6$promise$$internal$$reject(promise, error); + } + }, promise); + } + + function lib$es6$promise$$internal$$handleOwnThenable(promise, thenable) { + if (thenable._state === lib$es6$promise$$internal$$FULFILLED) { + lib$es6$promise$$internal$$fulfill(promise, thenable._result); + } else if (thenable._state === lib$es6$promise$$internal$$REJECTED) { + lib$es6$promise$$internal$$reject(promise, thenable._result); + } else { + lib$es6$promise$$internal$$subscribe(thenable, undefined, function(value) { + lib$es6$promise$$internal$$resolve(promise, value); + }, function(reason) { + lib$es6$promise$$internal$$reject(promise, reason); + }); + } + } + + function lib$es6$promise$$internal$$handleMaybeThenable(promise, maybeThenable) { + if (maybeThenable.constructor === promise.constructor) { + lib$es6$promise$$internal$$handleOwnThenable(promise, maybeThenable); + } else { + var then = lib$es6$promise$$internal$$getThen(maybeThenable); + + if (then === lib$es6$promise$$internal$$GET_THEN_ERROR) { + lib$es6$promise$$internal$$reject(promise, lib$es6$promise$$internal$$GET_THEN_ERROR.error); + } else if (then === undefined) { + lib$es6$promise$$internal$$fulfill(promise, maybeThenable); + } else if (lib$es6$promise$utils$$isFunction(then)) { + lib$es6$promise$$internal$$handleForeignThenable(promise, maybeThenable, then); + } else { + lib$es6$promise$$internal$$fulfill(promise, maybeThenable); + } + } + } + + function lib$es6$promise$$internal$$resolve(promise, value) { + if (promise === value) { + lib$es6$promise$$internal$$reject(promise, lib$es6$promise$$internal$$selfFullfillment()); + } else if (lib$es6$promise$utils$$objectOrFunction(value)) { + lib$es6$promise$$internal$$handleMaybeThenable(promise, value); + } else { + lib$es6$promise$$internal$$fulfill(promise, value); + } + } + + function lib$es6$promise$$internal$$publishRejection(promise) { + if (promise._onerror) { + promise._onerror(promise._result); + } + + lib$es6$promise$$internal$$publish(promise); + } + + function lib$es6$promise$$internal$$fulfill(promise, value) { + if (promise._state !== lib$es6$promise$$internal$$PENDING) { return; } + + promise._result = value; + promise._state = lib$es6$promise$$internal$$FULFILLED; + + if (promise._subscribers.length !== 0) { + lib$es6$promise$asap$microtask$$default(lib$es6$promise$$internal$$publish, promise); + } + } + + function lib$es6$promise$$internal$$reject(promise, reason) { + if (promise._state !== lib$es6$promise$$internal$$PENDING) { return; } + promise._state = lib$es6$promise$$internal$$REJECTED; + promise._result = reason; + + lib$es6$promise$asap$microtask$$default(lib$es6$promise$$internal$$publishRejection, promise); + } + + function lib$es6$promise$$internal$$subscribe(parent, child, onFulfillment, onRejection) { + var subscribers = parent._subscribers; + var length = subscribers.length; + + parent._onerror = null; + + subscribers[length] = child; + subscribers[length + lib$es6$promise$$internal$$FULFILLED] = onFulfillment; + subscribers[length + lib$es6$promise$$internal$$REJECTED] = onRejection; + subscribers[length + lib$es6$promise$$internal$$INZONE] = lib$es6$promise$utils$$getCurrentZone(); + + if (length === 0 && parent._state) { + lib$es6$promise$asap$microtask$$default(lib$es6$promise$$internal$$publish, parent); + } + } + + function lib$es6$promise$$internal$$publish(promise) { + var subscribers = promise._subscribers; + var settled = promise._state; + + if (subscribers.length === 0) { return; } + + var child, callback, zone, detail = promise._result; + + for (var i = 0; i < subscribers.length; i += 4) { + child = subscribers[i]; + callback = subscribers[i + settled]; + zone = subscribers[i + lib$es6$promise$$internal$$INZONE]; + + if (child) { + lib$es6$promise$$internal$$invokeCallback(settled, child, callback, detail, zone); + } else { + zone.run(callback, void 0, [detail]); + } + } + + promise._subscribers.length = 0; + } + + function lib$es6$promise$$internal$$ErrorObject() { + this.error = null; + } + + var lib$es6$promise$$internal$$TRY_CATCH_ERROR = new lib$es6$promise$$internal$$ErrorObject(); + + // .then(successCb, errorCb) microtasks should run in the zone where they were scheduled + // For this to work, the zone error handler should rethrow excpetions + function lib$es6$promise$$internal$$tryCatch(callback, detail, zone) { + try { + return zone.run(callback, void 0, [detail]); + } catch(e) { + lib$es6$promise$$internal$$TRY_CATCH_ERROR.error = e; + return lib$es6$promise$$internal$$TRY_CATCH_ERROR; + } + } + + function lib$es6$promise$$internal$$invokeCallback(settled, promise, callback, detail, zone) { + var hasCallback = lib$es6$promise$utils$$isFunction(callback), + value, error, succeeded, failed; + + if (hasCallback) { + value = lib$es6$promise$$internal$$tryCatch(callback, detail, zone); + + if (value === lib$es6$promise$$internal$$TRY_CATCH_ERROR) { + failed = true; + error = value.error; + value = null; + } else { + succeeded = true; + } + + if (promise === value) { + lib$es6$promise$$internal$$reject(promise, lib$es6$promise$$internal$$cannotReturnOwn()); + return; + } + + } else { + value = detail; + succeeded = true; + } + + if (promise._state !== lib$es6$promise$$internal$$PENDING) { + // noop + } else if (hasCallback && succeeded) { + lib$es6$promise$$internal$$resolve(promise, value); + } else if (failed) { + lib$es6$promise$$internal$$reject(promise, error); + } else if (settled === lib$es6$promise$$internal$$FULFILLED) { + lib$es6$promise$$internal$$fulfill(promise, value); + } else if (settled === lib$es6$promise$$internal$$REJECTED) { + lib$es6$promise$$internal$$reject(promise, value); + } + } + + function lib$es6$promise$$internal$$initializePromise(promise, resolver) { + try { + resolver(function resolvePromise(value){ + lib$es6$promise$$internal$$resolve(promise, value); + }, function rejectPromise(reason) { + lib$es6$promise$$internal$$reject(promise, reason); + }); + } catch(e) { + lib$es6$promise$$internal$$reject(promise, e); + } + } + + var lib$es6$promise$asap$$len = 0; + var lib$es6$promise$asap$$toString = {}.toString; + var lib$es6$promise$asap$$vertxNext; + function lib$es6$promise$asap$$asap(callback, arg) { + lib$es6$promise$asap$$queue[lib$es6$promise$asap$$len] = callback; + lib$es6$promise$asap$$queue[lib$es6$promise$asap$$len + 1] = arg; + lib$es6$promise$asap$$len += 2; + if (lib$es6$promise$asap$$len === 2) { + // If len is 2, that means that we need to schedule an async flush. + // If additional callbacks are queued before the queue is flushed, they + // will be processed by this flush that we are scheduling. + lib$es6$promise$asap$$scheduleFlush(); + } + } + + var lib$es6$promise$asap$$default = lib$es6$promise$asap$$asap; + + var lib$es6$promise$asap$$browserWindow = (typeof window !== 'undefined') ? window : undefined; + var lib$es6$promise$asap$$browserGlobal = lib$es6$promise$asap$$browserWindow || {}; + var lib$es6$promise$asap$$BrowserMutationObserver = lib$es6$promise$asap$$browserGlobal.MutationObserver || lib$es6$promise$asap$$browserGlobal.WebKitMutationObserver; + var lib$es6$promise$asap$$isNode = typeof process !== 'undefined' && {}.toString.call(process) === '[object process]'; + + // test for web worker but not in IE10 + var lib$es6$promise$asap$$isWorker = typeof Uint8ClampedArray !== 'undefined' && + typeof importScripts !== 'undefined' && + typeof MessageChannel !== 'undefined'; + + // node + function lib$es6$promise$asap$$useNextTick() { + var nextTick = process.nextTick; + // node version 0.10.x displays a deprecation warning when nextTick is used recursively + // setImmediate should be used instead instead + var version = process.versions.node.match(/^(?:(\d+)\.)?(?:(\d+)\.)?(\*|\d+)$/); + if (Array.isArray(version) && version[1] === '0' && version[2] === '10') { + nextTick = setImmediate; + } + return function() { + nextTick(lib$es6$promise$asap$$flush); + }; + } + + // vertx + function lib$es6$promise$asap$$useVertxTimer() { + return function() { + lib$es6$promise$asap$$vertxNext(lib$es6$promise$asap$$flush); + }; + } + + function lib$es6$promise$asap$$useMutationObserver() { + var iterations = 0; + var observer = new lib$es6$promise$asap$$BrowserMutationObserver(lib$es6$promise$asap$$flush); + var node = document.createTextNode(''); + observer.observe(node, { characterData: true }); + + return function() { + node.data = (iterations = ++iterations % 2); + }; + } + + // web worker + function lib$es6$promise$asap$$useMessageChannel() { + var channel = new MessageChannel(); + channel.port1.onmessage = lib$es6$promise$asap$$flush; + return function () { + channel.port2.postMessage(0); + }; + } + + function lib$es6$promise$asap$$useSetTimeout() { + return function() { + setTimeout(lib$es6$promise$asap$$flush, 1); + }; + } + + var lib$es6$promise$asap$$queue = new Array(1000); + function lib$es6$promise$asap$$flush() { + for (var i = 0; i < lib$es6$promise$asap$$len; i+=2) { + var callback = lib$es6$promise$asap$$queue[i]; + var arg = lib$es6$promise$asap$$queue[i+1]; + + callback(arg); + + lib$es6$promise$asap$$queue[i] = undefined; + lib$es6$promise$asap$$queue[i+1] = undefined; + } + + lib$es6$promise$asap$$len = 0; + } + + function lib$es6$promise$asap$$attemptVertex() { + try { + var r = require; + var vertx = r('vertx'); + lib$es6$promise$asap$$vertxNext = vertx.runOnLoop || vertx.runOnContext; + return lib$es6$promise$asap$$useVertxTimer(); + } catch(e) { + return lib$es6$promise$asap$$useSetTimeout(); + } + } + + var lib$es6$promise$asap$$scheduleFlush; + // Decide what async method to use to triggering processing of queued callbacks: + if (lib$es6$promise$asap$$isNode) { + lib$es6$promise$asap$$scheduleFlush = lib$es6$promise$asap$$useNextTick(); + } else if (lib$es6$promise$asap$$BrowserMutationObserver) { + lib$es6$promise$asap$$scheduleFlush = lib$es6$promise$asap$$useMutationObserver(); + } else if (lib$es6$promise$asap$$isWorker) { + lib$es6$promise$asap$$scheduleFlush = lib$es6$promise$asap$$useMessageChannel(); + } else if (lib$es6$promise$asap$$browserWindow === undefined && typeof require === 'function') { + lib$es6$promise$asap$$scheduleFlush = lib$es6$promise$asap$$attemptVertex(); + } else { + lib$es6$promise$asap$$scheduleFlush = lib$es6$promise$asap$$useSetTimeout(); + } + + function lib$es6$promise$enumerator$$Enumerator(Constructor, input) { + var enumerator = this; + + enumerator._instanceConstructor = Constructor; + enumerator.promise = new Constructor(lib$es6$promise$$internal$$noop); + + if (enumerator._validateInput(input)) { + enumerator._input = input; + enumerator.length = input.length; + enumerator._remaining = input.length; + + enumerator._init(); + + if (enumerator.length === 0) { + lib$es6$promise$$internal$$fulfill(enumerator.promise, enumerator._result); + } else { + enumerator.length = enumerator.length || 0; + enumerator._enumerate(); + if (enumerator._remaining === 0) { + lib$es6$promise$$internal$$fulfill(enumerator.promise, enumerator._result); + } + } + } else { + lib$es6$promise$$internal$$reject(enumerator.promise, enumerator._validationError()); + } + } + + lib$es6$promise$enumerator$$Enumerator.prototype._validateInput = function(input) { + return lib$es6$promise$utils$$isArray(input); + }; + + lib$es6$promise$enumerator$$Enumerator.prototype._validationError = function() { + return new Error('Array Methods must be provided an Array'); + }; + + lib$es6$promise$enumerator$$Enumerator.prototype._init = function() { + this._result = new Array(this.length); + }; + + var lib$es6$promise$enumerator$$default = lib$es6$promise$enumerator$$Enumerator; + + lib$es6$promise$enumerator$$Enumerator.prototype._enumerate = function() { + var enumerator = this; + + var length = enumerator.length; + var promise = enumerator.promise; + var input = enumerator._input; + + for (var i = 0; promise._state === lib$es6$promise$$internal$$PENDING && i < length; i++) { + enumerator._eachEntry(input[i], i); + } + }; + + lib$es6$promise$enumerator$$Enumerator.prototype._eachEntry = function(entry, i) { + var enumerator = this; + var c = enumerator._instanceConstructor; + + if (lib$es6$promise$utils$$isMaybeThenable(entry)) { + if (entry.constructor === c && entry._state !== lib$es6$promise$$internal$$PENDING) { + entry._onerror = null; + enumerator._settledAt(entry._state, i, entry._result); + } else { + enumerator._willSettleAt(c.resolve(entry), i); + } + } else { + enumerator._remaining--; + enumerator._result[i] = entry; + } + }; + + lib$es6$promise$enumerator$$Enumerator.prototype._settledAt = function(state, i, value) { + var enumerator = this; + var promise = enumerator.promise; + + if (promise._state === lib$es6$promise$$internal$$PENDING) { + enumerator._remaining--; + + if (state === lib$es6$promise$$internal$$REJECTED) { + lib$es6$promise$$internal$$reject(promise, value); + } else { + enumerator._result[i] = value; + } + } + + if (enumerator._remaining === 0) { + lib$es6$promise$$internal$$fulfill(promise, enumerator._result); + } + }; + + lib$es6$promise$enumerator$$Enumerator.prototype._willSettleAt = function(promise, i) { + var enumerator = this; + + lib$es6$promise$$internal$$subscribe(promise, undefined, function(value) { + enumerator._settledAt(lib$es6$promise$$internal$$FULFILLED, i, value); + }, function(reason) { + enumerator._settledAt(lib$es6$promise$$internal$$REJECTED, i, reason); + }); + }; + function lib$es6$promise$promise$all$$all(entries) { + return new lib$es6$promise$enumerator$$default(this, entries).promise; + } + var lib$es6$promise$promise$all$$default = lib$es6$promise$promise$all$$all; + function lib$es6$promise$promise$race$$race(entries) { + /*jshint validthis:true */ + var Constructor = this; + + var promise = new Constructor(lib$es6$promise$$internal$$noop); + + if (!lib$es6$promise$utils$$isArray(entries)) { + lib$es6$promise$$internal$$reject(promise, new TypeError('You must pass an array to race.')); + return promise; + } + + var length = entries.length; + + function onFulfillment(value) { + lib$es6$promise$$internal$$resolve(promise, value); + } + + function onRejection(reason) { + lib$es6$promise$$internal$$reject(promise, reason); + } + + for (var i = 0; promise._state === lib$es6$promise$$internal$$PENDING && i < length; i++) { + lib$es6$promise$$internal$$subscribe(Constructor.resolve(entries[i]), undefined, onFulfillment, onRejection); + } + + return promise; + } + var lib$es6$promise$promise$race$$default = lib$es6$promise$promise$race$$race; + function lib$es6$promise$promise$resolve$$resolve(object) { + /*jshint validthis:true */ + var Constructor = this; + + if (object && typeof object === 'object' && object.constructor === Constructor) { + return object; + } + + var promise = new Constructor(lib$es6$promise$$internal$$noop); + lib$es6$promise$$internal$$resolve(promise, object); + return promise; + } + var lib$es6$promise$promise$resolve$$default = lib$es6$promise$promise$resolve$$resolve; + function lib$es6$promise$promise$reject$$reject(reason) { + /*jshint validthis:true */ + var Constructor = this; + var promise = new Constructor(lib$es6$promise$$internal$$noop); + lib$es6$promise$$internal$$reject(promise, reason); + return promise; + } + var lib$es6$promise$promise$reject$$default = lib$es6$promise$promise$reject$$reject; + + var lib$es6$promise$promise$$counter = 0; + + function lib$es6$promise$promise$$needsResolver() { + throw new TypeError('You must pass a resolver function as the first argument to the promise constructor'); + } + + function lib$es6$promise$promise$$needsNew() { + throw new TypeError("Failed to construct 'Promise': Please use the 'new' operator, this object constructor cannot be called as a function."); + } + + var lib$es6$promise$promise$$default = lib$es6$promise$promise$$Promise; + /** + Promise objects represent the eventual result of an asynchronous operation. The + primary way of interacting with a promise is through its `then` method, which + registers callbacks to receive either a promise’s eventual value or the reason + why the promise cannot be fulfilled. + + Terminology + ----------- + + - `promise` is an object or function with a `then` method whose behavior conforms to this specification. + - `thenable` is an object or function that defines a `then` method. + - `value` is any legal JavaScript value (including undefined, a thenable, or a promise). + - `exception` is a value that is thrown using the throw statement. + - `reason` is a value that indicates why a promise was rejected. + - `settled` the final resting state of a promise, fulfilled or rejected. + + A promise can be in one of three states: pending, fulfilled, or rejected. + + Promises that are fulfilled have a fulfillment value and are in the fulfilled + state. Promises that are rejected have a rejection reason and are in the + rejected state. A fulfillment value is never a thenable. + + Promises can also be said to *resolve* a value. If this value is also a + promise, then the original promise's settled state will match the value's + settled state. So a promise that *resolves* a promise that rejects will + itself reject, and a promise that *resolves* a promise that fulfills will + itself fulfill. + + + Basic Usage: + ------------ + + ```js + var promise = new Promise(function(resolve, reject) { + // on success + resolve(value); + + // on failure + reject(reason); + }); + + promise.then(function(value) { + // on fulfillment + }, function(reason) { + // on rejection + }); + ``` + + Advanced Usage: + --------------- + + Promises shine when abstracting away asynchronous interactions such as + `XMLHttpRequest`s. + + ```js + function getJSON(url) { + return new Promise(function(resolve, reject){ + var xhr = new XMLHttpRequest(); + + xhr.open('GET', url); + xhr.onreadystatechange = handler; + xhr.responseType = 'json'; + xhr.setRequestHeader('Accept', 'application/json'); + xhr.send(); + + function handler() { + if (this.readyState === this.DONE) { + if (this.status === 200) { + resolve(this.response); + } else { + reject(new Error('getJSON: `' + url + '` failed with status: [' + this.status + ']')); + } + } + }; + }); + } + + getJSON('/posts.json').then(function(json) { + // on fulfillment + }, function(reason) { + // on rejection + }); + ``` + + Unlike callbacks, promises are great composable primitives. + + ```js + Promise.all([ + getJSON('/posts'), + getJSON('/comments') + ]).then(function(values){ + values[0] // => postsJSON + values[1] // => commentsJSON + + return values; + }); + ``` + + @class Promise + @param {function} resolver + Useful for tooling. + @constructor + */ + function lib$es6$promise$promise$$Promise(resolver) { + this._id = lib$es6$promise$promise$$counter++; + this._state = undefined; + this._result = undefined; + this._subscribers = []; + + if (lib$es6$promise$$internal$$noop !== resolver) { + if (!lib$es6$promise$utils$$isFunction(resolver)) { + lib$es6$promise$promise$$needsResolver(); + } + + if (!(this instanceof lib$es6$promise$promise$$Promise)) { + lib$es6$promise$promise$$needsNew(); + } + + lib$es6$promise$$internal$$initializePromise(this, resolver); + } + } + + lib$es6$promise$promise$$Promise.all = lib$es6$promise$promise$all$$default; + lib$es6$promise$promise$$Promise.race = lib$es6$promise$promise$race$$default; + lib$es6$promise$promise$$Promise.resolve = lib$es6$promise$promise$resolve$$default; + lib$es6$promise$promise$$Promise.reject = lib$es6$promise$promise$reject$$default; + + lib$es6$promise$promise$$Promise.prototype = { + constructor: lib$es6$promise$promise$$Promise, + + /** + The primary way of interacting with a promise is through its `then` method, + which registers callbacks to receive either a promise's eventual value or the + reason why the promise cannot be fulfilled. + + ```js + findUser().then(function(user){ + // user is available + }, function(reason){ + // user is unavailable, and you are given the reason why + }); + ``` + + Chaining + -------- + + The return value of `then` is itself a promise. This second, 'downstream' + promise is resolved with the return value of the first promise's fulfillment + or rejection handler, or rejected if the handler throws an exception. + + ```js + findUser().then(function (user) { + return user.name; + }, function (reason) { + return 'default name'; + }).then(function (userName) { + // If `findUser` fulfilled, `userName` will be the user's name, otherwise it + // will be `'default name'` + }); + + findUser().then(function (user) { + throw new Error('Found user, but still unhappy'); + }, function (reason) { + throw new Error('`findUser` rejected and we're unhappy'); + }).then(function (value) { + // never reached + }, function (reason) { + // if `findUser` fulfilled, `reason` will be 'Found user, but still unhappy'. + // If `findUser` rejected, `reason` will be '`findUser` rejected and we're unhappy'. + }); + ``` + If the downstream promise does not specify a rejection handler, rejection reasons will be propagated further downstream. + + ```js + findUser().then(function (user) { + throw new PedagogicalException('Upstream error'); + }).then(function (value) { + // never reached + }).then(function (value) { + // never reached + }, function (reason) { + // The `PedgagocialException` is propagated all the way down to here + }); + ``` + + Assimilation + ------------ + + Sometimes the value you want to propagate to a downstream promise can only be + retrieved asynchronously. This can be achieved by returning a promise in the + fulfillment or rejection handler. The downstream promise will then be pending + until the returned promise is settled. This is called *assimilation*. + + ```js + findUser().then(function (user) { + return findCommentsByAuthor(user); + }).then(function (comments) { + // The user's comments are now available + }); + ``` + + If the assimliated promise rejects, then the downstream promise will also reject. + + ```js + findUser().then(function (user) { + return findCommentsByAuthor(user); + }).then(function (comments) { + // If `findCommentsByAuthor` fulfills, we'll have the value here + }, function (reason) { + // If `findCommentsByAuthor` rejects, we'll have the reason here + }); + ``` + + Simple Example + -------------- + + Synchronous Example + + ```javascript + var result; + + try { + result = findResult(); + // success + } catch(reason) { + // failure + } + ``` + + Errback Example + + ```js + findResult(function(result, err){ + if (err) { + // failure + } else { + // success + } + }); + ``` + + Promise Example; + + ```javascript + findResult().then(function(result){ + // success + }, function(reason){ + // failure + }); + ``` + + Advanced Example + -------------- + + Synchronous Example + + ```javascript + var author, books; + + try { + author = findAuthor(); + books = findBooksByAuthor(author); + // success + } catch(reason) { + // failure + } + ``` + + Errback Example + + ```js + + function foundBooks(books) { + + } + + function failure(reason) { + + } + + findAuthor(function(author, err){ + if (err) { + failure(err); + // failure + } else { + try { + findBoooksByAuthor(author, function(books, err) { + if (err) { + failure(err); + } else { + try { + foundBooks(books); + } catch(reason) { + failure(reason); + } + } + }); + } catch(error) { + failure(err); + } + // success + } + }); + ``` + + Promise Example; + + ```javascript + findAuthor(). + then(findBooksByAuthor). + then(function(books){ + // found books + }).catch(function(reason){ + // something went wrong + }); + ``` + + @method then + @param {Function} onFulfilled + @param {Function} onRejected + Useful for tooling. + @return {Promise} + */ + then: function(onFulfillment, onRejection) { + var parent = this; + var state = parent._state; + + if (state === lib$es6$promise$$internal$$FULFILLED && !onFulfillment || state === lib$es6$promise$$internal$$REJECTED && !onRejection) { + return this; + } + + var child = new this.constructor(lib$es6$promise$$internal$$noop); + var result = parent._result; + var zone = lib$es6$promise$utils$$getCurrentZone(); + + if (state) { + var callback = arguments[state - 1]; + lib$es6$promise$asap$microtask$$default(function() { + lib$es6$promise$$internal$$invokeCallback(state, child, callback, result, zone); + }); + } else { + lib$es6$promise$$internal$$subscribe(parent, child, onFulfillment, onRejection); + } + + return child; + }, + + /** + `catch` is simply sugar for `then(undefined, onRejection)` which makes it the same + as the catch block of a try/catch statement. + + ```js + function findAuthor(){ + throw new Error('couldn't find that author'); + } + + // synchronous + try { + findAuthor(); + } catch(reason) { + // something went wrong + } + + // async with promises + findAuthor().catch(function(reason){ + // something went wrong + }); + ``` + + @method catch + @param {Function} onRejection + Useful for tooling. + @return {Promise} + */ + 'catch': function(onRejection) { + return this.then(null, onRejection); + } + }; + function lib$es6$promise$polyfill$$polyfill() { + lib$es6$promise$utils$$getGlobal().Promise = lib$es6$promise$promise$$default; + } + var lib$es6$promise$polyfill$$default = lib$es6$promise$polyfill$$polyfill; + + var lib$es6$promise$umd$$ES6Promise = { + 'Promise': lib$es6$promise$promise$$default, + 'polyfill': lib$es6$promise$polyfill$$default + }; + + /* global define:true module:true window: true */ + if (typeof define === 'function' && define['amd']) { + define(function() { return lib$es6$promise$umd$$ES6Promise; }); + } else if (typeof module !== 'undefined' && module['exports']) { + module['exports'] = lib$es6$promise$umd$$ES6Promise; + } else if (typeof this !== 'undefined') { + this['ES6Promise'] = lib$es6$promise$umd$$ES6Promise; + } + + lib$es6$promise$polyfill$$default(); +}).call(this); + diff --git a/zone/long-stack-trace-zone.js b/zone/long-stack-trace-zone.js new file mode 100644 index 0000000000..2199b2a538 --- /dev/null +++ b/zone/long-stack-trace-zone.js @@ -0,0 +1,88 @@ +/* + * Wrapped stacktrace + * + * We need this because in some implementations, constructing a trace is slow + * and so we want to defer accessing the trace for as long as possible + */ +Zone.Stacktrace = function (e) { + this._e = e; +}; +Zone.Stacktrace.prototype.get = function () { + if (zone.stackFramesFilter) { + return this._e.stack. + split('\n'). + filter(zone.stackFramesFilter). + join('\n'); + } + return this._e.stack; +} + +Zone.getStacktrace = function () { + function getStacktraceWithUncaughtError () { + return new Zone.Stacktrace(new Error()); + } + + function getStacktraceWithCaughtError () { + try { + throw new Error(); + } catch (e) { + return new Zone.Stacktrace(e); + } + } + + // Some implementations of exception handling don't create a stack trace if the exception + // isn't thrown, however it's faster not to actually throw the exception. + var stack = getStacktraceWithUncaughtError(); + if (stack && stack._e.stack) { + Zone.getStacktrace = getStacktraceWithUncaughtError; + return stack; + } else { + Zone.getStacktrace = getStacktraceWithCaughtError; + return Zone.getStacktrace(); + } +}; + +Zone.longStackTraceZone = { + getLongStacktrace: function (exception) { + var trace = []; + var zone = this; + if (exception) { + if (zone.stackFramesFilter) { + trace.push(exception.stack.split('\n'). + filter(zone.stackFramesFilter). + join('\n')); + } else { + trace.push(exception.stack); + } + } + var now = Date.now(); + while (zone && zone.constructedAtException) { + trace.push( + '--- ' + (Date(zone.constructedAtTime)).toString() + + ' - ' + (now - zone.constructedAtTime) + 'ms ago', + zone.constructedAtException.get()); + zone = zone.parent; + } + return trace.join('\n'); + }, + + stackFramesFilter: function (line) { + return line.indexOf('zone.js') === -1; + }, + + onError: function (exception) { + var reporter = this.reporter || console.log.bind(console); + reporter(exception.toString()); + reporter(this.getLongStacktrace(exception)); + }, + + fork: function (locals) { + var newZone = this._fork(locals); + newZone.constructedAtException = Zone.getStacktrace(); + newZone.constructedAtTime = Date.now(); + return newZone; + }, + + _fork: zone.fork +}; + diff --git a/zone/zone.js b/zone/zone.js new file mode 100644 index 0000000000..2ca5983493 --- /dev/null +++ b/zone/zone.js @@ -0,0 +1,716 @@ +'use strict'; + +(function (exports) { + +var zone = null; + + +function Zone(parentZone, data) { + var zone = (arguments.length) ? Object.create(parentZone) : this; + + zone.parent = parentZone; + + Object.keys(data || {}).forEach(function(property) { + + var _property = property.substr(1); + + // augment the new zone with a hook decorates the parent's hook + if (property[0] === '$') { + zone[_property] = data[property](parentZone[_property] || function () {}); + + // augment the new zone with a hook that runs after the parent's hook + } else if (property[0] === '+') { + if (parentZone[_property]) { + zone[_property] = function () { + var result = parentZone[_property].apply(this, arguments); + data[property].apply(this, arguments); + return result; + }; + } else { + zone[_property] = data[property]; + } + + // augment the new zone with a hook that runs before the parent's hook + } else if (property[0] === '-') { + if (parentZone[_property]) { + zone[_property] = function () { + data[property].apply(this, arguments); + return parentZone[_property].apply(this, arguments); + }; + } else { + zone[_property] = data[property]; + } + + // set the new zone's hook (replacing the parent zone's) + } else { + zone[property] = (typeof data[property] === 'object') ? + JSON.parse(JSON.stringify(data[property])) : + data[property]; + } + }); + + zone.$id = ++Zone.nextId; + + return zone; +} + +Zone.prototype = { + constructor: Zone, + + fork: function (locals) { + this.onZoneCreated(); + return new Zone(this, locals); + }, + + bind: function (fn, skipEnqueue) { + skipEnqueue || this.enqueueTask(fn); + var zone = this.fork(); + return function zoneBoundFn() { + return zone.run(fn, this, arguments); + }; + }, + + bindOnce: function (fn) { + var boundZone = this; + return this.bind(function () { + var result = fn.apply(this, arguments); + boundZone.dequeueTask(fn); + return result; + }); + }, + + scheduleMicrotask: function (fn) { + var executionZone = this; + Zone.microtaskQueue.push(function() { + executionZone.run(fn); + }); + }, + + run: function run (fn, applyTo, applyWith) { + applyWith = applyWith || []; + + var oldZone = zone, + result; + + exports.zone = zone = this; + + try { + Zone.nestedRun++; + this.beforeTask(); + return fn.apply(applyTo, applyWith); + } catch (e) { + if (zone.onError) { + zone.onError(e); + } else { + throw e; + } + } finally { + this.afterTask(); + Zone.nestedRun--; + // Check if there are microtasks to execute unless: + // - we are already executing them (drainingMicrotasks is true), + // - we are in a recursive call to run (nesetdRun > 0) + if (!Zone.drainingMicrotasks && Zone.nestedRun == 0) { + this.runMicrotasks(); + } + exports.zone = zone = oldZone; + } + }, + + runMicrotasks: function () { + Zone.drainingMicrotasks = true; + do { + // Drain the microtask queue + while (Zone.microtaskQueue.length > 0) { + var microtask = Zone.microtaskQueue.shift(); + microtask(); + } + this.afterTurn(); + // Check the queue length again as afterTurn might have enqueued more microtasks + } while (Zone.microtaskQueue.length > 0) + Zone.drainingMicrotasks = false; + }, + + afterTurn: function() {}, + beforeTask: function () {}, + onZoneCreated: function () {}, + afterTask: function () {}, + enqueueTask: function () {}, + dequeueTask: function () {} +}; + + +Zone.patchSetClearFn = function (obj, fnNames) { + fnNames.map(function (name) { + return name[0].toUpperCase() + name.substr(1); + }). + forEach(function (name) { + var setName = 'set' + name; + var delegate = obj[setName]; + + if (delegate) { + var clearName = 'clear' + name; + var ids = {}; + + var bindArgs = setName === 'setInterval' ? Zone.bindArguments : Zone.bindArgumentsOnce; + + zone[setName] = function (fn) { + var id, fnRef = fn; + arguments[0] = function () { + delete ids[id]; + return fnRef.apply(this, arguments); + }; + var args = bindArgs(arguments); + id = delegate.apply(obj, args); + ids[id] = true; + return id; + }; + + obj[setName] = function () { + return zone[setName].apply(this, arguments); + }; + + var clearDelegate = obj[clearName]; + + zone[clearName] = function (id) { + if (ids[id]) { + delete ids[id]; + zone.dequeueTask(); + } + return clearDelegate.apply(this, arguments); + }; + + obj[clearName] = function () { + return zone[clearName].apply(this, arguments); + }; + } + }); +}; + +Zone.nextId = 1; +// Pending microtasks to be executed after the macrotask +Zone.microtaskQueue = []; +// Whether we are currently draining the microtask queue +Zone.drainingMicrotasks = false; +// Recursive calls to run +Zone.nestedRun = 0; + +Zone.patchSetFn = function (obj, fnNames) { + fnNames.forEach(function (name) { + var delegate = obj[name]; + + if (delegate) { + zone[name] = function (fn) { + var fnRef = fn; + arguments[0] = function () { + return fnRef.apply(this, arguments); + }; + var args = Zone.bindArgumentsOnce(arguments); + return delegate.apply(obj, args); + }; + + obj[name] = function () { + return zone[name].apply(this, arguments); + }; + } + }); +}; + +Zone.patchPrototype = function (obj, fnNames) { + fnNames.forEach(function (name) { + var delegate = obj[name]; + if (delegate) { + obj[name] = function () { + return delegate.apply(this, Zone.bindArguments(arguments)); + }; + } + }); +}; + +Zone.bindArguments = function (args) { + for (var i = args.length - 1; i >= 0; i--) { + if (typeof args[i] === 'function') { + args[i] = zone.bind(args[i]); + } + } + return args; +}; + + +Zone.bindArgumentsOnce = function (args) { + for (var i = args.length - 1; i >= 0; i--) { + if (typeof args[i] === 'function') { + args[i] = zone.bindOnce(args[i]); + } + } + return args; +}; + +/* + * patch a fn that returns a promise + */ +Zone.bindPromiseFn = (function() { + // if the browser natively supports Promises, we can just return a native promise + if (window.Promise) { + return function (delegate) { + return function() { + var delegatePromise = delegate.apply(this, arguments); + if (delegatePromise instanceof Promise) { + return delegatePromise; + } else { + return new Promise(function(resolve, reject) { + delegatePromise.then(resolve, reject); + }); + } + }; + }; + } else { + // if the browser does not have native promises, we have to patch each promise instance + return function (delegate) { + return function () { + return patchThenable(delegate.apply(this, arguments)); + }; + }; + } + + function patchThenable(thenable) { + var then = thenable.then; + thenable.then = function () { + var args = Zone.bindArguments(arguments); + var nextThenable = then.apply(thenable, args); + return patchThenable(nextThenable); + }; + + var ocatch = thenable.catch; + thenable.catch = function () { + var args = Zone.bindArguments(arguments); + var nextThenable = ocatch.apply(thenable, args); + return patchThenable(nextThenable); + }; + return thenable; + } +}()); + + +Zone.patchableFn = function (obj, fnNames) { + fnNames.forEach(function (name) { + var delegate = obj[name]; + zone[name] = function () { + return delegate.apply(obj, arguments); + }; + + obj[name] = function () { + return zone[name].apply(this, arguments); + }; + }); +}; + +Zone.patchProperty = function (obj, prop) { + var desc = Object.getOwnPropertyDescriptor(obj, prop) || { + enumerable: true, + configurable: true + }; + + // A property descriptor cannot have getter/setter and be writable + // deleting the writable and value properties avoids this error: + // + // TypeError: property descriptors must not specify a value or be writable when a + // getter or setter has been specified + delete desc.writable; + delete desc.value; + + // substr(2) cuz 'onclick' -> 'click', etc + var eventName = prop.substr(2); + var _prop = '_' + prop; + + desc.set = function (fn) { + if (this[_prop]) { + this.removeEventListener(eventName, this[_prop]); + } + + if (typeof fn === 'function') { + this[_prop] = fn; + this.addEventListener(eventName, fn, false); + } else { + this[_prop] = null; + } + }; + + desc.get = function () { + return this[_prop]; + }; + + Object.defineProperty(obj, prop, desc); +}; + +Zone.patchProperties = function (obj, properties) { + + (properties || (function () { + var props = []; + for (var prop in obj) { + props.push(prop); + } + return props; + }()). + filter(function (propertyName) { + return propertyName.substr(0,2) === 'on'; + })). + forEach(function (eventName) { + Zone.patchProperty(obj, eventName); + }); +}; + +Zone.patchEventTargetMethods = function (obj) { + var addDelegate = obj.addEventListener; + obj.addEventListener = function (eventName, fn) { + arguments[1] = fn._bound = zone.bind(fn); + return addDelegate.apply(this, arguments); + }; + + var removeDelegate = obj.removeEventListener; + obj.removeEventListener = function (eventName, fn) { + arguments[1] = arguments[1]._bound || arguments[1]; + var result = removeDelegate.apply(this, arguments); + zone.dequeueTask(fn); + return result; + }; +}; + +Zone.patch = function patch () { + Zone.patchSetClearFn(window, [ + 'timeout', + 'interval', + 'immediate' + ]); + + Zone.patchSetFn(window, [ + 'requestAnimationFrame', + 'mozRequestAnimationFrame', + 'webkitRequestAnimationFrame' + ]); + + Zone.patchableFn(window, ['alert', 'prompt']); + + // patched properties depend on addEventListener, so this needs to come first + if (window.EventTarget) { + Zone.patchEventTargetMethods(window.EventTarget.prototype); + + // Note: EventTarget is not available in all browsers, + // if it's not available, we instead patch the APIs in the IDL that inherit from EventTarget + } else { + [ 'ApplicationCache', + 'EventSource', + 'FileReader', + 'InputMethodContext', + 'MediaController', + 'MessagePort', + 'Node', + 'Performance', + 'SVGElementInstance', + 'SharedWorker', + 'TextTrack', + 'TextTrackCue', + 'TextTrackList', + 'WebKitNamedFlow', + 'Window', + 'Worker', + 'WorkerGlobalScope', + 'XMLHttpRequestEventTarget', + 'XMLHttpRequestUpload' + ]. + filter(function (thing) { + return window[thing]; + }). + map(function (thing) { + return window[thing].prototype; + }). + forEach(Zone.patchEventTargetMethods); + } + + if (Zone.canPatchViaPropertyDescriptor()) { + Zone.patchViaPropertyDescriptor(); + } else { + Zone.patchViaCapturingAllTheEvents(); + Zone.patchClass('XMLHttpRequest'); + Zone.patchWebSocket(); + } + + // Do not patch promises when using out own version supporting microtasks + //// patch promises + //if (window.Promise) { + // Zone.patchPrototype(Promise.prototype, [ + // 'then', + // 'catch' + // ]); + //} + Zone.patchMutationObserverClass('MutationObserver'); + Zone.patchMutationObserverClass('WebKitMutationObserver'); + Zone.patchDefineProperty(); + Zone.patchRegisterElement(); +}; + +// +Zone.canPatchViaPropertyDescriptor = function () { + if (!Object.getOwnPropertyDescriptor(HTMLElement.prototype, 'onclick') && + typeof Element !== 'undefined') { + // WebKit https://bugs.webkit.org/show_bug.cgi?id=134364 + // IDL interface attributes are not configurable + var desc = Object.getOwnPropertyDescriptor(Element.prototype, 'onclick'); + if (desc && !desc.configurable) return false; + } + + Object.defineProperty(HTMLElement.prototype, 'onclick', { + get: function () { + return true; + } + }); + var elt = document.createElement('div'); + var result = !!elt.onclick; + Object.defineProperty(HTMLElement.prototype, 'onclick', {}); + return result; +}; + +// for browsers that we can patch the descriptor: +// - eventually Chrome once this bug gets resolved +// - Firefox +Zone.patchViaPropertyDescriptor = function () { + Zone.patchProperties(HTMLElement.prototype, Zone.onEventNames); + Zone.patchProperties(XMLHttpRequest.prototype); +}; + +// Whenever any event fires, we check the event target and all parents +// for `onwhatever` properties and replace them with zone-bound functions +// - Chrome (for now) +Zone.patchViaCapturingAllTheEvents = function () { + Zone.eventNames.forEach(function (property) { + var onproperty = 'on' + property; + document.addEventListener(property, function (event) { + var elt = event.target, bound; + while (elt) { + if (elt[onproperty] && !elt[onproperty]._unbound) { + bound = zone.bind(elt[onproperty]); + bound._unbound = elt[onproperty]; + elt[onproperty] = bound; + } + elt = elt.parentElement; + } + }, true); + }); +}; + +// we have to patch the instance since the proto is non-configurable +Zone.patchWebSocket = function() { + var WS = window.WebSocket; + window.WebSocket = function(a, b) { + var socket = arguments.length > 1 ? new WS(a, b) : new WS(a); + Zone.patchProperties(socket, ['onclose', 'onerror', 'onmessage', 'onopen']); + return socket; + }; +} + + +// wrap some native API on `window` +Zone.patchClass = function (className) { + var OriginalClass = window[className]; + if (!OriginalClass) { + return; + } + window[className] = function () { + var a = Zone.bindArguments(arguments); + switch (a.length) { + case 0: this._o = new OriginalClass(); break; + case 1: this._o = new OriginalClass(a[0]); break; + case 2: this._o = new OriginalClass(a[0], a[1]); break; + case 3: this._o = new OriginalClass(a[0], a[1], a[2]); break; + case 4: this._o = new OriginalClass(a[0], a[1], a[2], a[3]); break; + default: throw new Error('what are you even doing?'); + } + }; + + var instance = new OriginalClass(className.substr(-16) === 'MutationObserver' ? function () {} : undefined); + + var prop; + for (prop in instance) { + (function (prop) { + if (typeof instance[prop] === 'function') { + window[className].prototype[prop] = function () { + return this._o[prop].apply(this._o, arguments); + }; + } else { + Object.defineProperty(window[className].prototype, prop, { + set: function (fn) { + if (typeof fn === 'function') { + this._o[prop] = zone.bind(fn); + } else { + this._o[prop] = fn; + } + }, + get: function () { + return this._o[prop]; + } + }); + } + }(prop)); + }; +}; + + +// wrap some native API on `window` +Zone.patchMutationObserverClass = function (className) { + var OriginalClass = window[className]; + if (!OriginalClass) { + return; + } + window[className] = function (fn) { + this._o = new OriginalClass(zone.bind(fn, true)); + }; + + var instance = new OriginalClass(function () {}); + + window[className].prototype.disconnect = function () { + var result = this._o.disconnect.apply(this._o, arguments); + this._active && zone.dequeueTask(); + this._active = false; + return result; + }; + + window[className].prototype.observe = function () { + if (!this._active) { + zone.enqueueTask(); + } + this._active = true; + return this._o.observe.apply(this._o, arguments); + }; + + var prop; + for (prop in instance) { + (function (prop) { + if (typeof window[className].prototype !== undefined) { + return; + } + if (typeof instance[prop] === 'function') { + window[className].prototype[prop] = function () { + return this._o[prop].apply(this._o, arguments); + }; + } else { + Object.defineProperty(window[className].prototype, prop, { + set: function (fn) { + if (typeof fn === 'function') { + this._o[prop] = zone.bind(fn); + } else { + this._o[prop] = fn; + } + }, + get: function () { + return this._o[prop]; + } + }); + } + }(prop)); + } +}; + +// might need similar for object.freeze +// i regret nothing +Zone.patchDefineProperty = function () { + var _defineProperty = Object.defineProperty; + var _getOwnPropertyDescriptor = Object.getOwnPropertyDescriptor; + var _create = Object.create; + + Object.defineProperty = function (obj, prop, desc) { + if (isUnconfigurable(obj, prop)) { + throw new TypeError('Cannot assign to read only property \'' + prop + '\' of ' + obj); + } + if (prop !== 'prototype') { + desc = rewriteDescriptor(obj, prop, desc); + } + return _defineProperty(obj, prop, desc); + }; + + Object.defineProperties = function (obj, props) { + Object.keys(props).forEach(function (prop) { + Object.defineProperty(obj, prop, props[prop]); + }); + return obj; + }; + + Object.create = function (obj, proto) { + if (typeof proto === 'object') { + Object.keys(proto).forEach(function (prop) { + proto[prop] = rewriteDescriptor(obj, prop, proto[prop]); + }); + } + return _create(obj, proto); + }; + + Object.getOwnPropertyDescriptor = function (obj, prop) { + var desc = _getOwnPropertyDescriptor(obj, prop); + if (isUnconfigurable(obj, prop)) { + desc.configurable = false; + } + return desc; + }; + + Zone._redefineProperty = function (obj, prop, desc) { + desc = rewriteDescriptor(obj, prop, desc); + return _defineProperty(obj, prop, desc); + }; + + function isUnconfigurable (obj, prop) { + return obj && obj.__unconfigurables && obj.__unconfigurables[prop]; + } + + function rewriteDescriptor (obj, prop, desc) { + desc.configurable = true; + if (!desc.configurable) { + if (!obj.__unconfigurables) { + _defineProperty(obj, '__unconfigurables', { writable: true, value: {} }); + } + obj.__unconfigurables[prop] = true; + } + return desc; + } +}; + +Zone.patchRegisterElement = function () { + if (!('registerElement' in document)) { + return; + } + var _registerElement = document.registerElement; + var callbacks = [ + 'createdCallback', + 'attachedCallback', + 'detachedCallback', + 'attributeChangedCallback' + ]; + document.registerElement = function (name, opts) { + callbacks.forEach(function (callback) { + if (opts.prototype[callback]) { + var descriptor = Object.getOwnPropertyDescriptor(opts.prototype, callback); + if (descriptor.value) { + descriptor.value = zone.bind(descriptor.value || opts.prototype[callback]); + Zone._redefineProperty(opts.prototype, callback, descriptor); + } + } + }); + return _registerElement.apply(document, [name, opts]); + }; +} + +Zone.eventNames = 'copy cut paste abort blur focus canplay canplaythrough change click contextmenu dblclick drag dragend dragenter dragleave dragover dragstart drop durationchange emptied ended input invalid keydown keypress keyup load loadeddata loadedmetadata loadstart message mousedown mouseenter mouseleave mousemove mouseout mouseover mouseup pause play playing progress ratechange reset scroll seeked seeking select show stalled submit suspend timeupdate volumechange waiting mozfullscreenchange mozfullscreenerror mozpointerlockchange mozpointerlockerror error webglcontextrestored webglcontextlost webglcontextcreationerror'.split(' '); +Zone.onEventNames = Zone.eventNames.map(function (property) { + return 'on' + property; +}); + +Zone.init = function init () { + exports.zone = zone = new Zone(); + Zone.patch(); +}; + + +Zone.init(); + +exports.Zone = Zone; + +}((typeof module !== 'undefined' && module && module.exports) ? + module.exports : window));