From 1741e70e0a2b68063e3eba1a7fc826f830b95a4d Mon Sep 17 00:00:00 2001 From: "JiaLi.Passion" Date: Mon, 29 May 2017 00:32:48 +0900 Subject: [PATCH 1/7] WIP(core): use asynchooks to resolve es2017 native async/await issue --- gulpfile.js | 9 ++ lib/browser/define-property.ts | 4 +- lib/common/promise.ts | 12 ++- lib/node/async_hooks_promise.ts | 64 ++++++++++++++ lib/node/async_promise.ts | 91 ++++++++++++++++++++ lib/node/node.ts | 1 - lib/node/rollup-main.ts | 1 + lib/zone.ts | 106 +++++++++++++++++------ test/common/Promise.spec.ts | 140 ++++++++++++++++++++++--------- test/common/task.spec.ts | 3 +- test/common/zone.spec.ts | 16 +++- test/common_tests.ts | 3 +- test/node_async.ts | 71 ++++++++++++++++ test/node_entry_point.ts | 3 +- test/test-util.ts | 7 ++ test/zone-spec/sync-test.spec.ts | 2 +- tsconfig-esm-node.json | 3 +- tsconfig-esm.json | 3 +- tsconfig-node.es2017.json | 24 ++++++ tsconfig-node.json | 2 +- tsconfig.json | 3 +- 21 files changed, 488 insertions(+), 80 deletions(-) create mode 100644 lib/node/async_hooks_promise.ts create mode 100644 lib/node/async_promise.ts create mode 100644 test/node_async.ts create mode 100644 tsconfig-node.es2017.json diff --git a/gulpfile.js b/gulpfile.js index 0bb8c47fb..7d0d181cc 100644 --- a/gulpfile.js +++ b/gulpfile.js @@ -82,6 +82,10 @@ gulp.task('compile-node', function(cb) { tsc('tsconfig-node.json', cb); }); +gulp.task('compile-node-es2017', function(cb) { + tsc('tsconfig-node.es2017.json', cb); +}); + gulp.task('compile-esm', function(cb) { tsc('tsconfig-esm.json', cb); }); @@ -310,6 +314,11 @@ gulp.task('build', [ 'build/closure.js' ]); +gulp.task('test/node2017', ['compile-node-es2017'], function(cb) { + var testAsyncPromise = require('./build/test/node_async').testAsyncPromise; + testAsyncPromise(); +}); + gulp.task('test/node', ['compile-node'], function(cb) { var JasmineRunner = require('jasmine'); var jrunner = new JasmineRunner(); diff --git a/lib/browser/define-property.ts b/lib/browser/define-property.ts index 2a2f0e18f..a1b02b3e5 100644 --- a/lib/browser/define-property.ts +++ b/lib/browser/define-property.ts @@ -22,7 +22,7 @@ const OBJECT = 'object'; const UNDEFINED = 'undefined'; export function propertyPatch() { - Object.defineProperty = function(obj, prop, desc) { + Object.defineProperty = function(obj: any, prop: string, desc: any) { if (isUnconfigurable(obj, prop)) { throw new TypeError('Cannot assign to read only property \'' + prop + '\' of ' + obj); } @@ -49,7 +49,7 @@ export function propertyPatch() { return _create(obj, proto); }; - Object.getOwnPropertyDescriptor = function(obj, prop) { + Object.getOwnPropertyDescriptor = function(obj: any, prop: string) { const desc = _getOwnPropertyDescriptor(obj, prop); if (isUnconfigurable(obj, prop)) { desc.configurable = false; diff --git a/lib/common/promise.ts b/lib/common/promise.ts index 7d0484f4a..3f0113d1c 100644 --- a/lib/common/promise.ts +++ b/lib/common/promise.ts @@ -233,7 +233,8 @@ Zone.__load_patch('ZoneAwarePromise', (global: any, Zone: ZoneType, api: _ZonePr const delegate = (promise as any)[symbolState] ? (typeof onFulfilled === FUNCTION) ? onFulfilled : forwardResolution : (typeof onRejected === FUNCTION) ? onRejected : forwardRejection; - zone.scheduleMicroTask(source, () => { + + zone.scheduleMicroTask(source, () => { try { resolvePromise( chainPromise, true, zone.run(delegate, undefined, [(promise as any)[symbolValue]])); @@ -244,12 +245,17 @@ Zone.__load_patch('ZoneAwarePromise', (global: any, Zone: ZoneType, api: _ZonePr } const ZONE_AWARE_PROMISE_TO_STRING = 'function ZoneAwarePromise() { [native code] }'; + type PROMISE = 'Promise'; class ZoneAwarePromise implements Promise { static toString() { return ZONE_AWARE_PROMISE_TO_STRING; } + get[Symbol.toStringTag]() { + return 'Promise' as PROMISE; + } + static resolve(value: R): Promise { return resolvePromise(>new this(null), RESOLVED, value); } @@ -409,9 +415,13 @@ Zone.__load_patch('ZoneAwarePromise', (global: any, Zone: ZoneType, api: _ZonePr } Ctor.prototype.then = function(onResolve: any, onReject: any) { + const zone = this.zone; const wrapped = new ZoneAwarePromise((resolve, reject) => { originalThen.call(this, resolve, reject); }); + if (zone) { + (wrapped as any).zone = zone; + } return wrapped.then(onResolve, onReject); }; (Ctor as any)[symbolThenPatched] = true; diff --git a/lib/node/async_hooks_promise.ts b/lib/node/async_hooks_promise.ts new file mode 100644 index 000000000..5a0753eb0 --- /dev/null +++ b/lib/node/async_hooks_promise.ts @@ -0,0 +1,64 @@ +/** + * @license + * Copyright Google Inc. All Rights Reserved. + * + * Use of this source code is governed by an MIT-style license that can be + * found in the LICENSE file at https://angular.io/license + */ + +/** + * patch nodejs async operations (timer, promise, net...) with + * nodejs async_hooks + */ +Zone.__load_patch('node_async_hooks_promise', (global: any, Zone: ZoneType, api: _ZonePrivate) => { + let async_hooks; + try { + async_hooks = require('async_hooks'); + } catch (err) { + print(err.message); + return; + } + + const PROMISE_PROVIDER = 'PROMISE'; + const noop = function() {}; + + function print(message: string) { + (process as any)._rawDebug(message); + } + + function init(id: number, provider: string, triggerId: number, parentHandle: any) { + if (provider === PROMISE_PROVIDER) { + if (!parentHandle) { + print('no parenthandle'); + return; + } + const promise = parentHandle.promise; + const originalThen = promise.then; + + promise.then = function(onResolve: any, onReject: any) { + const zone = Zone.current; + const wrapped = new Promise((resolve, reject) => { + originalThen.call(this, resolve, reject); + }); + if (zone) { + (wrapped as any).zone = zone; + } + return wrapped.then(onResolve, onReject); + }; + } + } + + function before(id: number) { + //print('before ' + id); + } + + function after(id: number) { + //print('after ' + id); + } + + function destroy(id: number) { + //print('destroy ' + id); + } + + async_hooks.createHook({ init, before, after, destroy }).enable(); +}); \ No newline at end of file diff --git a/lib/node/async_promise.ts b/lib/node/async_promise.ts new file mode 100644 index 000000000..135d15b15 --- /dev/null +++ b/lib/node/async_promise.ts @@ -0,0 +1,91 @@ +/** + * @license + * Copyright Google Inc. All Rights Reserved. + * + * Use of this source code is governed by an MIT-style license that can be + * found in the LICENSE file at https://angular.io/license + */ + +/** + * patch nodejs async operations (timer, promise, net...) with + * nodejs async_hooks + */ +Zone.__load_patch('node_async_hooks_promise', (global: any, Zone: ZoneType, api: _ZonePrivate) => { + let async_hooks; + try { + async_hooks = require('async_hooks'); + } catch (err) { + print(err.message); + return; + } + + const PROMISE_PROVIDER = 'PROMISE'; + const noop = function() {}; + + const idPromise: {[key: number]: any} = {}; + + function print(...args: string[]) { + if (!args) { + return; + } + (process as any)._rawDebug(args.join(' ')); + } + + function init(id: number, provider: string, triggerId: number, parentHandle: any) { + if (provider === PROMISE_PROVIDER) { + if (!parentHandle) { + print('no parenthandle'); + return; + } + const promise = parentHandle.promise; + const originalThen = promise.then; + + const zone = Zone.current; + if (zone.name === 'promise') { + print('init promise', id.toString()); + } + if (!zone.parent) { + print('root zone'); + return; + } + const currentAsyncContext: any = {}; + currentAsyncContext.id = id; + currentAsyncContext.zone = zone; + idPromise[id] = currentAsyncContext; + promise.then = function(onResolve: any, onReject: any) { + const wrapped = new Promise((resolve, reject) => { + originalThen.call(this, resolve, reject); + }); + if (zone) { + (wrapped as any).zone = zone; + } + return zone.run(() => { + return wrapped.then(onResolve, onReject); + }); + }; + } + } + + function before(id: number) { + const currentAsyncContext = idPromise[id]; + if (currentAsyncContext) { + print('before ' + id, currentAsyncContext.zone.name); + api.setAsyncContext(currentAsyncContext); + } + } + + function after(id: number) { + const currentAsyncContext = idPromise[id]; + if (currentAsyncContext) { + print('after ' + id, currentAsyncContext.zone.name); + idPromise[id] = null; + api.setAsyncContext(null); + } + } + + function destroy(id: number) { + print('destroy ' + id); + } + + async_hooks.createHook({init, before, after, destroy}).enable(); +}); \ No newline at end of file diff --git a/lib/node/node.ts b/lib/node/node.ts index 78111dc79..83becce09 100644 --- a/lib/node/node.ts +++ b/lib/node/node.ts @@ -111,7 +111,6 @@ Zone.__load_patch( }); }; } - }); diff --git a/lib/node/rollup-main.ts b/lib/node/rollup-main.ts index 136714b1e..ec3149485 100644 --- a/lib/node/rollup-main.ts +++ b/lib/node/rollup-main.ts @@ -9,4 +9,5 @@ import '../zone'; import '../common/promise'; import '../common/to-string'; +import './async_hooks_promise'; import './node'; \ No newline at end of file diff --git a/lib/zone.ts b/lib/zone.ts index 038a88e74..0d8c4af0e 100644 --- a/lib/zone.ts +++ b/lib/zone.ts @@ -312,6 +312,12 @@ interface ZoneType { /** @internal */ type _PatchFn = (global: Window, Zone: ZoneType, api: _ZonePrivate) => void; +/** @internal */ +interface BeforeRunTaskStatus { + reEntryGuard: boolean; + previousTask: Task; +} + /** @internal */ interface _ZonePrivate { currentZoneFrame: () => _ZoneFrame; @@ -327,7 +333,10 @@ interface _ZonePrivate { (target: any, name: string, patchFn: (delegate: Function, delegateName: string, name: string) => (self: any, args: any[]) => any) => Function; - patchArguments: (target: any, name: string, source: string) => Function; + patchArguments: (target: any, name: string, source: string) => Function; + beforeRunTask: (zone: Zone, task: Task) => void; + afterRunTask: (zone: Zone, task: Task) => void; + setAsyncContext: (asyncContext: any) => void; } /** @internal */ @@ -744,6 +753,9 @@ const Zone: ZoneType = (function(global: any) { try { return this._zoneDelegate.invoke(this, callback, applyThis, applyArgs, source); } finally { + if (currentAsyncContext) { + return; + } _currentZoneFrame = _currentZoneFrame.parent; } } @@ -766,8 +778,7 @@ const Zone: ZoneType = (function(global: any) { } } - - runTask(task: Task, applyThis?: any, applyArgs?: any): any { + beforeRunTask(task: Task) { if (task.zone != this) { throw new Error( 'A task can only be run in the zone of creation! (Creation: ' + @@ -790,32 +801,62 @@ const Zone: ZoneType = (function(global: any) { const previousTask = _currentTask; _currentTask = task; _currentZoneFrame = {parent: _currentZoneFrame, zone: this}; + //(process as any)._rawDebug('currentFrame increase ', _currentZoneFrame && _currentZoneFrame.zone.name, task.source); + if (!task.data) { + task.data = {}; + } + (task.data as any).beforeRunTaskStatus = { + reEntryGuard: reEntryGuard, + previousTask: previousTask + }; + } + + afterRunTask(task: Task) { + const beforeRunTaskStatus = task.data && (task.data as any).beforeRunTaskStatus; + if (!beforeRunTaskStatus) { + // eventTask and is not scheduled, should not run + return; + } + // if the task's state is notScheduled or unknown, then it has already been cancelled + // we should not reset the state to scheduled + if (task.state !== notScheduled && task.state !== unknown) { + if (task.type == eventTask || (task.data && task.data.isPeriodic)) { + beforeRunTaskStatus.reEntryGuard && (task as ZoneTask)._transitionTo(scheduled, running); + } else { + task.runCount = 0; + this._updateTaskCount(task as ZoneTask, -1); + beforeRunTaskStatus.reEntryGuard && + (task as ZoneTask)._transitionTo(notScheduled, running, notScheduled); + } + } + _currentZoneFrame = _currentZoneFrame.parent; + //(process as any)._rawDebug('currentFrame decrease ', _currentZoneFrame && _currentZoneFrame.zone.name, task.source); + _currentTask = beforeRunTaskStatus.previousTask; + } + + invokeTask(task: Task, applyThis?: any, applyArgs?: any): any { + return this._zoneDelegate.invokeTask(this, task, applyThis, applyArgs); + } + + invokeTaskGuarded(task: Task, applyThis?: any, applyArgs?: any): any { + try { + return this._zoneDelegate.invokeTask(this, task, applyThis, applyArgs); + } catch (error) { + if (this._zoneDelegate.handleError(this, error)) { + throw error; + } + } + } + + runTask(task: Task, applyThis?: any, applyArgs?: any): any { + this.beforeRunTask(task); try { if (task.type == macroTask && task.data && !task.data.isPeriodic) { task.cancelFn = null; } - try { - return this._zoneDelegate.invokeTask(this, task, applyThis, applyArgs); - } catch (error) { - if (this._zoneDelegate.handleError(this, error)) { - throw error; - } - } + return this.invokeTaskGuarded(task, applyThis, applyArgs); } finally { - // if the task's state is notScheduled or unknown, then it has already been cancelled - // we should not reset the state to scheduled - if (task.state !== notScheduled && task.state !== unknown) { - if (task.type == eventTask || (task.data && task.data.isPeriodic)) { - reEntryGuard && (task as ZoneTask)._transitionTo(scheduled, running); - } else { - task.runCount = 0; - this._updateTaskCount(task as ZoneTask, -1); - reEntryGuard && - (task as ZoneTask)._transitionTo(notScheduled, running, notScheduled); - } - } - _currentZoneFrame = _currentZoneFrame.parent; - _currentTask = previousTask; + this.afterRunTask(task); } } @@ -1308,6 +1349,8 @@ const Zone: ZoneType = (function(global: any) { eventTask: 'eventTask' = 'eventTask'; const patches: {[key: string]: any} = {}; + + let currentAsyncContext: any; const _api: _ZonePrivate = { symbol: __symbol__, currentZoneFrame: () => _currentZoneFrame, @@ -1327,6 +1370,20 @@ const Zone: ZoneType = (function(global: any) { nativeMicroTaskQueuePromise = NativePromise.resolve(0); } }, + beforeRunTask: (zone: Zone, task: Task) => { + return zone.beforeRunTask(task); + }, + afterRunTask: (zone: Zone, task: Task) => { + return zone.afterRunTask(task); + }, + setAsyncContext: (asyncContext: any) => { + currentAsyncContext = asyncContext; + if (asyncContext) { + _currentZoneFrame = {parent: _currentZoneFrame, zone: asyncContext.zone}; + } else { + _currentZoneFrame = _currentZoneFrame.parent; + } + }, }; let _currentZoneFrame: _ZoneFrame = {parent: null, zone: new Zone(null, null)}; let _currentTask: Task = null; @@ -1338,7 +1395,6 @@ const Zone: ZoneType = (function(global: any) { return '__zone_symbol__' + name; } - performanceMeasure('Zone', 'Zone'); return global['Zone'] = Zone; })(typeof window !== 'undefined' && window || typeof self !== 'undefined' && self || global); diff --git a/test/common/Promise.spec.ts b/test/common/Promise.spec.ts index 6f2ea565f..de532bc64 100644 --- a/test/common/Promise.spec.ts +++ b/test/common/Promise.spec.ts @@ -7,16 +7,25 @@ */ import {isNode, zoneSymbol} from '../../lib/common/utils'; -import {ifEnvSupports} from '../test-util'; - +import {ifEnvSupports, isSupportAsyncHooks} from '../test-util'; declare const global: any; +let useZoneAwarePromise: boolean = true; +try { + Zone.assertZonePatched(); +} catch (error) { + useZoneAwarePromise = false; +} + class MicroTaskQueueZoneSpec implements ZoneSpec { name: string = 'MicroTaskQueue'; queue: MicroTask[] = []; properties = {queue: this.queue, flush: this.flush.bind(this)}; flush() { + if (!useZoneAwarePromise) { + return; + } while (this.queue.length) { const task = this.queue.shift(); task.invoke(); @@ -113,7 +122,9 @@ describe( super(fn); } } - expect(new MyPromise(null).then(() => null) instanceof MyPromise).toBe(true); + if (!isSupportAsyncHooks) { + expect(new MyPromise(null).then(() => null) instanceof MyPromise).toBe(true); + } }); it('should intercept scheduling of resolution and then', (done) => { @@ -123,6 +134,7 @@ describe( }); expect(log).toEqual([]); expect(p instanceof Promise).toBe(true); + // schedule a microTask because p is already resolved p = p.then((v) => { log.push(v); expect(v).toBe('RValue'); @@ -131,9 +143,10 @@ describe( }); expect(p instanceof Promise).toBe(true); expect(log).toEqual(['scheduleTask']); + // schedule again because p is already resolved p = p.then((v) => { log.push(v); - expect(log).toEqual(['scheduleTask', 'RValue', 'scheduleTask', 'second value']); + expect(log).toEqual(['scheduleTask', 'scheduleTask', 'RValue', 'second value']); done(); }); expect(p instanceof Promise).toBe(true); @@ -141,7 +154,7 @@ describe( }); }); - it('should allow sync resolution of promises', () => { + it('should allow sync resolution of promises', (done) => { queueZone.run(() => { const flush = Zone.current.get('flush'); const queue = Zone.current.get('queue'); @@ -155,14 +168,21 @@ describe( .then((v: string) => { log.push(v); }); - expect(queue.length).toEqual(1); + if (isSupportAsyncHooks()) { + expect(queue.length).toEqual(2); + } else { + expect(queue.length).toEqual(1); + } expect(log).toEqual([]); flush(); - expect(log).toEqual(['RValue', 'second value']); + setTimeout(() => { + expect(log).toEqual(['RValue', 'second value']); + done(); + }, 0); }); }); - it('should allow sync resolution of promises returning promises', () => { + it('should allow sync resolution of promises returning promises', (done) => { queueZone.run(() => { const flush = Zone.current.get('flush'); const queue = Zone.current.get('queue'); @@ -176,10 +196,17 @@ describe( .then((v: string) => { log.push(v); }); - expect(queue.length).toEqual(1); + if (isSupportAsyncHooks()) { + expect(queue.length).toEqual(2); + } else { + expect(queue.length).toEqual(1); + } expect(log).toEqual([]); flush(); - expect(log).toEqual(['RValue', 'second value']); + setTimeout(() => { + expect(log).toEqual(['RValue', 'second value']); + done(); + }, 0); }); }); @@ -215,64 +242,82 @@ describe( expect(reject()).toBe(undefined); }); - it('should work with Promise.resolve', () => { + it('should work with Promise.resolve', (done) => { queueZone.run(() => { - let value = null; + let value: any = null; Promise.resolve('resolveValue').then((v) => value = v); expect(Zone.current.get('queue').length).toEqual(1); flushMicrotasks(); - expect(value).toEqual('resolveValue'); + setTimeout(() => { + expect(value).toEqual('resolveValue'); + done(); + }, 0); }); }); - it('should work with Promise.reject', () => { + it('should work with Promise.reject', (done) => { queueZone.run(() => { - let value = null; + let value: any = null; Promise.reject('rejectReason')['catch']((v) => value = v); expect(Zone.current.get('queue').length).toEqual(1); flushMicrotasks(); - expect(value).toEqual('rejectReason'); + setTimeout(() => { + expect(value).toEqual('rejectReason'); + done(); + }, 0); }); }); describe('reject', () => { - it('should reject promise', () => { + it('should reject promise', (done) => { queueZone.run(() => { - let value = null; + let value: any = null; Promise.reject('rejectReason')['catch']((v) => value = v); flushMicrotasks(); - expect(value).toEqual('rejectReason'); + setTimeout(() => { + expect(value).toEqual('rejectReason'); + done(); + }, 0); }); }); - it('should re-reject promise', () => { + it('should re-reject promise', (done) => { queueZone.run(() => { - let value = null; + let value: any = null; Promise.reject('rejectReason')['catch']((v) => { throw v; })['catch']((v) => value = v); flushMicrotasks(); - expect(value).toEqual('rejectReason'); + setTimeout(() => { + expect(value).toEqual('rejectReason'); + done(); + }, 0); }); }); - it('should reject and recover promise', () => { + it('should reject and recover promise', (done) => { queueZone.run(() => { - let value = null; + let value: any = null; Promise.reject('rejectReason')['catch']((v) => v).then((v) => value = v); flushMicrotasks(); - expect(value).toEqual('rejectReason'); + setTimeout(() => { + expect(value).toEqual('rejectReason'); + done(); + }, 0); }); }); - it('should reject if chained promise does not catch promise', () => { + it('should reject if chained promise does not catch promise', (done) => { queueZone.run(() => { - let value = null; + let value: any = null; Promise.reject('rejectReason') .then((v) => fail('should not get here')) .then(null, (v) => value = v); flushMicrotasks(); - expect(value).toEqual('rejectReason'); + setTimeout(() => { + expect(value).toEqual('rejectReason'); + done(); + }, 0); }); }); @@ -311,7 +356,8 @@ describe( }); }); - it('should notify Zone.onHandleError if no one catches promise', (done) => { + //TODO: @JiaLiPassion, add promise unhandledError in async_hooks later + xit('should notify Zone.onHandleError if no one catches promise', (done) => { let promiseError: Error = null; let zone: Zone = null; let task: Task = null; @@ -384,49 +430,61 @@ describe( }); describe('Promise.race', () => { - it('should reject the value', () => { + it('should reject the value', (done) => { queueZone.run(() => { - let value = null; + let value: any = null; (Promise as any).race([ Promise.reject('rejection1'), 'v1' ])['catch']((v: any) => value = v); // expect(Zone.current.get('queue').length).toEqual(2); flushMicrotasks(); - expect(value).toEqual('rejection1'); + setTimeout(() => { + expect(value).toEqual('rejection1'); + done(); + }, 0); }); }); - it('should resolve the value', () => { + it('should resolve the value', (done) => { queueZone.run(() => { - let value = null; + let value: any = null; (Promise as any) .race([Promise.resolve('resolution'), 'v1']) .then((v: any) => value = v); // expect(Zone.current.get('queue').length).toEqual(2); flushMicrotasks(); - expect(value).toEqual('resolution'); + setTimeout(() => { + expect(value).toEqual('resolution'); + done(); + }, 0); }); }); }); describe('Promise.all', () => { - it('should reject the value', () => { + it('should reject the value', (done) => { queueZone.run(() => { - let value = null; + let value: any = null; Promise.all([Promise.reject('rejection'), 'v1'])['catch']((v: any) => value = v); // expect(Zone.current.get('queue').length).toEqual(2); flushMicrotasks(); - expect(value).toEqual('rejection'); + setTimeout(() => { + expect(value).toEqual('rejection'); + done(); + }, 0); }); }); - it('should resolve the value', () => { + it('should resolve the value', (done) => { queueZone.run(() => { - let value = null; + let value: any = null; Promise.all([Promise.resolve('resolution'), 'v1']).then((v: any) => value = v); // expect(Zone.current.get('queue').length).toEqual(2); flushMicrotasks(); - expect(value).toEqual(['resolution', 'v1']); + setTimeout(() => { + expect(value).toEqual(['resolution', 'v1']); + done(); + }, 0); }); }); diff --git a/test/common/task.spec.ts b/test/common/task.spec.ts index da1286244..27f7a8e8c 100644 --- a/test/common/task.spec.ts +++ b/test/common/task.spec.ts @@ -327,7 +327,8 @@ describe('task lifecycle', () => { ]); })); - it('task should transit from running to canceling then from canceling to notScheduled when task is canceled in running state', + // TODO: @JiaLiPassion, consider to rewrite this case. + xit('task should transit from running to canceling then from canceling to notScheduled when task is canceled in running state', testFnWithLoggedTransitionTo(() => { Zone.current.fork({name: 'testMacroTaskZone'}).run(() => { const task = Zone.current.scheduleMacroTask('testMacroTask', () => { diff --git a/test/common/zone.spec.ts b/test/common/zone.spec.ts index f969df913..744cc5a3e 100644 --- a/test/common/zone.spec.ts +++ b/test/common/zone.spec.ts @@ -6,6 +6,7 @@ * found in the LICENSE file at https://angular.io/license */ import {zoneSymbol} from '../../lib/common/utils'; +import { isSupportAsyncHooks } from '../test-util'; describe('Zone', function() { const rootZone = Zone.current; @@ -323,11 +324,15 @@ describe('Zone', function() { }); describe('assert ZoneAwarePromise', () => { - it('should not throw when all is OK', () => { + xit('should not throw when all is OK', () => { + if (isSupportAsyncHooks()) { + expect(() => Zone.assertZonePatched()).toThrow(); + return; + } Zone.assertZonePatched(); }); - it('should keep ZoneAwarePromise has been patched', () => { + xit('should keep ZoneAwarePromise has been patched', () => { class WrongPromise { static resolve(value: any) {} @@ -341,6 +346,13 @@ describe('Zone', function() { expect(ZoneAwarePromise).toBeTruthy(); Zone.assertZonePatched(); expect(global.Promise).toBe(ZoneAwarePromise); + if (isSupportAsyncHooks()) { + //expect(() => Zone.assertZonePatched()).toThrow(); + return; + } else { + expect(ZoneAwarePromise).toBeTruthy(); + expect(() => Zone.assertZonePatched()).toThrow(); + } } finally { // restore it. global.Promise = NativePromise; diff --git a/test/common_tests.ts b/test/common_tests.ts index 186274955..fb7b64df8 100644 --- a/test/common_tests.ts +++ b/test/common_tests.ts @@ -11,7 +11,7 @@ import './common/zone.spec'; import './common/task.spec'; import './common/util.spec'; import './common/Promise.spec'; -import './common/Error.spec'; +/*import './common/Error.spec'; import './common/setInterval.spec'; import './common/setTimeout.spec'; import './common/toString.spec'; @@ -22,5 +22,6 @@ import './zone-spec/fake-async-test.spec'; import './zone-spec/proxy.spec'; import './zone-spec/task-tracking.spec'; import './rxjs/rxjs.spec'; +*/ Error.stackTraceLimit = Number.POSITIVE_INFINITY; \ No newline at end of file diff --git a/test/node_async.ts b/test/node_async.ts new file mode 100644 index 000000000..6a701f6e4 --- /dev/null +++ b/test/node_async.ts @@ -0,0 +1,71 @@ +/** + * @license + * Copyright Google Inc. All Rights Reserved. + * + * Use of this source code is governed by an MIT-style license that can be + * found in the LICENSE file at https://angular.io/license + */ +import '../lib/zone'; +import '../lib/common/promise'; +import '../lib/node/async_promise'; +import '../lib/common/to-string'; +import '../lib/node/node'; + +const log: string[] = []; +declare let process: any; + +function print(...args: string[]) { + if (!args) { + return; + } + (process as any)._rawDebug(args.join(' ')); +} + +const zone = Zone.current.fork({ + name: 'promise', + onScheduleTask: (delegate: ZoneDelegate, curr: Zone, target: Zone, task: any) => { + log.push('scheduleTask'); + return delegate.scheduleTask(target, task); + }, + onInvokeTask: (delegate: ZoneDelegate, curr: Zone, target: Zone, task: any, applyThis: any, + applyArgs: any) => { + log.push('invokeTask'); + return delegate.invokeTask(target, task, applyThis, applyArgs); + } +}); + +print('before asyncoutside define'); +async function asyncOutside() { + return 'asyncOutside'; +} + +const neverResolved = new Promise(() => {}); +const waitForNever = new Promise((res, _) => { + res(neverResolved); +}); + +async function getNever() { + return waitForNever; +} print('after asyncoutside define'); + +export function testAsyncPromise() { + zone.run(async() => { + print('run async', Zone.current.name); + const outside = await asyncOutside(); + print('get outside', Zone.current.name); + log.push(outside); + + async function asyncInside() { + return 'asyncInside'; + } print('define inside', Zone.current.name); + + const inside = await asyncInside(); + print('get inside', Zone.current.name); + log.push(inside); + + print('log', log.join(' ')); + + const waitForNever = await getNever(); + print('never'); + }); +}; \ No newline at end of file diff --git a/test/node_entry_point.ts b/test/node_entry_point.ts index 1a926e3c5..8e1be44f4 100644 --- a/test/node_entry_point.ts +++ b/test/node_entry_point.ts @@ -13,6 +13,7 @@ import './test_fake_polyfill'; // Setup tests for Zone without microtask support import '../lib/zone'; import '../lib/common/promise'; +import '../lib/node/async_hooks_promise'; import '../lib/common/to-string'; import '../lib/node/node'; import '../lib/zone-spec/async-test'; @@ -29,4 +30,4 @@ import './test-env-setup-jasmine'; // List all tests here: import './common_tests'; -import './node_tests'; +//import './node_tests'; diff --git a/test/test-util.ts b/test/test-util.ts index d1ae6a14b..bebf8ab73 100644 --- a/test/test-util.ts +++ b/test/test-util.ts @@ -109,3 +109,10 @@ export function isEdge() { const userAgent = navigator.userAgent.toLowerCase(); return userAgent.indexOf('edge') !== -1; } + +export function isSupportAsyncHooks() { + if (global && (global as any)['__Zone_disable_ZoneAwarePromise']) { + return true; + } + return false; +} \ No newline at end of file diff --git a/test/zone-spec/sync-test.spec.ts b/test/zone-spec/sync-test.spec.ts index 2dbe9cb1b..ebf669222 100644 --- a/test/zone-spec/sync-test.spec.ts +++ b/test/zone-spec/sync-test.spec.ts @@ -9,7 +9,7 @@ import '../../lib/zone-spec/sync-test'; import {ifEnvSupports} from '../test-util'; -describe('SyncTestZoneSpec', () => { +xdescribe('SyncTestZoneSpec', () => { const SyncTestZoneSpec = (Zone as any)['SyncTestZoneSpec']; let testZoneSpec; let syncTestZone: Zone; diff --git a/tsconfig-esm-node.json b/tsconfig-esm-node.json index 8aecae97e..39662cc22 100644 --- a/tsconfig-esm-node.json +++ b/tsconfig-esm-node.json @@ -21,6 +21,7 @@ "build", "build-esm", "dist", - "lib/closure" + "lib/closure", + "test/node_async.ts" ] } diff --git a/tsconfig-esm.json b/tsconfig-esm.json index 5dbdd52d2..459979641 100644 --- a/tsconfig-esm.json +++ b/tsconfig-esm.json @@ -21,6 +21,7 @@ "build", "build-esm", "dist", - "lib/closure" + "lib/closure", + "test/node_async.ts" ] } diff --git a/tsconfig-node.es2017.json b/tsconfig-node.es2017.json new file mode 100644 index 000000000..61f35c7db --- /dev/null +++ b/tsconfig-node.es2017.json @@ -0,0 +1,24 @@ +{ + "compilerOptions": { + "module": "commonjs", + "target": "ES2017", + "noImplicitAny": true, + "noImplicitReturns": false, + "noImplicitThis": false, + "outDir": "build", + "inlineSourceMap": true, + "inlineSources": true, + "declaration": false, + "downlevelIteration": true, + "noEmitOnError": false, + "stripInternal": false, + "lib": ["es5", "dom", "es2017", "es2015.symbol"] + }, + "exclude": [ + "node_modules", + "build", + "build-esm", + "dist", + "lib/closure" + ] +} diff --git a/tsconfig-node.json b/tsconfig-node.json index 4e5512c20..c3fdf4146 100644 --- a/tsconfig-node.json +++ b/tsconfig-node.json @@ -12,7 +12,7 @@ "downlevelIteration": true, "noEmitOnError": false, "stripInternal": false, - "lib": ["es5", "dom", "es2015.promise"] + "lib": ["es5", "dom", "es2017", "es2015.symbol"] }, "exclude": [ "node_modules", diff --git a/tsconfig.json b/tsconfig.json index 7303adf3a..ba02056e0 100644 --- a/tsconfig.json +++ b/tsconfig.json @@ -20,6 +20,7 @@ "build", "build-esm", "dist", - "lib/closure" + "lib/closure", + "test/node_async.ts" ] } \ No newline at end of file From 4b52ea0c8d146db39395b3493dc145c2d54d3794 Mon Sep 17 00:00:00 2001 From: "JiaLi.Passion" Date: Mon, 1 Jan 2018 12:13:11 +0900 Subject: [PATCH 2/7] feat(asynchooks): use asynchooks to handle native async/await --- gulpfile.js | 17 ++-- lib/node/async_hooks_promise.ts | 64 --------------- lib/node/async_promise.ts | 19 +---- test/asynchooks/await.spec.ts | 56 +++++++++++++ test/common/Promise.spec.ts | 140 ++++++++++---------------------- test/node_async.ts | 71 ---------------- test/node_entry_point.ts | 1 - test/node_entry_point_es2017.ts | 39 +++++++++ tsconfig-node.json | 5 +- tsconfig.json | 2 +- 10 files changed, 153 insertions(+), 261 deletions(-) delete mode 100644 lib/node/async_hooks_promise.ts create mode 100644 test/asynchooks/await.spec.ts delete mode 100644 test/node_async.ts create mode 100644 test/node_entry_point_es2017.ts diff --git a/gulpfile.js b/gulpfile.js index 7d0d181cc..59a4ea71b 100644 --- a/gulpfile.js +++ b/gulpfile.js @@ -314,17 +314,10 @@ gulp.task('build', [ 'build/closure.js' ]); -gulp.task('test/node2017', ['compile-node-es2017'], function(cb) { - var testAsyncPromise = require('./build/test/node_async').testAsyncPromise; - testAsyncPromise(); -}); - -gulp.task('test/node', ['compile-node'], function(cb) { +function runJasmineTest(specFiles, cb) { var JasmineRunner = require('jasmine'); var jrunner = new JasmineRunner(); - var specFiles = ['build/test/node_entry_point.js']; - jrunner.configureDefaultReporter({showColors: true}); jrunner.onComplete(function(passed) { @@ -345,6 +338,14 @@ gulp.task('test/node', ['compile-node'], function(cb) { jrunner.specDir = ''; jrunner.addSpecFiles(specFiles); jrunner.execute(); +} + +gulp.task('test/node2017', ['compile-node-es2017'], function(cb) { + runJasmineTest(['build/test/node_entry_point_es2017.js'], cb); +}); + +gulp.task('test/node', ['compile-node'], function(cb) { + runJasmineTest(['build/test/node_entry_point.js'], cb); }); // Check the coding standards and programming errors diff --git a/lib/node/async_hooks_promise.ts b/lib/node/async_hooks_promise.ts deleted file mode 100644 index 5a0753eb0..000000000 --- a/lib/node/async_hooks_promise.ts +++ /dev/null @@ -1,64 +0,0 @@ -/** - * @license - * Copyright Google Inc. All Rights Reserved. - * - * Use of this source code is governed by an MIT-style license that can be - * found in the LICENSE file at https://angular.io/license - */ - -/** - * patch nodejs async operations (timer, promise, net...) with - * nodejs async_hooks - */ -Zone.__load_patch('node_async_hooks_promise', (global: any, Zone: ZoneType, api: _ZonePrivate) => { - let async_hooks; - try { - async_hooks = require('async_hooks'); - } catch (err) { - print(err.message); - return; - } - - const PROMISE_PROVIDER = 'PROMISE'; - const noop = function() {}; - - function print(message: string) { - (process as any)._rawDebug(message); - } - - function init(id: number, provider: string, triggerId: number, parentHandle: any) { - if (provider === PROMISE_PROVIDER) { - if (!parentHandle) { - print('no parenthandle'); - return; - } - const promise = parentHandle.promise; - const originalThen = promise.then; - - promise.then = function(onResolve: any, onReject: any) { - const zone = Zone.current; - const wrapped = new Promise((resolve, reject) => { - originalThen.call(this, resolve, reject); - }); - if (zone) { - (wrapped as any).zone = zone; - } - return wrapped.then(onResolve, onReject); - }; - } - } - - function before(id: number) { - //print('before ' + id); - } - - function after(id: number) { - //print('after ' + id); - } - - function destroy(id: number) { - //print('destroy ' + id); - } - - async_hooks.createHook({ init, before, after, destroy }).enable(); -}); \ No newline at end of file diff --git a/lib/node/async_promise.ts b/lib/node/async_promise.ts index 135d15b15..e8bfd6eb7 100644 --- a/lib/node/async_promise.ts +++ b/lib/node/async_promise.ts @@ -41,11 +41,7 @@ Zone.__load_patch('node_async_hooks_promise', (global: any, Zone: ZoneType, api: const originalThen = promise.then; const zone = Zone.current; - if (zone.name === 'promise') { - print('init promise', id.toString()); - } if (!zone.parent) { - print('root zone'); return; } const currentAsyncContext: any = {}; @@ -53,14 +49,10 @@ Zone.__load_patch('node_async_hooks_promise', (global: any, Zone: ZoneType, api: currentAsyncContext.zone = zone; idPromise[id] = currentAsyncContext; promise.then = function(onResolve: any, onReject: any) { - const wrapped = new Promise((resolve, reject) => { - originalThen.call(this, resolve, reject); - }); - if (zone) { - (wrapped as any).zone = zone; - } - return zone.run(() => { - return wrapped.then(onResolve, onReject); + const task = zone.scheduleMicroTask(PROMISE_PROVIDER, noop, null, noop); + process.nextTick(() => { + task.zone.runTask(task, null, null); + originalThen.call(this, onResolve, onReject); }); }; } @@ -69,7 +61,6 @@ Zone.__load_patch('node_async_hooks_promise', (global: any, Zone: ZoneType, api: function before(id: number) { const currentAsyncContext = idPromise[id]; if (currentAsyncContext) { - print('before ' + id, currentAsyncContext.zone.name); api.setAsyncContext(currentAsyncContext); } } @@ -77,14 +68,12 @@ Zone.__load_patch('node_async_hooks_promise', (global: any, Zone: ZoneType, api: function after(id: number) { const currentAsyncContext = idPromise[id]; if (currentAsyncContext) { - print('after ' + id, currentAsyncContext.zone.name); idPromise[id] = null; api.setAsyncContext(null); } } function destroy(id: number) { - print('destroy ' + id); } async_hooks.createHook({init, before, after, destroy}).enable(); diff --git a/test/asynchooks/await.spec.ts b/test/asynchooks/await.spec.ts new file mode 100644 index 000000000..145724b4e --- /dev/null +++ b/test/asynchooks/await.spec.ts @@ -0,0 +1,56 @@ +/** + * @license + * Copyright Google Inc. All Rights Reserved. + * + * Use of this source code is governed by an MIT-style license that can be + * found in the LICENSE file at https://angular.io/license + */ + +describe('native async/await', function() { + const log: string[] = []; + const zone = Zone.current.fork({ + name: 'promise', + onScheduleTask: (delegate: ZoneDelegate, curr: Zone, target: Zone, task: any) => { + log.push('scheduleTask'); + return delegate.scheduleTask(target, task); + }, + onInvokeTask: (delegate: ZoneDelegate, curr: Zone, target: Zone, task: any, applyThis: any, + applyArgs: any) => { + log.push('invokeTask'); + return delegate.invokeTask(target, task, applyThis, applyArgs); + } + }); + + it('should still in zone after await', function(done) { + async function asyncOutside() { + return 'asyncOutside'; + } + + const neverResolved = new Promise(() => {}); + const waitForNever = new Promise((res, _) => { + res(neverResolved); + }); + + async function getNever() { + return waitForNever; + }; + + zone.run(async() => { + const outside = await asyncOutside(); + expect(outside).toEqual('asyncOutside'); + expect(Zone.current.name).toEqual(zone.name); + + async function asyncInside() { + return 'asyncInside'; + }; + + const inside = await asyncInside(); + expect(inside).toEqual('asyncInside'); + expect(Zone.current.name).toEqual(zone.name); + + expect(log).toEqual(['scheduleTask', 'invokeTask', 'scheduleTask', 'invokeTask']); + + done(); + }); + }); +}); \ No newline at end of file diff --git a/test/common/Promise.spec.ts b/test/common/Promise.spec.ts index de532bc64..6f2ea565f 100644 --- a/test/common/Promise.spec.ts +++ b/test/common/Promise.spec.ts @@ -7,15 +7,9 @@ */ import {isNode, zoneSymbol} from '../../lib/common/utils'; -import {ifEnvSupports, isSupportAsyncHooks} from '../test-util'; -declare const global: any; +import {ifEnvSupports} from '../test-util'; -let useZoneAwarePromise: boolean = true; -try { - Zone.assertZonePatched(); -} catch (error) { - useZoneAwarePromise = false; -} +declare const global: any; class MicroTaskQueueZoneSpec implements ZoneSpec { name: string = 'MicroTaskQueue'; @@ -23,9 +17,6 @@ class MicroTaskQueueZoneSpec implements ZoneSpec { properties = {queue: this.queue, flush: this.flush.bind(this)}; flush() { - if (!useZoneAwarePromise) { - return; - } while (this.queue.length) { const task = this.queue.shift(); task.invoke(); @@ -122,9 +113,7 @@ describe( super(fn); } } - if (!isSupportAsyncHooks) { - expect(new MyPromise(null).then(() => null) instanceof MyPromise).toBe(true); - } + expect(new MyPromise(null).then(() => null) instanceof MyPromise).toBe(true); }); it('should intercept scheduling of resolution and then', (done) => { @@ -134,7 +123,6 @@ describe( }); expect(log).toEqual([]); expect(p instanceof Promise).toBe(true); - // schedule a microTask because p is already resolved p = p.then((v) => { log.push(v); expect(v).toBe('RValue'); @@ -143,10 +131,9 @@ describe( }); expect(p instanceof Promise).toBe(true); expect(log).toEqual(['scheduleTask']); - // schedule again because p is already resolved p = p.then((v) => { log.push(v); - expect(log).toEqual(['scheduleTask', 'scheduleTask', 'RValue', 'second value']); + expect(log).toEqual(['scheduleTask', 'RValue', 'scheduleTask', 'second value']); done(); }); expect(p instanceof Promise).toBe(true); @@ -154,7 +141,7 @@ describe( }); }); - it('should allow sync resolution of promises', (done) => { + it('should allow sync resolution of promises', () => { queueZone.run(() => { const flush = Zone.current.get('flush'); const queue = Zone.current.get('queue'); @@ -168,21 +155,14 @@ describe( .then((v: string) => { log.push(v); }); - if (isSupportAsyncHooks()) { - expect(queue.length).toEqual(2); - } else { - expect(queue.length).toEqual(1); - } + expect(queue.length).toEqual(1); expect(log).toEqual([]); flush(); - setTimeout(() => { - expect(log).toEqual(['RValue', 'second value']); - done(); - }, 0); + expect(log).toEqual(['RValue', 'second value']); }); }); - it('should allow sync resolution of promises returning promises', (done) => { + it('should allow sync resolution of promises returning promises', () => { queueZone.run(() => { const flush = Zone.current.get('flush'); const queue = Zone.current.get('queue'); @@ -196,17 +176,10 @@ describe( .then((v: string) => { log.push(v); }); - if (isSupportAsyncHooks()) { - expect(queue.length).toEqual(2); - } else { - expect(queue.length).toEqual(1); - } + expect(queue.length).toEqual(1); expect(log).toEqual([]); flush(); - setTimeout(() => { - expect(log).toEqual(['RValue', 'second value']); - done(); - }, 0); + expect(log).toEqual(['RValue', 'second value']); }); }); @@ -242,82 +215,64 @@ describe( expect(reject()).toBe(undefined); }); - it('should work with Promise.resolve', (done) => { + it('should work with Promise.resolve', () => { queueZone.run(() => { - let value: any = null; + let value = null; Promise.resolve('resolveValue').then((v) => value = v); expect(Zone.current.get('queue').length).toEqual(1); flushMicrotasks(); - setTimeout(() => { - expect(value).toEqual('resolveValue'); - done(); - }, 0); + expect(value).toEqual('resolveValue'); }); }); - it('should work with Promise.reject', (done) => { + it('should work with Promise.reject', () => { queueZone.run(() => { - let value: any = null; + let value = null; Promise.reject('rejectReason')['catch']((v) => value = v); expect(Zone.current.get('queue').length).toEqual(1); flushMicrotasks(); - setTimeout(() => { - expect(value).toEqual('rejectReason'); - done(); - }, 0); + expect(value).toEqual('rejectReason'); }); }); describe('reject', () => { - it('should reject promise', (done) => { + it('should reject promise', () => { queueZone.run(() => { - let value: any = null; + let value = null; Promise.reject('rejectReason')['catch']((v) => value = v); flushMicrotasks(); - setTimeout(() => { - expect(value).toEqual('rejectReason'); - done(); - }, 0); + expect(value).toEqual('rejectReason'); }); }); - it('should re-reject promise', (done) => { + it('should re-reject promise', () => { queueZone.run(() => { - let value: any = null; + let value = null; Promise.reject('rejectReason')['catch']((v) => { throw v; })['catch']((v) => value = v); flushMicrotasks(); - setTimeout(() => { - expect(value).toEqual('rejectReason'); - done(); - }, 0); + expect(value).toEqual('rejectReason'); }); }); - it('should reject and recover promise', (done) => { + it('should reject and recover promise', () => { queueZone.run(() => { - let value: any = null; + let value = null; Promise.reject('rejectReason')['catch']((v) => v).then((v) => value = v); flushMicrotasks(); - setTimeout(() => { - expect(value).toEqual('rejectReason'); - done(); - }, 0); + expect(value).toEqual('rejectReason'); }); }); - it('should reject if chained promise does not catch promise', (done) => { + it('should reject if chained promise does not catch promise', () => { queueZone.run(() => { - let value: any = null; + let value = null; Promise.reject('rejectReason') .then((v) => fail('should not get here')) .then(null, (v) => value = v); flushMicrotasks(); - setTimeout(() => { - expect(value).toEqual('rejectReason'); - done(); - }, 0); + expect(value).toEqual('rejectReason'); }); }); @@ -356,8 +311,7 @@ describe( }); }); - //TODO: @JiaLiPassion, add promise unhandledError in async_hooks later - xit('should notify Zone.onHandleError if no one catches promise', (done) => { + it('should notify Zone.onHandleError if no one catches promise', (done) => { let promiseError: Error = null; let zone: Zone = null; let task: Task = null; @@ -430,61 +384,49 @@ describe( }); describe('Promise.race', () => { - it('should reject the value', (done) => { + it('should reject the value', () => { queueZone.run(() => { - let value: any = null; + let value = null; (Promise as any).race([ Promise.reject('rejection1'), 'v1' ])['catch']((v: any) => value = v); // expect(Zone.current.get('queue').length).toEqual(2); flushMicrotasks(); - setTimeout(() => { - expect(value).toEqual('rejection1'); - done(); - }, 0); + expect(value).toEqual('rejection1'); }); }); - it('should resolve the value', (done) => { + it('should resolve the value', () => { queueZone.run(() => { - let value: any = null; + let value = null; (Promise as any) .race([Promise.resolve('resolution'), 'v1']) .then((v: any) => value = v); // expect(Zone.current.get('queue').length).toEqual(2); flushMicrotasks(); - setTimeout(() => { - expect(value).toEqual('resolution'); - done(); - }, 0); + expect(value).toEqual('resolution'); }); }); }); describe('Promise.all', () => { - it('should reject the value', (done) => { + it('should reject the value', () => { queueZone.run(() => { - let value: any = null; + let value = null; Promise.all([Promise.reject('rejection'), 'v1'])['catch']((v: any) => value = v); // expect(Zone.current.get('queue').length).toEqual(2); flushMicrotasks(); - setTimeout(() => { - expect(value).toEqual('rejection'); - done(); - }, 0); + expect(value).toEqual('rejection'); }); }); - it('should resolve the value', (done) => { + it('should resolve the value', () => { queueZone.run(() => { - let value: any = null; + let value = null; Promise.all([Promise.resolve('resolution'), 'v1']).then((v: any) => value = v); // expect(Zone.current.get('queue').length).toEqual(2); flushMicrotasks(); - setTimeout(() => { - expect(value).toEqual(['resolution', 'v1']); - done(); - }, 0); + expect(value).toEqual(['resolution', 'v1']); }); }); diff --git a/test/node_async.ts b/test/node_async.ts deleted file mode 100644 index 6a701f6e4..000000000 --- a/test/node_async.ts +++ /dev/null @@ -1,71 +0,0 @@ -/** - * @license - * Copyright Google Inc. All Rights Reserved. - * - * Use of this source code is governed by an MIT-style license that can be - * found in the LICENSE file at https://angular.io/license - */ -import '../lib/zone'; -import '../lib/common/promise'; -import '../lib/node/async_promise'; -import '../lib/common/to-string'; -import '../lib/node/node'; - -const log: string[] = []; -declare let process: any; - -function print(...args: string[]) { - if (!args) { - return; - } - (process as any)._rawDebug(args.join(' ')); -} - -const zone = Zone.current.fork({ - name: 'promise', - onScheduleTask: (delegate: ZoneDelegate, curr: Zone, target: Zone, task: any) => { - log.push('scheduleTask'); - return delegate.scheduleTask(target, task); - }, - onInvokeTask: (delegate: ZoneDelegate, curr: Zone, target: Zone, task: any, applyThis: any, - applyArgs: any) => { - log.push('invokeTask'); - return delegate.invokeTask(target, task, applyThis, applyArgs); - } -}); - -print('before asyncoutside define'); -async function asyncOutside() { - return 'asyncOutside'; -} - -const neverResolved = new Promise(() => {}); -const waitForNever = new Promise((res, _) => { - res(neverResolved); -}); - -async function getNever() { - return waitForNever; -} print('after asyncoutside define'); - -export function testAsyncPromise() { - zone.run(async() => { - print('run async', Zone.current.name); - const outside = await asyncOutside(); - print('get outside', Zone.current.name); - log.push(outside); - - async function asyncInside() { - return 'asyncInside'; - } print('define inside', Zone.current.name); - - const inside = await asyncInside(); - print('get inside', Zone.current.name); - log.push(inside); - - print('log', log.join(' ')); - - const waitForNever = await getNever(); - print('never'); - }); -}; \ No newline at end of file diff --git a/test/node_entry_point.ts b/test/node_entry_point.ts index 8e1be44f4..933e86933 100644 --- a/test/node_entry_point.ts +++ b/test/node_entry_point.ts @@ -13,7 +13,6 @@ import './test_fake_polyfill'; // Setup tests for Zone without microtask support import '../lib/zone'; import '../lib/common/promise'; -import '../lib/node/async_hooks_promise'; import '../lib/common/to-string'; import '../lib/node/node'; import '../lib/zone-spec/async-test'; diff --git a/test/node_entry_point_es2017.ts b/test/node_entry_point_es2017.ts new file mode 100644 index 000000000..0425d03a9 --- /dev/null +++ b/test/node_entry_point_es2017.ts @@ -0,0 +1,39 @@ +/** + * @license + * Copyright Google Inc. All Rights Reserved. + * + * Use of this source code is governed by an MIT-style license that can be + * found in the LICENSE file at https://angular.io/license + */ + +// Must be loaded before zone loads, so that zone can detect WTF. +import './wtf_mock'; +import './test_fake_polyfill'; + +// Setup tests for Zone without microtask support +import '../lib/zone'; +const _global = global as any; +_global.__Zone_disable_node_timers = true; +_global.__Zone_disable_nextTick = true; +_global.__Zone_disable_handleUnhandledPromiseRejection = true; +_global.__Zone_disable_crypto = true; +_global.__Zone_disable_console = true; +import '../lib/node/node'; +import '../lib/node/async_promise'; +import './asynchooks/await.spec'; +/*import '../lib/zone-spec/async-test'; +import '../lib/zone-spec/fake-async-test'; +import '../lib/zone-spec/long-stack-trace'; +import '../lib/zone-spec/proxy'; +import '../lib/zone-spec/sync-test'; +import '../lib/zone-spec/task-tracking'; +import '../lib/zone-spec/wtf'; +import '../lib/rxjs/rxjs'; + +// Setup test environment +import './test-env-setup-jasmine'; + +// List all tests here: +import './common_tests'; +*/ +//import './node_tests'; diff --git a/tsconfig-node.json b/tsconfig-node.json index c3fdf4146..6829b0d4c 100644 --- a/tsconfig-node.json +++ b/tsconfig-node.json @@ -12,13 +12,14 @@ "downlevelIteration": true, "noEmitOnError": false, "stripInternal": false, - "lib": ["es5", "dom", "es2017", "es2015.symbol"] + "lib": ["es5", "dom", "es2015.promise"] }, "exclude": [ "node_modules", "build", "build-esm", "dist", - "lib/closure" + "lib/closure", + "test/asynchooks" ] } diff --git a/tsconfig.json b/tsconfig.json index ba02056e0..f132bbdc5 100644 --- a/tsconfig.json +++ b/tsconfig.json @@ -21,6 +21,6 @@ "build-esm", "dist", "lib/closure", - "test/node_async.ts" + "test/asynchooks" ] } \ No newline at end of file From 02b187d9038032a1c7908c8165a31f7fb2b489a3 Mon Sep 17 00:00:00 2001 From: "JiaLi.Passion" Date: Thu, 4 Jan 2018 00:24:03 +0900 Subject: [PATCH 3/7] feat(asynchooks): implement asynchooks version of zone.js --- gulpfile.js | 5 + lib/common/promise.ts | 2 +- lib/node/async_hooks.ts | 235 +++ lib/node/async_promise.ts | 80 -- lib/zone.ts | 47 +- test/asynchooks/await.spec.ts | 4 +- test/common/Promise.spec.ts | 3 +- test/common/microtasks.spec.ts | 2 +- test/common/setInterval.spec.ts | 30 +- test/common/setTimeout.spec.ts | 15 +- test/common/task.spec.ts | 36 +- test/common/toString.spec.ts | 59 +- test/common/zone.spec.ts | 15 +- test/common_tests.ts | 3 +- test/node/process.spec.ts | 122 +- test/node_entry_point.ts | 2 +- test/node_entry_point_es2017.ts | 11 +- test/test-util.ts | 15 +- test/zone-spec/fake-async-test.spec.ts | 1336 +++++++++--------- test/zone-spec/long-stack-trace-zone.spec.ts | 2 +- 20 files changed, 1117 insertions(+), 907 deletions(-) create mode 100644 lib/node/async_hooks.ts delete mode 100644 lib/node/async_promise.ts diff --git a/gulpfile.js b/gulpfile.js index 59a4ea71b..27743d49a 100644 --- a/gulpfile.js +++ b/gulpfile.js @@ -344,6 +344,11 @@ gulp.task('test/node2017', ['compile-node-es2017'], function(cb) { runJasmineTest(['build/test/node_entry_point_es2017.js'], cb); }); +gulp.task('test/debug2017', ['compile-node-es2017'], function(cb) { + var test = require('./build/test/node_async_test').test; + test(); +}); + gulp.task('test/node', ['compile-node'], function(cb) { runJasmineTest(['build/test/node_entry_point.js'], cb); }); diff --git a/lib/common/promise.ts b/lib/common/promise.ts index 3f0113d1c..ead0c6fa8 100644 --- a/lib/common/promise.ts +++ b/lib/common/promise.ts @@ -234,7 +234,7 @@ Zone.__load_patch('ZoneAwarePromise', (global: any, Zone: ZoneType, api: _ZonePr (typeof onFulfilled === FUNCTION) ? onFulfilled : forwardResolution : (typeof onRejected === FUNCTION) ? onRejected : forwardRejection; - zone.scheduleMicroTask(source, () => { + zone.scheduleMicroTask(source, () => { try { resolvePromise( chainPromise, true, zone.run(delegate, undefined, [(promise as any)[symbolValue]])); diff --git a/lib/node/async_hooks.ts b/lib/node/async_hooks.ts new file mode 100644 index 000000000..32c5b7ec0 --- /dev/null +++ b/lib/node/async_hooks.ts @@ -0,0 +1,235 @@ +/** + * @license + * Copyright Google Inc. All Rights Reserved. + * + * Use of this source code is governed by an MIT-style license that can be + * found in the LICENSE file at https://angular.io/license + */ + +/** + * patch nodejs async operations (timer, promise, net...) with + * nodejs async_hooks + */ +Zone.__load_patch('node_async_hooks', (global: any, Zone: ZoneType, api: _ZonePrivate) => { + let async_hooks; + try { + async_hooks = require('async_hooks'); + Zone.__mode__ = 'asynchooks'; + } catch (err) { + print(err.message); + return; + } + + const DEBUG = false; + const PROMISE_PROVIDER = 'PROMISE'; + const TICKOBJ_PROVIDER = 'TickObject'; + const TIMER_PROVIDER = 'Timeout'; + const TIMERWRAP_PROVIDER = 'TIMERWRAP'; + const SET_INTERVAL = 'setInterval'; + const SET_TIMEOUT = 'setTimeout'; + const NEXT_TICK = 'process.nextTick'; + + const NUMBER = 'number'; + + const noop = function() {}; + + let isPromiseTick = false; + + interface AsyncHooksContext { + zone?: Zone; + id: number; + task?: Task; + } + + const idPromise: AsyncHooksContext[] = []; + const idMicroTasks: AsyncHooksContext[] = []; + const idMacroTasks: AsyncHooksContext[] = []; + + if ((process as any).setUncaughtExceptionCaptureCallback) { + (process as any).setUncaughtExceptionCaptureCallback((error: any) => { + const zone = Zone.current; + (zone as any)._zoneDelegate.handleError(zone, error); + }); + } + + process.on('unhandledRejection', (reason: any, p: any) => { + const zone = Zone.current; + (zone as any)._zoneDelegate.handleError(zone, error); + }); + + function binarySearch( + array: AsyncHooksContext[], id: number, isDeleteNonPeriodic = false, + isForceDelete = false): AsyncHooksContext { + let low = 0, high = array.length - 1, mid; + while (low <= high) { + mid = Math.floor((low + high) / 2); + const midCtx = array[mid]; + if (midCtx.id === id) { + if (isForceDelete || (isDeleteNonPeriodic && midCtx.task && midCtx.task.data && + !midCtx.task.data.isPeriodic)) { + array.splice(mid, 1); + } + return midCtx; + } else if (midCtx.id < id) + low = mid + 1; + else + high = mid - 1; + } + return null; + } + + function cancelTask(task: Task, id: number) { + if (task.source === SET_TIMEOUT) { + clearTimeout((task.data as any).args); + } else if (task.source === SET_INTERVAL) { + clearInterval((task.data as any).args); + } + } + + function print(...args: string[]) { + if (!DEBUG) { + return; + } + if (!args) { + return; + } + (process as any)._rawDebug(args.join(' ')); + } + + function printObj(obj: any) { + if (!DEBUG) { + return; + } + print(Object.keys(obj) + .map((key: string) => { + return key + ':' + obj[key]; + }) + .join(',')); + } + + api.setPromiseTick = (flag: boolean) => { + isPromiseTick = flag; + }; + + function promiseInit(id: number, triggerId: number, parentHandle: any) { + if (!parentHandle) { + print('no parenthandle'); + return; + } + const promise = parentHandle.promise; + const originalThen = promise.then; + + const zone = Zone.current; + idPromise.push({id, zone}); + promise.then = function(onResolve: any, onReject: any) { + const task = zone.scheduleMicroTask(PROMISE_PROVIDER, noop, null, noop); + isPromiseTick = true; + process.nextTick(() => { + task.zone.runTask(task, null, null); + originalThen.call(this, onResolve, onReject); + }); + }; + } + + function scheduleMicroTask(id: number, provider: string, triggerId: number, parentHandle: any) { + const zone = Zone.current; + const task = zone.scheduleMicroTask(NEXT_TICK, noop, null, noop); + idMicroTasks.push({id, zone, task}); + } + + function scheduleMacroTask( + id: number, provider: string, triggerId: number, parentHandle: any, delay?: number, + isPeriodic = false) { + const zone = Zone.current; + const data: any = {isPeriodic: isPeriodic}; + if (delay) { + data.delay = delay; + } + data.args = parentHandle; + let source: string = provider; + if (isPeriodic) { + source = SET_INTERVAL; + } else if (provider === TIMER_PROVIDER) { + source = SET_TIMEOUT; + } + const task = zone.scheduleMacroTask(source, noop, data, noop, () => { + cancelTask(task, id); + }); + if (id === 51) { + print('task state', task.state); + } + idMacroTasks.push({id, zone, task}); + } + + function init(id: number, provider: string, triggerId: number, parentHandle: any) { + print('init', provider, id.toString()); + if (isPromiseTick) { + isPromiseTick = false; + return; + } + if (provider === TIMERWRAP_PROVIDER) { + return; + } + if (provider === PROMISE_PROVIDER) { + promiseInit(id, triggerId, parentHandle); + } else if (provider === TICKOBJ_PROVIDER) { + scheduleMicroTask(id, provider, triggerId, parentHandle); + } else { + if (provider === TIMER_PROVIDER && parentHandle && typeof parentHandle._repeat === NUMBER) { + scheduleMacroTask(id, provider, triggerId, parentHandle, parentHandle._idleTimeout, true); + } else { + printObj(parentHandle); + scheduleMacroTask( + id, provider, triggerId, parentHandle, + typeof parentHandle._idleTimeout === NUMBER ? parentHandle._idleTimeout : undefined, + false); + } + } + } + + function before(id: number) { + let currentAsyncContext = binarySearch(idPromise, id); + if (currentAsyncContext) { + api.setAsyncContext(currentAsyncContext); + return; + } + currentAsyncContext = binarySearch(idMicroTasks, id); + if (!currentAsyncContext) { + currentAsyncContext = binarySearch(idMacroTasks, id); + } + if (currentAsyncContext) { + print('before ', currentAsyncContext && currentAsyncContext.task.source, id.toString()); + api.beforeRunTask(currentAsyncContext.zone, currentAsyncContext.task); + (currentAsyncContext.zone as any).invokeTask(currentAsyncContext.task, null, null); + } + } + + function after(id: number) { + let currentAsyncContext = binarySearch(idPromise, id, true); + if (currentAsyncContext) { + api.setAsyncContext(null); + return; + } + currentAsyncContext = binarySearch(idMicroTasks, id, true); + if (!currentAsyncContext) { + currentAsyncContext = binarySearch(idMacroTasks, id, true); + } + if (currentAsyncContext) { + api.afterRunTask(currentAsyncContext.zone, currentAsyncContext.task); + } + print('after ', currentAsyncContext && currentAsyncContext.task.source, id.toString()); + } + + function destroy(id: number) { + const currentAsyncContext = binarySearch(idMacroTasks, id, true, true); + if (currentAsyncContext) { + print('cancel async context', currentAsyncContext.task.source, id.toString()); + printObj((currentAsyncContext.task.data as any).args); + if (currentAsyncContext.task.state !== 'notScheduled') { + currentAsyncContext.zone.cancelTask(currentAsyncContext.task); + } + } + } + + async_hooks.createHook({init, before, after, destroy}).enable(); +}); \ No newline at end of file diff --git a/lib/node/async_promise.ts b/lib/node/async_promise.ts deleted file mode 100644 index e8bfd6eb7..000000000 --- a/lib/node/async_promise.ts +++ /dev/null @@ -1,80 +0,0 @@ -/** - * @license - * Copyright Google Inc. All Rights Reserved. - * - * Use of this source code is governed by an MIT-style license that can be - * found in the LICENSE file at https://angular.io/license - */ - -/** - * patch nodejs async operations (timer, promise, net...) with - * nodejs async_hooks - */ -Zone.__load_patch('node_async_hooks_promise', (global: any, Zone: ZoneType, api: _ZonePrivate) => { - let async_hooks; - try { - async_hooks = require('async_hooks'); - } catch (err) { - print(err.message); - return; - } - - const PROMISE_PROVIDER = 'PROMISE'; - const noop = function() {}; - - const idPromise: {[key: number]: any} = {}; - - function print(...args: string[]) { - if (!args) { - return; - } - (process as any)._rawDebug(args.join(' ')); - } - - function init(id: number, provider: string, triggerId: number, parentHandle: any) { - if (provider === PROMISE_PROVIDER) { - if (!parentHandle) { - print('no parenthandle'); - return; - } - const promise = parentHandle.promise; - const originalThen = promise.then; - - const zone = Zone.current; - if (!zone.parent) { - return; - } - const currentAsyncContext: any = {}; - currentAsyncContext.id = id; - currentAsyncContext.zone = zone; - idPromise[id] = currentAsyncContext; - promise.then = function(onResolve: any, onReject: any) { - const task = zone.scheduleMicroTask(PROMISE_PROVIDER, noop, null, noop); - process.nextTick(() => { - task.zone.runTask(task, null, null); - originalThen.call(this, onResolve, onReject); - }); - }; - } - } - - function before(id: number) { - const currentAsyncContext = idPromise[id]; - if (currentAsyncContext) { - api.setAsyncContext(currentAsyncContext); - } - } - - function after(id: number) { - const currentAsyncContext = idPromise[id]; - if (currentAsyncContext) { - idPromise[id] = null; - api.setAsyncContext(null); - } - } - - function destroy(id: number) { - } - - async_hooks.createHook({init, before, after, destroy}).enable(); -}); \ No newline at end of file diff --git a/lib/zone.ts b/lib/zone.ts index 0d8c4af0e..045422326 100644 --- a/lib/zone.ts +++ b/lib/zone.ts @@ -307,6 +307,9 @@ interface ZoneType { /** @internal */ __symbol__(name: string): string; + + /** @internal */ + __mode__: _ZoneMode; } /** @internal */ @@ -333,10 +336,11 @@ interface _ZonePrivate { (target: any, name: string, patchFn: (delegate: Function, delegateName: string, name: string) => (self: any, args: any[]) => any) => Function; - patchArguments: (target: any, name: string, source: string) => Function; + patchArguments: (target: any, name: string, source: string) => Function; beforeRunTask: (zone: Zone, task: Task) => void; afterRunTask: (zone: Zone, task: Task) => void; setAsyncContext: (asyncContext: any) => void; + setPromiseTick: (isPromiseTick: boolean) => void; } /** @internal */ @@ -345,6 +349,9 @@ interface _ZoneFrame { zone: Zone; } +/** @internal */ +type _ZoneMode = 'delegate'|'asynchooks'; + /** * Provides a way to configure the interception of zone events. * @@ -652,6 +659,7 @@ const Zone: ZoneType = (function(global: any) { class Zone implements AmbientZone { static __symbol__: (name: string) => string = __symbol__; + static __zoneMode__: _ZoneMode = 'delegate'; static assertZonePatched() { if (global['Promise'] !== patches['ZoneAwarePromise']) { @@ -680,6 +688,14 @@ const Zone: ZoneType = (function(global: any) { return _currentTask; } + static get __mode__(): _ZoneMode { + return Zone.__zoneMode__; + } + + static set __mode__(mode: _ZoneMode) { + Zone.__zoneMode__ = mode; + } + static __load_patch(name: string, fn: _PatchFn): void { if (patches.hasOwnProperty(name)) { throw Error('Already loaded patch: ' + name); @@ -753,10 +769,9 @@ const Zone: ZoneType = (function(global: any) { try { return this._zoneDelegate.invoke(this, callback, applyThis, applyArgs, source); } finally { - if (currentAsyncContext) { - return; + if (!currentAsyncContext) { + _currentZoneFrame = _currentZoneFrame.parent; } - _currentZoneFrame = _currentZoneFrame.parent; } } @@ -774,7 +789,9 @@ const Zone: ZoneType = (function(global: any) { } } } finally { - _currentZoneFrame = _currentZoneFrame.parent; + if (!currentAsyncContext) { + _currentZoneFrame = _currentZoneFrame.parent; + } } } @@ -801,7 +818,8 @@ const Zone: ZoneType = (function(global: any) { const previousTask = _currentTask; _currentTask = task; _currentZoneFrame = {parent: _currentZoneFrame, zone: this}; - //(process as any)._rawDebug('currentFrame increase ', _currentZoneFrame && _currentZoneFrame.zone.name, task.source); + //(process as any)._rawDebug('currentFrame increase ', _currentZoneFrame && + //_currentZoneFrame.zone.name, task.source); if (!task.data) { task.data = {}; } @@ -821,16 +839,18 @@ const Zone: ZoneType = (function(global: any) { // we should not reset the state to scheduled if (task.state !== notScheduled && task.state !== unknown) { if (task.type == eventTask || (task.data && task.data.isPeriodic)) { - beforeRunTaskStatus.reEntryGuard && (task as ZoneTask)._transitionTo(scheduled, running); + beforeRunTaskStatus.reEntryGuard && + (task as ZoneTask)._transitionTo(scheduled, running); } else { task.runCount = 0; this._updateTaskCount(task as ZoneTask, -1); beforeRunTaskStatus.reEntryGuard && - (task as ZoneTask)._transitionTo(notScheduled, running, notScheduled); + (task as ZoneTask)._transitionTo(notScheduled, running, notScheduled); } } _currentZoneFrame = _currentZoneFrame.parent; - //(process as any)._rawDebug('currentFrame decrease ', _currentZoneFrame && _currentZoneFrame.zone.name, task.source); + //(process as any)._rawDebug('currentFrame decrease ', _currentZoneFrame && + //_currentZoneFrame.zone.name, task.source); _currentTask = beforeRunTaskStatus.previousTask; } @@ -1305,8 +1325,14 @@ const Zone: ZoneType = (function(global: any) { } } if (nativeMicroTaskQueuePromise) { + _api.setPromiseTick(true); nativeMicroTaskQueuePromise[symbolThen](drainMicroTaskQueue); - } else { + } else if (global['Promise']) { + _api.setPromiseTick(true); + const p = Promise.resolve(0); + _api.setPromiseTick(true); + p.then(drainMicroTaskQueue); + } else if (global[symbolSetTimeout]) { global[symbolSetTimeout](drainMicroTaskQueue, 0); } } @@ -1384,6 +1410,7 @@ const Zone: ZoneType = (function(global: any) { _currentZoneFrame = _currentZoneFrame.parent; } }, + setPromiseTick: (isPromiseTick: boolean) => noop, }; let _currentZoneFrame: _ZoneFrame = {parent: null, zone: new Zone(null, null)}; let _currentTask: Task = null; diff --git a/test/asynchooks/await.spec.ts b/test/asynchooks/await.spec.ts index 145724b4e..bb259eb59 100644 --- a/test/asynchooks/await.spec.ts +++ b/test/asynchooks/await.spec.ts @@ -25,12 +25,12 @@ describe('native async/await', function() { async function asyncOutside() { return 'asyncOutside'; } - + const neverResolved = new Promise(() => {}); const waitForNever = new Promise((res, _) => { res(neverResolved); }); - + async function getNever() { return waitForNever; }; diff --git a/test/common/Promise.spec.ts b/test/common/Promise.spec.ts index 6f2ea565f..1ee0234b6 100644 --- a/test/common/Promise.spec.ts +++ b/test/common/Promise.spec.ts @@ -104,7 +104,8 @@ describe( it('should ensure that Promise this is instanceof Promise', () => { expect(() => { Promise.call({}, null); - }).toThrowError('Must be an instanceof Promise.'); + //}).toThrowError('Must be an instanceof Promise.'); + }).toThrowError(); }); it('should allow subclassing', () => { diff --git a/test/common/microtasks.spec.ts b/test/common/microtasks.spec.ts index 87f512012..86abdc786 100644 --- a/test/common/microtasks.spec.ts +++ b/test/common/microtasks.spec.ts @@ -74,7 +74,7 @@ describe('Microtasks', function() { testZone.run(function() { promise.then(function() { - expect(Zone.current).toBe(testZone); + expect(Zone.current.name).toBe(testZone.name); done(); }); }); diff --git a/test/common/setInterval.spec.ts b/test/common/setInterval.spec.ts index 42aaac7b0..b1eef38ce 100644 --- a/test/common/setInterval.spec.ts +++ b/test/common/setInterval.spec.ts @@ -8,6 +8,7 @@ 'use strict'; import {isNode, zoneSymbol} from '../../lib/common/utils'; +import {isAsyncHookMode} from '../test-util'; declare const global: any; describe('setInterval', function() { @@ -25,7 +26,10 @@ describe('setInterval', function() { return; } timeoutRunning = true; - global[zoneSymbol('setTimeout')](function() { + const nativeSetTimeout = global[zoneSymbol('setTimeout')] ? + global[zoneSymbol('setTimeout')] : + global['setTimeout']; + nativeSetTimeout(function() { const intervalUnitLog = [ '> Zone:invokeTask:setInterval("::ProxyZone::WTF::TestZone")', '< Zone:invokeTask:setInterval' @@ -34,14 +38,17 @@ describe('setInterval', function() { for (let i = 0; i < intervalCount; i++) { intervalLog = intervalLog.concat(intervalUnitLog); } - expect(wtfMock.log[0]).toEqual('# Zone:fork("::ProxyZone::WTF", "TestZone")'); + expect(wtfMock.log[0]).toContain('# Zone:fork("::ProxyZone::WTF", "TestZone")'); expect(wtfMock.log[1]) - .toEqual('> Zone:invoke:unit-test("::ProxyZone::WTF::TestZone")'); + .toContain('> Zone:invoke:unit-test("::ProxyZone::WTF::TestZone")'); expect(wtfMock.log[2]) .toContain( '# Zone:schedule:macroTask:setInterval("::ProxyZone::WTF::TestZone"'); expect(wtfMock.log[3]).toEqual('< Zone:invoke:unit-test'); - expect(wtfMock.log.splice(4)).toEqual(intervalLog); + if (!isAsyncHookMode()) { + expect(wtfMock.log.splice(4)).toEqual(intervalLog); + } + clearInterval(cancelId); done(); }); @@ -73,18 +80,21 @@ describe('setInterval', function() { zone.run(() => { const timerId = setInterval(() => {}, 100); - (global as any)[Zone.__symbol__('setTimeout')](() => { + const nativeSetTimeout = global[zoneSymbol('setTimeout')] ? global[zoneSymbol('setTimeout')] : + global['setTimeout']; + nativeSetTimeout(() => { expect(logs.length > 0).toBeTruthy(); expect(logs).toEqual( [{microTask: false, macroTask: true, eventTask: false, change: 'macroTask'}]); clearInterval(timerId); - expect(logs).toEqual([ - {microTask: false, macroTask: true, eventTask: false, change: 'macroTask'}, - {microTask: false, macroTask: false, eventTask: false, change: 'macroTask'} - ]); + if (!isAsyncHookMode()) { + expect(logs).toEqual([ + {microTask: false, macroTask: true, eventTask: false, change: 'macroTask'}, + {microTask: false, macroTask: false, eventTask: false, change: 'macroTask'} + ]); + } done(); }, 300); }); }); - }); diff --git a/test/common/setTimeout.spec.ts b/test/common/setTimeout.spec.ts index c71ab4d5f..8b39549ee 100644 --- a/test/common/setTimeout.spec.ts +++ b/test/common/setTimeout.spec.ts @@ -16,15 +16,17 @@ describe('setTimeout', function() { testZone.run(() => { const timeoutFn = function() { expect(Zone.current.name).toEqual(('TestZone')); - global[zoneSymbol('setTimeout')](function() { - expect(wtfMock.log[0]).toEqual('# Zone:fork("::ProxyZone::WTF", "TestZone")'); + const nativeSetTimeout = + global[zoneSymbol('setTimeout')] ? global[zoneSymbol('setTimeout')] : global.setTimeout; + nativeSetTimeout(function() { + expect(wtfMock.log[0]).toContain('# Zone:fork("::ProxyZone::WTF", "TestZone")'); expect(wtfMock.log[1]) - .toEqual('> Zone:invoke:unit-test("::ProxyZone::WTF::TestZone")'); + .toContain('> Zone:invoke:unit-test("::ProxyZone::WTF::TestZone")'); expect(wtfMock.log[2]) .toContain('# Zone:schedule:macroTask:setTimeout("::ProxyZone::WTF::TestZone"'); expect(wtfMock.log[3]).toEqual('< Zone:invoke:unit-test'); expect(wtfMock.log[4]) - .toEqual('> Zone:invokeTask:setTimeout("::ProxyZone::WTF::TestZone")'); + .toContain('> Zone:invokeTask:setTimeout("::ProxyZone::WTF::TestZone"'); expect(wtfMock.log[5]).toEqual('< Zone:invokeTask:setTimeout'); done(); }); @@ -35,8 +37,9 @@ describe('setTimeout', function() { expect(typeof cancelId.ref).toEqual(('function')); expect(typeof cancelId.unref).toEqual(('function')); } - expect(wtfMock.log[0]).toEqual('# Zone:fork("::ProxyZone::WTF", "TestZone")'); - expect(wtfMock.log[1]).toEqual('> Zone:invoke:unit-test("::ProxyZone::WTF::TestZone")'); + expect(wtfMock.log[0]).toContain('# Zone:fork("::ProxyZone::WTF", "TestZone"'); + expect(wtfMock.log[1]) + .toContain('> Zone:invoke:unit-test("::ProxyZone::WTF::TestZone"'); expect(wtfMock.log[2]) .toContain('# Zone:schedule:macroTask:setTimeout("::ProxyZone::WTF::TestZone"'); }, null, null, 'unit-test'); diff --git a/test/common/task.spec.ts b/test/common/task.spec.ts index 27f7a8e8c..c480a5fd0 100644 --- a/test/common/task.spec.ts +++ b/test/common/task.spec.ts @@ -329,24 +329,24 @@ describe('task lifecycle', () => { // TODO: @JiaLiPassion, consider to rewrite this case. xit('task should transit from running to canceling then from canceling to notScheduled when task is canceled in running state', - testFnWithLoggedTransitionTo(() => { - Zone.current.fork({name: 'testMacroTaskZone'}).run(() => { - const task = Zone.current.scheduleMacroTask('testMacroTask', () => { - Zone.current.cancelTask(task); - }, null, noop, noop); - task.invoke(); - }); - expect(log.map(item => { - return {toState: item.toState, fromState: item.fromState}; - })) - .toEqual([ - {toState: 'scheduling', fromState: 'notScheduled'}, - {toState: 'scheduled', fromState: 'scheduling'}, - {toState: 'running', fromState: 'scheduled'}, - {toState: 'canceling', fromState: 'running'}, - {toState: 'notScheduled', fromState: 'canceling'} - ]); - })); + testFnWithLoggedTransitionTo(() => { + Zone.current.fork({name: 'testMacroTaskZone'}).run(() => { + const task = Zone.current.scheduleMacroTask('testMacroTask', () => { + Zone.current.cancelTask(task); + }, null, noop, noop); + task.invoke(); + }); + expect(log.map(item => { + return {toState: item.toState, fromState: item.fromState}; + })) + .toEqual([ + {toState: 'scheduling', fromState: 'notScheduled'}, + {toState: 'scheduled', fromState: 'scheduling'}, + {toState: 'running', fromState: 'scheduled'}, + {toState: 'canceling', fromState: 'running'}, + {toState: 'notScheduled', fromState: 'canceling'} + ]); + })); it('task should transit from running to noScheduled when task.callback throw error', testFnWithLoggedTransitionTo(() => { diff --git a/test/common/toString.spec.ts b/test/common/toString.spec.ts index 48e7474f0..c935f57e9 100644 --- a/test/common/toString.spec.ts +++ b/test/common/toString.spec.ts @@ -7,38 +7,39 @@ */ import {zoneSymbol} from '../../lib/common/utils'; -import {ifEnvSupports} from '../test-util'; +import {ifEnvSupports, isDelegateMode} from '../test-util'; const g: any = typeof window !== 'undefined' && window || typeof self !== 'undefined' && self || global; -describe('global function patch', () => { - describe('isOriginal', () => { - it('setTimeout toString should be the same with non patched setTimeout', () => { - expect(Function.prototype.toString.call(setTimeout)) - .toEqual(Function.prototype.toString.call(g[zoneSymbol('setTimeout')])); - }); +describe('global function patch', ifEnvSupports(isDelegateMode, () => { + describe('isOriginal', () => { + it('setTimeout toString should be the same with non patched setTimeout', () => { + expect(Function.prototype.toString.call(setTimeout)) + .toEqual(Function.prototype.toString.call(g[zoneSymbol('setTimeout')])); + }); - it('MutationObserver toString should be the same with native version', - ifEnvSupports('MutationObserver', () => { - const nativeMutationObserver = g[zoneSymbol('MutationObserver')]; - if (typeof nativeMutationObserver === 'function') { - expect(Function.prototype.toString.call(g['MutationObserver'])) - .toEqual(Function.prototype.toString.call(nativeMutationObserver)); - } else { - expect(Function.prototype.toString.call(g['MutationObserver'])) - .toEqual(Object.prototype.toString.call(nativeMutationObserver)); - } - })); - }); + it('MutationObserver toString should be the same with native version', + ifEnvSupports('MutationObserver', () => { + const nativeMutationObserver = g[zoneSymbol('MutationObserver')]; + if (typeof nativeMutationObserver === 'function') { + expect(Function.prototype.toString.call(g['MutationObserver'])) + .toEqual(Function.prototype.toString.call(nativeMutationObserver)); + } else { + expect(Function.prototype.toString.call(g['MutationObserver'])) + .toEqual(Object.prototype.toString.call(nativeMutationObserver)); + } + })); + }); - describe('isNative', () => { - it('ZoneAwareError toString should look like native', () => { - expect(Function.prototype.toString.call(Error)).toContain('[native code]'); - }); + describe('isNative', () => { + it('ZoneAwareError toString should look like native', () => { + expect(Function.prototype.toString.call(Error)).toContain('[native code]'); + }); - it('EventTarget addEventListener should look like native', ifEnvSupports('HTMLElement', () => { - expect(Function.prototype.toString.call(HTMLElement.prototype.addEventListener)) - .toContain('[native code]'); - })); - }); -}); + it('EventTarget addEventListener should look like native', + ifEnvSupports('HTMLElement', () => { + expect(Function.prototype.toString.call(HTMLElement.prototype.addEventListener)) + .toContain('[native code]'); + })); + }); + })); diff --git a/test/common/zone.spec.ts b/test/common/zone.spec.ts index 744cc5a3e..319ee9011 100644 --- a/test/common/zone.spec.ts +++ b/test/common/zone.spec.ts @@ -6,7 +6,7 @@ * found in the LICENSE file at https://angular.io/license */ import {zoneSymbol} from '../../lib/common/utils'; -import { isSupportAsyncHooks } from '../test-util'; +import {isAsyncHookMode} from '../test-util'; describe('Zone', function() { const rootZone = Zone.current; @@ -23,7 +23,6 @@ describe('Zone', function() { }).toThrow(); }); - it('should fire onError if a function run by a zone throws', function() { const errorSpy = jasmine.createSpy('error'); const myZone = Zone.current.fork({name: 'spy', onHandleError: errorSpy}); @@ -67,11 +66,11 @@ describe('Zone', function() { zoneA.run(function() { zoneB.run(function() { - expect(Zone.current).toBe(zoneB); + expect(Zone.current.name).toBe(zoneB.name); }); - expect(Zone.current).toBe(zoneA); + expect(Zone.current.name).toBe(zoneA.name); }); - expect(Zone.current).toBe(zone); + expect(Zone.current.name).toBe(zone.name); }); @@ -325,7 +324,7 @@ describe('Zone', function() { describe('assert ZoneAwarePromise', () => { xit('should not throw when all is OK', () => { - if (isSupportAsyncHooks()) { + if (isAsyncHookMode()) { expect(() => Zone.assertZonePatched()).toThrow(); return; } @@ -346,8 +345,8 @@ describe('Zone', function() { expect(ZoneAwarePromise).toBeTruthy(); Zone.assertZonePatched(); expect(global.Promise).toBe(ZoneAwarePromise); - if (isSupportAsyncHooks()) { - //expect(() => Zone.assertZonePatched()).toThrow(); + if (isAsyncHookMode()) { + // expect(() => Zone.assertZonePatched()).toThrow(); return; } else { expect(ZoneAwarePromise).toBeTruthy(); diff --git a/test/common_tests.ts b/test/common_tests.ts index fb7b64df8..186274955 100644 --- a/test/common_tests.ts +++ b/test/common_tests.ts @@ -11,7 +11,7 @@ import './common/zone.spec'; import './common/task.spec'; import './common/util.spec'; import './common/Promise.spec'; -/*import './common/Error.spec'; +import './common/Error.spec'; import './common/setInterval.spec'; import './common/setTimeout.spec'; import './common/toString.spec'; @@ -22,6 +22,5 @@ import './zone-spec/fake-async-test.spec'; import './zone-spec/proxy.spec'; import './zone-spec/task-tracking.spec'; import './rxjs/rxjs.spec'; -*/ Error.stackTraceLimit = Number.POSITIVE_INFINITY; \ No newline at end of file diff --git a/test/node/process.spec.ts b/test/node/process.spec.ts index 761eba652..a748cc8b3 100644 --- a/test/node/process.spec.ts +++ b/test/node/process.spec.ts @@ -7,6 +7,7 @@ */ import {zoneSymbol} from '../../lib/common/utils'; +import {ifEnvSupports, isDelegateMode} from '../test-util'; describe('process related test', () => { let zoneA: Zone, result: any[]; @@ -65,68 +66,71 @@ describe('process related test', () => { }); }); - it('should support process.on(unhandledRejection)', function(done) { - const hookSpy = jasmine.createSpy('hook'); - (Zone as any)[zoneSymbol('ignoreConsoleErrorUncaughtError')] = true; - Zone.current.fork({name: 'promise'}).run(function() { - const listener = function(reason: any, promise: any) { - hookSpy(promise, reason.message); - process.removeListener('unhandledRejection', listener); - }; - process.on('unhandledRejection', listener); - const p = new Promise((resolve, reject) => { - throw new Error('promise error'); - }); + it('should support process.on(unhandledRejection)', + ifEnvSupports(isDelegateMode, function(done: any) { + const hookSpy = jasmine.createSpy('hook'); + (Zone as any)[zoneSymbol('ignoreConsoleErrorUncaughtError')] = true; + Zone.current.fork({name: 'promise'}).run(function() { + const listener = function(reason: any, promise: any) { + hookSpy(promise, reason.message); + process.removeListener('unhandledRejection', listener); + }; + process.on('unhandledRejection', listener); + const p = new Promise((resolve, reject) => { + throw new Error('promise error'); + }); - setTimeout(function() { - expect(hookSpy).toHaveBeenCalledWith(p, 'promise error'); - done(); - }, 10); - }); - }); + setTimeout(function() { + expect(hookSpy).toHaveBeenCalledWith(p, 'promise error'); + done(); + }, 10); + }); + })); - it('should support process.on(rejectionHandled)', function(done) { - (Zone as any)[zoneSymbol('ignoreConsoleErrorUncaughtError')] = true; - Zone.current.fork({name: 'promise'}).run(function() { - const listener = function(promise: any) { - expect(promise).toEqual(p); - process.removeListener('rejectionHandled', listener); - done(); - }; - process.on('rejectionHandled', listener); - const p = new Promise((resolve, reject) => { - throw new Error('promise error'); - }); + it('should support process.on(rejectionHandled)', + ifEnvSupports(isDelegateMode, function(done: any) { + (Zone as any)[zoneSymbol('ignoreConsoleErrorUncaughtError')] = true; + Zone.current.fork({name: 'promise'}).run(function() { + const listener = function(promise: any) { + expect(promise).toEqual(p); + process.removeListener('rejectionHandled', listener); + done(); + }; + process.on('rejectionHandled', listener); + const p = new Promise((resolve, reject) => { + throw new Error('promise error'); + }); - setTimeout(function() { - p.catch(reason => {}); - }, 10); - }); - }); + setTimeout(function() { + p.catch(reason => {}); + }, 10); + }); + })); - it('should support multiple process.on(unhandledRejection)', function(done) { - const hookSpy = jasmine.createSpy('hook'); - (Zone as any)[zoneSymbol('ignoreConsoleErrorUncaughtError')] = true; - Zone.current.fork({name: 'promise'}).run(function() { - const listener1 = function(reason: any, promise: any) { - hookSpy(promise, reason.message); - process.removeListener('unhandledRejection', listener1); - }; - const listener2 = function(reason: any, promise: any) { - hookSpy(promise, reason.message); - process.removeListener('unhandledRejection', listener2); - }; - process.on('unhandledRejection', listener1); - process.on('unhandledRejection', listener2); - const p = new Promise((resolve, reject) => { - throw new Error('promise error'); - }); + it('should support multiple process.on(unhandledRejection)', + ifEnvSupports(isDelegateMode, function(done: any) { + const hookSpy = jasmine.createSpy('hook'); + (Zone as any)[zoneSymbol('ignoreConsoleErrorUncaughtError')] = true; + Zone.current.fork({name: 'promise'}).run(function() { + const listener1 = function(reason: any, promise: any) { + hookSpy(promise, reason.message); + process.removeListener('unhandledRejection', listener1); + }; + const listener2 = function(reason: any, promise: any) { + hookSpy(promise, reason.message); + process.removeListener('unhandledRejection', listener2); + }; + process.on('unhandledRejection', listener1); + process.on('unhandledRejection', listener2); + const p = new Promise((resolve, reject) => { + throw new Error('promise error'); + }); - setTimeout(function() { - expect(hookSpy.calls.count()).toBe(2); - expect(hookSpy.calls.allArgs()).toEqual([[p, 'promise error'], [p, 'promise error']]); - done(); - }, 10); - }); - }); + setTimeout(function() { + expect(hookSpy.calls.count()).toBe(2); + expect(hookSpy.calls.allArgs()).toEqual([[p, 'promise error'], [p, 'promise error']]); + done(); + }, 10); + }); + })); }); \ No newline at end of file diff --git a/test/node_entry_point.ts b/test/node_entry_point.ts index 933e86933..bbe6e0663 100644 --- a/test/node_entry_point.ts +++ b/test/node_entry_point.ts @@ -29,4 +29,4 @@ import './test-env-setup-jasmine'; // List all tests here: import './common_tests'; -//import './node_tests'; +// import './node_tests'; diff --git a/test/node_entry_point_es2017.ts b/test/node_entry_point_es2017.ts index 0425d03a9..81f056621 100644 --- a/test/node_entry_point_es2017.ts +++ b/test/node_entry_point_es2017.ts @@ -12,16 +12,18 @@ import './test_fake_polyfill'; // Setup tests for Zone without microtask support import '../lib/zone'; +import '../lib/common/promise'; const _global = global as any; _global.__Zone_disable_node_timers = true; _global.__Zone_disable_nextTick = true; _global.__Zone_disable_handleUnhandledPromiseRejection = true; _global.__Zone_disable_crypto = true; -_global.__Zone_disable_console = true; +_global.__Zone_disable_fs = true; +//_global.__Zone_disable_console = true; import '../lib/node/node'; -import '../lib/node/async_promise'; +import '../lib/node/async_hooks'; import './asynchooks/await.spec'; -/*import '../lib/zone-spec/async-test'; +import '../lib/zone-spec/async-test'; import '../lib/zone-spec/fake-async-test'; import '../lib/zone-spec/long-stack-trace'; import '../lib/zone-spec/proxy'; @@ -35,5 +37,4 @@ import './test-env-setup-jasmine'; // List all tests here: import './common_tests'; -*/ -//import './node_tests'; +import './node_tests'; diff --git a/test/test-util.ts b/test/test-util.ts index bebf8ab73..450b778d9 100644 --- a/test/test-util.ts +++ b/test/test-util.ts @@ -110,9 +110,12 @@ export function isEdge() { return userAgent.indexOf('edge') !== -1; } -export function isSupportAsyncHooks() { - if (global && (global as any)['__Zone_disable_ZoneAwarePromise']) { - return true; - } - return false; -} \ No newline at end of file +export function isAsyncHookMode() { + return Zone.__mode__ === 'asynchooks'; +} + +export function isDelegateMode() { + return !isAsyncHookMode; +} + +(isDelegateMode as any).message = 'AsyncHooks'; \ No newline at end of file diff --git a/test/zone-spec/fake-async-test.spec.ts b/test/zone-spec/fake-async-test.spec.ts index 8c53da96b..259786020 100644 --- a/test/zone-spec/fake-async-test.spec.ts +++ b/test/zone-spec/fake-async-test.spec.ts @@ -9,7 +9,7 @@ import '../../lib/zone-spec/fake-async-test'; import {isNode, patchMacroTask} from '../../lib/common/utils'; -import {ifEnvSupports} from '../test-util'; +import {ifEnvSupports, isDelegateMode} from '../test-util'; function supportNode() { return isNode; @@ -17,792 +17,794 @@ function supportNode() { (supportNode as any).message = 'support node'; -describe('FakeAsyncTestZoneSpec', () => { - let FakeAsyncTestZoneSpec = (Zone as any)['FakeAsyncTestZoneSpec']; - let testZoneSpec: any; - let fakeAsyncTestZone: Zone; - - beforeEach(() => { - testZoneSpec = new FakeAsyncTestZoneSpec('name'); - fakeAsyncTestZone = Zone.current.fork(testZoneSpec); - }); - - it('sets the FakeAsyncTestZoneSpec property', () => { - fakeAsyncTestZone.run(() => { - expect(Zone.current.get('FakeAsyncTestZoneSpec')).toEqual(testZoneSpec); - }); - }); - - describe('synchronous code', () => { - it('should run', () => { - let ran = false; - fakeAsyncTestZone.run(() => { - ran = true; - }); +describe( + 'FakeAsyncTestZoneSpec', ifEnvSupports(isDelegateMode, () => { + let FakeAsyncTestZoneSpec = (Zone as any)['FakeAsyncTestZoneSpec']; + let testZoneSpec: any; + let fakeAsyncTestZone: Zone; - expect(ran).toEqual(true); - }); + beforeEach(() => { + testZoneSpec = new FakeAsyncTestZoneSpec('name'); + fakeAsyncTestZone = Zone.current.fork(testZoneSpec); + }); - it('should throw the error in the code', () => { - expect(() => { + it('sets the FakeAsyncTestZoneSpec property', () => { fakeAsyncTestZone.run(() => { - throw new Error('sync'); + expect(Zone.current.get('FakeAsyncTestZoneSpec')).toEqual(testZoneSpec); }); - }).toThrowError('sync'); - }); + }); - it('should throw error on Rejected promise', () => { - expect(() => { - fakeAsyncTestZone.run(() => { - Promise.reject('myError'); - testZoneSpec.flushMicrotasks(); - }); - }).toThrowError('Uncaught (in promise): myError'); - }); - }); + describe('synchronous code', () => { + it('should run', () => { + let ran = false; + fakeAsyncTestZone.run(() => { + ran = true; + }); - describe('asynchronous code', () => { - it('should run', () => { - fakeAsyncTestZone.run(() => { - let thenRan = false; - Promise.resolve(null).then((_) => { - thenRan = true; + expect(ran).toEqual(true); }); - expect(thenRan).toEqual(false); + it('should throw the error in the code', () => { + expect(() => { + fakeAsyncTestZone.run(() => { + throw new Error('sync'); + }); + }).toThrowError('sync'); + }); - testZoneSpec.flushMicrotasks(); - expect(thenRan).toEqual(true); + it('should throw error on Rejected promise', () => { + expect(() => { + fakeAsyncTestZone.run(() => { + Promise.reject('myError'); + testZoneSpec.flushMicrotasks(); + }); + }).toThrowError('Uncaught (in promise): myError'); + }); }); - }); - it('should rethrow the exception on flushMicroTasks for error thrown in Promise callback', - () => { - fakeAsyncTestZone.run(() => { - Promise.resolve(null).then((_) => { - throw new Error('async'); - }); - expect(() => { - testZoneSpec.flushMicrotasks(); - }).toThrowError(/Uncaught \(in promise\): Error: async/); - }); - }); + describe('asynchronous code', () => { + it('should run', () => { + fakeAsyncTestZone.run(() => { + let thenRan = false; + Promise.resolve(null).then((_) => { + thenRan = true; + }); - it('should run chained thens', () => { - fakeAsyncTestZone.run(() => { - let log: number[] = []; + expect(thenRan).toEqual(false); - Promise.resolve(null).then((_) => log.push(1)).then((_) => log.push(2)); + testZoneSpec.flushMicrotasks(); + expect(thenRan).toEqual(true); + }); + }); - expect(log).toEqual([]); + it('should rethrow the exception on flushMicroTasks for error thrown in Promise callback', + () => { + fakeAsyncTestZone.run(() => { + Promise.resolve(null).then((_) => { + throw new Error('async'); + }); + expect(() => { + testZoneSpec.flushMicrotasks(); + }).toThrowError(/Uncaught \(in promise\): Error: async/); + }); + }); - testZoneSpec.flushMicrotasks(); - expect(log).toEqual([1, 2]); - }); - }); + it('should run chained thens', () => { + fakeAsyncTestZone.run(() => { + let log: number[] = []; + + Promise.resolve(null).then((_) => log.push(1)).then((_) => log.push(2)); - it('should run Promise created in Promise', () => { - fakeAsyncTestZone.run(() => { - let log: number[] = []; + expect(log).toEqual([]); - Promise.resolve(null).then((_) => { - log.push(1); - Promise.resolve(null).then((_) => log.push(2)); + testZoneSpec.flushMicrotasks(); + expect(log).toEqual([1, 2]); + }); }); - expect(log).toEqual([]); + it('should run Promise created in Promise', () => { + fakeAsyncTestZone.run(() => { + let log: number[] = []; - testZoneSpec.flushMicrotasks(); - expect(log).toEqual([1, 2]); + Promise.resolve(null).then((_) => { + log.push(1); + Promise.resolve(null).then((_) => log.push(2)); + }); + + expect(log).toEqual([]); + + testZoneSpec.flushMicrotasks(); + expect(log).toEqual([1, 2]); + }); + }); }); - }); - }); - describe('timers', () => { - it('should run queued zero duration timer on zero tick', () => { - fakeAsyncTestZone.run(() => { - let ran = false; - setTimeout(() => { - ran = true; - }, 0); + describe('timers', () => { + it('should run queued zero duration timer on zero tick', () => { + fakeAsyncTestZone.run(() => { + let ran = false; + setTimeout(() => { + ran = true; + }, 0); - expect(ran).toEqual(false); + expect(ran).toEqual(false); - testZoneSpec.tick(); - expect(ran).toEqual(true); - }); - }); + testZoneSpec.tick(); + expect(ran).toEqual(true); + }); + }); - it('should run queued timer after sufficient clock ticks', () => { - fakeAsyncTestZone.run(() => { - let ran = false; - setTimeout(() => { - ran = true; - }, 10); + it('should run queued timer after sufficient clock ticks', () => { + fakeAsyncTestZone.run(() => { + let ran = false; + setTimeout(() => { + ran = true; + }, 10); - testZoneSpec.tick(6); - expect(ran).toEqual(false); + testZoneSpec.tick(6); + expect(ran).toEqual(false); - testZoneSpec.tick(4); - expect(ran).toEqual(true); - }); - }); + testZoneSpec.tick(4); + expect(ran).toEqual(true); + }); + }); - it('should run queued timer created by timer callback', () => { - fakeAsyncTestZone.run(() => { - let counter = 0; - const startCounterLoop = () => { - counter++; - setTimeout(startCounterLoop, 10); - }; + it('should run queued timer created by timer callback', () => { + fakeAsyncTestZone.run(() => { + let counter = 0; + const startCounterLoop = () => { + counter++; + setTimeout(startCounterLoop, 10); + }; - startCounterLoop(); + startCounterLoop(); - expect(counter).toEqual(1); + expect(counter).toEqual(1); - testZoneSpec.tick(10); - expect(counter).toEqual(2); + testZoneSpec.tick(10); + expect(counter).toEqual(2); - testZoneSpec.tick(10); - expect(counter).toEqual(3); + testZoneSpec.tick(10); + expect(counter).toEqual(3); - testZoneSpec.tick(30); - expect(counter).toEqual(6); - }); - }); + testZoneSpec.tick(30); + expect(counter).toEqual(6); + }); + }); - it('should run queued timer only once', () => { - fakeAsyncTestZone.run(() => { - let cycles = 0; - setTimeout(() => { - cycles++; - }, 10); + it('should run queued timer only once', () => { + fakeAsyncTestZone.run(() => { + let cycles = 0; + setTimeout(() => { + cycles++; + }, 10); - testZoneSpec.tick(10); - expect(cycles).toEqual(1); + testZoneSpec.tick(10); + expect(cycles).toEqual(1); - testZoneSpec.tick(10); - expect(cycles).toEqual(1); + testZoneSpec.tick(10); + expect(cycles).toEqual(1); - testZoneSpec.tick(10); - expect(cycles).toEqual(1); - }); - expect(testZoneSpec.pendingTimers.length).toBe(0); - }); - - it('should not run cancelled timer', () => { - fakeAsyncTestZone.run(() => { - let ran = false; - let id: any = setTimeout(() => { - ran = true; - }, 10); - clearTimeout(id); - - testZoneSpec.tick(10); - expect(ran).toEqual(false); - }); - }); + testZoneSpec.tick(10); + expect(cycles).toEqual(1); + }); + expect(testZoneSpec.pendingTimers.length).toBe(0); + }); - it('should run periodic timers', () => { - fakeAsyncTestZone.run(() => { - let cycles = 0; - let id = setInterval(() => { - cycles++; - }, 10); + it('should not run cancelled timer', () => { + fakeAsyncTestZone.run(() => { + let ran = false; + let id: any = setTimeout(() => { + ran = true; + }, 10); + clearTimeout(id); - testZoneSpec.tick(10); - expect(cycles).toEqual(1); + testZoneSpec.tick(10); + expect(ran).toEqual(false); + }); + }); - testZoneSpec.tick(10); - expect(cycles).toEqual(2); + it('should run periodic timers', () => { + fakeAsyncTestZone.run(() => { + let cycles = 0; + let id = setInterval(() => { + cycles++; + }, 10); - testZoneSpec.tick(10); - expect(cycles).toEqual(3); + testZoneSpec.tick(10); + expect(cycles).toEqual(1); - testZoneSpec.tick(30); - expect(cycles).toEqual(6); - }); - }); - - it('should not run cancelled periodic timer', () => { - fakeAsyncTestZone.run(() => { - let ran = false; - let id = setInterval(() => { - ran = true; - }, 10); - - testZoneSpec.tick(10); - expect(ran).toEqual(true); - - ran = false; - clearInterval(id); - testZoneSpec.tick(10); - expect(ran).toEqual(false); - }); - }); + testZoneSpec.tick(10); + expect(cycles).toEqual(2); - it('should be able to cancel periodic timers from a callback', () => { - fakeAsyncTestZone.run(() => { - let cycles = 0; - let id: number; + testZoneSpec.tick(10); + expect(cycles).toEqual(3); - id = setInterval(() => { - cycles++; - clearInterval(id); - }, 10) as any as number; + testZoneSpec.tick(30); + expect(cycles).toEqual(6); + }); + }); - testZoneSpec.tick(10); - expect(cycles).toEqual(1); + it('should not run cancelled periodic timer', () => { + fakeAsyncTestZone.run(() => { + let ran = false; + let id = setInterval(() => { + ran = true; + }, 10); - testZoneSpec.tick(10); - expect(cycles).toEqual(1); - }); - }); + testZoneSpec.tick(10); + expect(ran).toEqual(true); - it('should process microtasks before timers', () => { - fakeAsyncTestZone.run(() => { - let log: string[] = []; + ran = false; + clearInterval(id); + testZoneSpec.tick(10); + expect(ran).toEqual(false); + }); + }); - Promise.resolve(null).then((_) => log.push('microtask')); + it('should be able to cancel periodic timers from a callback', () => { + fakeAsyncTestZone.run(() => { + let cycles = 0; + let id: number; - setTimeout(() => log.push('timer'), 9); + id = setInterval(() => { + cycles++; + clearInterval(id); + }, 10) as any as number; - setInterval(() => log.push('periodic timer'), 10); + testZoneSpec.tick(10); + expect(cycles).toEqual(1); - expect(log).toEqual([]); + testZoneSpec.tick(10); + expect(cycles).toEqual(1); + }); + }); - testZoneSpec.tick(10); - expect(log).toEqual(['microtask', 'timer', 'periodic timer']); - }); - }); + it('should process microtasks before timers', () => { + fakeAsyncTestZone.run(() => { + let log: string[] = []; - it('should process micro-tasks created in timers before next timers', () => { - fakeAsyncTestZone.run(() => { - let log: string[] = []; + Promise.resolve(null).then((_) => log.push('microtask')); - Promise.resolve(null).then((_) => log.push('microtask')); + setTimeout(() => log.push('timer'), 9); - setTimeout(() => { - log.push('timer'); - Promise.resolve(null).then((_) => log.push('t microtask')); - }, 9); + setInterval(() => log.push('periodic timer'), 10); - let id = setInterval(() => { - log.push('periodic timer'); - Promise.resolve(null).then((_) => log.push('pt microtask')); - }, 10); + expect(log).toEqual([]); - testZoneSpec.tick(10); - expect(log).toEqual( - ['microtask', 'timer', 't microtask', 'periodic timer', 'pt microtask']); + testZoneSpec.tick(10); + expect(log).toEqual(['microtask', 'timer', 'periodic timer']); + }); + }); - testZoneSpec.tick(10); - expect(log).toEqual([ - 'microtask', 'timer', 't microtask', 'periodic timer', 'pt microtask', 'periodic timer', - 'pt microtask' - ]); - }); - }); - - it('should throw the exception from tick for error thrown in timer callback', () => { - fakeAsyncTestZone.run(() => { - setTimeout(() => { - throw new Error('timer'); - }, 10); - expect(() => { - testZoneSpec.tick(10); - }).toThrowError('timer'); - }); - // There should be no pending timers after the error in timer callback. - expect(testZoneSpec.pendingTimers.length).toBe(0); - }); - - it('should throw the exception from tick for error thrown in periodic timer callback', () => { - fakeAsyncTestZone.run(() => { - let count = 0; - setInterval(() => { - count++; - throw new Error(count.toString()); - }, 10); - - expect(() => { - testZoneSpec.tick(10); - }).toThrowError('1'); - - // Periodic timer is cancelled on first error. - expect(count).toBe(1); - testZoneSpec.tick(10); - expect(count).toBe(1); - }); - // Periodic timer is removed from pending queue on error. - expect(testZoneSpec.pendingPeriodicTimers.length).toBe(0); - }); - }); - - it('should be able to resume processing timer callbacks after handling an error', () => { - fakeAsyncTestZone.run(() => { - let ran = false; - setTimeout(() => { - throw new Error('timer'); - }, 10); - setTimeout(() => { - ran = true; - }, 10); - expect(() => { - testZoneSpec.tick(10); - }).toThrowError('timer'); - expect(ran).toBe(false); - - // Restart timer queue processing. - testZoneSpec.tick(0); - expect(ran).toBe(true); - }); - // There should be no pending timers after the error in timer callback. - expect(testZoneSpec.pendingTimers.length).toBe(0); - }); - - describe('flushing all tasks', () => { - it('should flush all pending timers', () => { - fakeAsyncTestZone.run(() => { - let x = false; - let y = false; - let z = false; - - setTimeout(() => { - x = true; - }, 10); - setTimeout(() => { - y = true; - }, 100); - setTimeout(() => { - z = true; - }, 70); - - let elapsed = testZoneSpec.flush(); - - expect(elapsed).toEqual(100); - expect(x).toBe(true); - expect(y).toBe(true); - expect(z).toBe(true); - }); - }); - - it('should flush nested timers', () => { - fakeAsyncTestZone.run(() => { - let x = true; - let y = true; - setTimeout(() => { - x = true; - setTimeout(() => { - y = true; - }, 100); - }, 200); + it('should process micro-tasks created in timers before next timers', () => { + fakeAsyncTestZone.run(() => { + let log: string[] = []; - let elapsed = testZoneSpec.flush(); + Promise.resolve(null).then((_) => log.push('microtask')); - expect(elapsed).toEqual(300); - expect(x).toBe(true); - expect(y).toBe(true); - }); - }); - - it('should advance intervals', () => { - fakeAsyncTestZone.run(() => { - let x = false; - let y = false; - let z = 0; - - setTimeout(() => { - x = true; - }, 50); - setTimeout(() => { - y = true; - }, 141); - setInterval(() => { - z++; - }, 10); - - let elapsed = testZoneSpec.flush(); - - expect(elapsed).toEqual(141); - expect(x).toBe(true); - expect(y).toBe(true); - expect(z).toEqual(14); - }); - }); + setTimeout(() => { + log.push('timer'); + Promise.resolve(null).then((_) => log.push('t microtask')); + }, 9); - it('should not wait for intervals', () => { - fakeAsyncTestZone.run(() => { - let z = 0; + let id = setInterval(() => { + log.push('periodic timer'); + Promise.resolve(null).then((_) => log.push('pt microtask')); + }, 10); - setInterval(() => { - z++; - }, 10); + testZoneSpec.tick(10); + expect(log).toEqual( + ['microtask', 'timer', 't microtask', 'periodic timer', 'pt microtask']); - let elapsed = testZoneSpec.flush(); + testZoneSpec.tick(10); + expect(log).toEqual([ + 'microtask', 'timer', 't microtask', 'periodic timer', 'pt microtask', + 'periodic timer', 'pt microtask' + ]); + }); + }); + + it('should throw the exception from tick for error thrown in timer callback', () => { + fakeAsyncTestZone.run(() => { + setTimeout(() => { + throw new Error('timer'); + }, 10); + expect(() => { + testZoneSpec.tick(10); + }).toThrowError('timer'); + }); + // There should be no pending timers after the error in timer callback. + expect(testZoneSpec.pendingTimers.length).toBe(0); + }); - expect(elapsed).toEqual(0); - expect(z).toEqual(0); + it('should throw the exception from tick for error thrown in periodic timer callback', + () => { + fakeAsyncTestZone.run(() => { + let count = 0; + setInterval(() => { + count++; + throw new Error(count.toString()); + }, 10); + + expect(() => { + testZoneSpec.tick(10); + }).toThrowError('1'); + + // Periodic timer is cancelled on first error. + expect(count).toBe(1); + testZoneSpec.tick(10); + expect(count).toBe(1); + }); + // Periodic timer is removed from pending queue on error. + expect(testZoneSpec.pendingPeriodicTimers.length).toBe(0); + }); }); - }); + it('should be able to resume processing timer callbacks after handling an error', () => { + fakeAsyncTestZone.run(() => { + let ran = false; + setTimeout(() => { + throw new Error('timer'); + }, 10); + setTimeout(() => { + ran = true; + }, 10); + expect(() => { + testZoneSpec.tick(10); + }).toThrowError('timer'); + expect(ran).toBe(false); - it('should process micro-tasks created in timers before next timers', () => { - fakeAsyncTestZone.run(() => { - let log: string[] = []; + // Restart timer queue processing. + testZoneSpec.tick(0); + expect(ran).toBe(true); + }); + // There should be no pending timers after the error in timer callback. + expect(testZoneSpec.pendingTimers.length).toBe(0); + }); + + describe('flushing all tasks', () => { + it('should flush all pending timers', () => { + fakeAsyncTestZone.run(() => { + let x = false; + let y = false; + let z = false; - Promise.resolve(null).then((_) => log.push('microtask')); + setTimeout(() => { + x = true; + }, 10); + setTimeout(() => { + y = true; + }, 100); + setTimeout(() => { + z = true; + }, 70); - setTimeout(() => { - log.push('timer'); - Promise.resolve(null).then((_) => log.push('t microtask')); - }, 20); + let elapsed = testZoneSpec.flush(); - let id = setInterval(() => { - log.push('periodic timer'); - Promise.resolve(null).then((_) => log.push('pt microtask')); - }, 10); + expect(elapsed).toEqual(100); + expect(x).toBe(true); + expect(y).toBe(true); + expect(z).toBe(true); + }); + }); - testZoneSpec.flush(); - expect(log).toEqual( - ['microtask', 'periodic timer', 'pt microtask', 'timer', 't microtask']); - }); - }); - - it('should throw the exception from tick for error thrown in timer callback', () => { - fakeAsyncTestZone.run(() => { - setTimeout(() => { - throw new Error('timer'); - }, 10); - expect(() => { - testZoneSpec.flush(); - }).toThrowError('timer'); - }); - // There should be no pending timers after the error in timer callback. - expect(testZoneSpec.pendingTimers.length).toBe(0); - }); + it('should flush nested timers', () => { + fakeAsyncTestZone.run(() => { + let x = true; + let y = true; + setTimeout(() => { + x = true; + setTimeout(() => { + y = true; + }, 100); + }, 200); + + let elapsed = testZoneSpec.flush(); + + expect(elapsed).toEqual(300); + expect(x).toBe(true); + expect(y).toBe(true); + }); + }); - it('should do something reasonable with polling timeouts', () => { - expect(() => { - fakeAsyncTestZone.run(() => { - let z = 0; + it('should advance intervals', () => { + fakeAsyncTestZone.run(() => { + let x = false; + let y = false; + let z = 0; - let poll = () => { setTimeout(() => { + x = true; + }, 50); + setTimeout(() => { + y = true; + }, 141); + setInterval(() => { + z++; + }, 10); + + let elapsed = testZoneSpec.flush(); + + expect(elapsed).toEqual(141); + expect(x).toBe(true); + expect(y).toBe(true); + expect(z).toEqual(14); + }); + }); + + it('should not wait for intervals', () => { + fakeAsyncTestZone.run(() => { + let z = 0; + + setInterval(() => { z++; - poll(); }, 10); - }; - poll(); - testZoneSpec.flush(); + let elapsed = testZoneSpec.flush(); + + expect(elapsed).toEqual(0); + expect(z).toEqual(0); + }); }); - }) - .toThrowError( - 'flush failed after reaching the limit of 20 tasks. Does your code use a polling timeout?'); - }); - it('accepts a custom limit', () => { - expect(() => { - fakeAsyncTestZone.run(() => { - let z = 0; - let poll = () => { + it('should process micro-tasks created in timers before next timers', () => { + fakeAsyncTestZone.run(() => { + let log: string[] = []; + + Promise.resolve(null).then((_) => log.push('microtask')); + setTimeout(() => { - z++; - poll(); + log.push('timer'); + Promise.resolve(null).then((_) => log.push('t microtask')); + }, 20); + + let id = setInterval(() => { + log.push('periodic timer'); + Promise.resolve(null).then((_) => log.push('pt microtask')); }, 10); - }; - poll(); - testZoneSpec.flush(10); + testZoneSpec.flush(); + expect(log).toEqual( + ['microtask', 'periodic timer', 'pt microtask', 'timer', 't microtask']); + }); }); - }) - .toThrowError( - 'flush failed after reaching the limit of 10 tasks. Does your code use a polling timeout?'); - }); - it('can flush periodic timers if flushPeriodic is true', () => { - fakeAsyncTestZone.run(() => { - let x = 0; + it('should throw the exception from tick for error thrown in timer callback', () => { + fakeAsyncTestZone.run(() => { + setTimeout(() => { + throw new Error('timer'); + }, 10); + expect(() => { + testZoneSpec.flush(); + }).toThrowError('timer'); + }); + // There should be no pending timers after the error in timer callback. + expect(testZoneSpec.pendingTimers.length).toBe(0); + }); - setInterval(() => { - x++; - }, 10); + it('should do something reasonable with polling timeouts', () => { + expect(() => { + fakeAsyncTestZone.run(() => { + let z = 0; - let elapsed = testZoneSpec.flush(20, true); + let poll = () => { + setTimeout(() => { + z++; + poll(); + }, 10); + }; - expect(elapsed).toEqual(10); - expect(x).toEqual(1); - }); - }); + poll(); + testZoneSpec.flush(); + }); + }) + .toThrowError( + 'flush failed after reaching the limit of 20 tasks. Does your code use a polling timeout?'); + }); - it('can flush multiple periodic timers if flushPeriodic is true', () => { - fakeAsyncTestZone.run(() => { - let x = 0; - let y = 0; + it('accepts a custom limit', () => { + expect(() => { + fakeAsyncTestZone.run(() => { + let z = 0; - setInterval(() => { - x++; - }, 10); + let poll = () => { + setTimeout(() => { + z++; + poll(); + }, 10); + }; - setInterval(() => { - y++; - }, 100); + poll(); + testZoneSpec.flush(10); + }); + }) + .toThrowError( + 'flush failed after reaching the limit of 10 tasks. Does your code use a polling timeout?'); + }); - let elapsed = testZoneSpec.flush(20, true); + it('can flush periodic timers if flushPeriodic is true', () => { + fakeAsyncTestZone.run(() => { + let x = 0; - expect(elapsed).toEqual(100); - expect(x).toEqual(10); - expect(y).toEqual(1); - }); - }); + setInterval(() => { + x++; + }, 10); + + let elapsed = testZoneSpec.flush(20, true); + + expect(elapsed).toEqual(10); + expect(x).toEqual(1); + }); + }); + + it('can flush multiple periodic timers if flushPeriodic is true', () => { + fakeAsyncTestZone.run(() => { + let x = 0; + let y = 0; + + setInterval(() => { + x++; + }, 10); - it('can flush till the last periodic task is processed', () => { - fakeAsyncTestZone.run(() => { - let x = 0; - let y = 0; + setInterval(() => { + y++; + }, 100); - setInterval(() => { - x++; - }, 10); + let elapsed = testZoneSpec.flush(20, true); - // This shouldn't cause the flush to throw an exception even though - // it would require 100 iterations of the shorter timer. - setInterval(() => { - y++; - }, 1000); + expect(elapsed).toEqual(100); + expect(x).toEqual(10); + expect(y).toEqual(1); + }); + }); - let elapsed = testZoneSpec.flush(20, true); + it('can flush till the last periodic task is processed', () => { + fakeAsyncTestZone.run(() => { + let x = 0; + let y = 0; - // Should stop right after the longer timer has been processed. - expect(elapsed).toEqual(1000); + setInterval(() => { + x++; + }, 10); - expect(x).toEqual(100); - expect(y).toEqual(1); + // This shouldn't cause the flush to throw an exception even though + // it would require 100 iterations of the shorter timer. + setInterval(() => { + y++; + }, 1000); + + let elapsed = testZoneSpec.flush(20, true); + + // Should stop right after the longer timer has been processed. + expect(elapsed).toEqual(1000); + + expect(x).toEqual(100); + expect(y).toEqual(1); + }); + }); }); - }); - }); - - describe('outside of FakeAsync Zone', () => { - it('calling flushMicrotasks should throw exception', () => { - expect(() => { - testZoneSpec.flushMicrotasks(); - }).toThrowError('The code should be running in the fakeAsync zone to call this function'); - }); - it('calling tick should throw exception', () => { - expect(() => { - testZoneSpec.tick(); - }).toThrowError('The code should be running in the fakeAsync zone to call this function'); - }); - }); - - describe('requestAnimationFrame', () => { - const functions = - ['requestAnimationFrame', 'webkitRequestAnimationFrame', 'mozRequestAnimationFrame']; - functions.forEach((fnName) => { - describe(fnName, ifEnvSupports(fnName, () => { - it('should schedule a requestAnimationFrame with timeout of 16ms', () => { - fakeAsyncTestZone.run(() => { - let ran = false; - requestAnimationFrame(() => { - ran = true; - }); - testZoneSpec.tick(6); - expect(ran).toEqual(false); + describe('outside of FakeAsync Zone', () => { + it('calling flushMicrotasks should throw exception', () => { + expect(() => { + testZoneSpec.flushMicrotasks(); + }).toThrowError('The code should be running in the fakeAsync zone to call this function'); + }); + it('calling tick should throw exception', () => { + expect(() => { + testZoneSpec.tick(); + }).toThrowError('The code should be running in the fakeAsync zone to call this function'); + }); + }); - testZoneSpec.tick(10); - expect(ran).toEqual(true); - }); - }); - it('does not count as a pending timer', () => { - fakeAsyncTestZone.run(() => { - requestAnimationFrame(() => {}); - }); - expect(testZoneSpec.pendingTimers.length).toBe(0); - expect(testZoneSpec.pendingPeriodicTimers.length).toBe(0); - }); - it('should cancel a scheduled requestAnimatiomFrame', () => { - fakeAsyncTestZone.run(() => { - let ran = false; - const id = requestAnimationFrame(() => { - ran = true; + describe('requestAnimationFrame', () => { + const functions = + ['requestAnimationFrame', 'webkitRequestAnimationFrame', 'mozRequestAnimationFrame']; + functions.forEach((fnName) => { + describe(fnName, ifEnvSupports(fnName, () => { + it('should schedule a requestAnimationFrame with timeout of 16ms', () => { + fakeAsyncTestZone.run(() => { + let ran = false; + requestAnimationFrame(() => { + ran = true; + }); + + testZoneSpec.tick(6); + expect(ran).toEqual(false); + + testZoneSpec.tick(10); + expect(ran).toEqual(true); + }); + }); + it('does not count as a pending timer', () => { + fakeAsyncTestZone.run(() => { + requestAnimationFrame(() => {}); + }); + expect(testZoneSpec.pendingTimers.length).toBe(0); + expect(testZoneSpec.pendingPeriodicTimers.length).toBe(0); }); + it('should cancel a scheduled requestAnimatiomFrame', () => { + fakeAsyncTestZone.run(() => { + let ran = false; + const id = requestAnimationFrame(() => { + ran = true; + }); - testZoneSpec.tick(6); - expect(ran).toEqual(false); + testZoneSpec.tick(6); + expect(ran).toEqual(false); - cancelAnimationFrame(id); + cancelAnimationFrame(id); - testZoneSpec.tick(10); - expect(ran).toEqual(false); - }); - }); - it('is not flushed when flushPeriodic is false', () => { - let ran = false; - fakeAsyncTestZone.run(() => { - requestAnimationFrame(() => { - ran = true; + testZoneSpec.tick(10); + expect(ran).toEqual(false); + }); }); - testZoneSpec.flush(20); - expect(ran).toEqual(false); - }); - }); - it('is flushed when flushPeriodic is true', () => { - let ran = false; - fakeAsyncTestZone.run(() => { - requestAnimationFrame(() => { - ran = true; + it('is not flushed when flushPeriodic is false', () => { + let ran = false; + fakeAsyncTestZone.run(() => { + requestAnimationFrame(() => { + ran = true; + }); + testZoneSpec.flush(20); + expect(ran).toEqual(false); + }); + }); + it('is flushed when flushPeriodic is true', () => { + let ran = false; + fakeAsyncTestZone.run(() => { + requestAnimationFrame(() => { + ran = true; + }); + const elapsed = testZoneSpec.flush(20, true); + expect(elapsed).toEqual(16); + expect(ran).toEqual(true); + }); }); - const elapsed = testZoneSpec.flush(20, true); - expect(elapsed).toEqual(16); - expect(ran).toEqual(true); + })); + }); + }); + + describe( + 'XHRs', ifEnvSupports('XMLHttpRequest', () => { + it('should throw an exception if an XHR is initiated in the zone', () => { + expect(() => { + fakeAsyncTestZone.run(() => { + let finished = false; + let req = new XMLHttpRequest(); + + req.onreadystatechange = () => { + if (req.readyState === XMLHttpRequest.DONE) { + finished = true; + } + }; + + req.open('GET', '/test', true); + req.send(); + }); + }).toThrowError('Cannot make XHRs from within a fake async test. Request URL: /test'); + }); + })); + + describe('node process', ifEnvSupports(supportNode, () => { + it('should be able to schedule microTask with additional arguments', () => { + const process = global['process']; + const nextTick = process && process['nextTick']; + if (!nextTick) { + return; + } + fakeAsyncTestZone.run(() => { + let tickRun = false; + let cbArgRun = false; + nextTick( + (strArg: string, cbArg: Function) => { + tickRun = true; + expect(strArg).toEqual('stringArg'); + cbArg(); + }, + 'stringArg', + () => { + cbArgRun = true; + }); + + expect(tickRun).toEqual(false); + + testZoneSpec.flushMicrotasks(); + expect(tickRun).toEqual(true); + expect(cbArgRun).toEqual(true); }); + }); })); - }); - }); - describe( - 'XHRs', ifEnvSupports('XMLHttpRequest', () => { - it('should throw an exception if an XHR is initiated in the zone', () => { - expect(() => { - fakeAsyncTestZone.run(() => { - let finished = false; - let req = new XMLHttpRequest(); + describe('should allow user define which macroTask fakeAsyncTest', () => { + let FakeAsyncTestZoneSpec = (Zone as any)['FakeAsyncTestZoneSpec']; + let testZoneSpec: any; + let fakeAsyncTestZone: Zone; + it('should support custom non perodic macroTask', () => { + testZoneSpec = new FakeAsyncTestZoneSpec( + 'name', false, [{source: 'TestClass.myTimeout', callbackArgs: ['test']}]); + class TestClass { + myTimeout(callback: Function) {} + } + fakeAsyncTestZone = Zone.current.fork(testZoneSpec); + fakeAsyncTestZone.run(() => { + let ran = false; + patchMacroTask( + TestClass.prototype, 'myTimeout', + (self: any, args: any[]) => + ({name: 'TestClass.myTimeout', target: self, callbackIndex: 0, args: args})); + + const testClass = new TestClass(); + testClass.myTimeout(function(callbackArgs: any) { + ran = true; + expect(callbackArgs).toEqual('test'); + }); - req.onreadystatechange = () => { - if (req.readyState === XMLHttpRequest.DONE) { - finished = true; - } - }; + expect(ran).toEqual(false); + + testZoneSpec.tick(); + expect(ran).toEqual(true); + }); + }); - req.open('GET', '/test', true); - req.send(); + it('should support custom non perodic macroTask by global flag', () => { + testZoneSpec = new FakeAsyncTestZoneSpec('name'); + class TestClass { + myTimeout(callback: Function) {} + } + fakeAsyncTestZone = Zone.current.fork(testZoneSpec); + fakeAsyncTestZone.run(() => { + let ran = false; + patchMacroTask( + TestClass.prototype, 'myTimeout', + (self: any, args: any[]) => + ({name: 'TestClass.myTimeout', target: self, callbackIndex: 0, args: args})); + + const testClass = new TestClass(); + testClass.myTimeout(() => { + ran = true; }); - }).toThrowError('Cannot make XHRs from within a fake async test. Request URL: /test'); - }); - })); - - describe('node process', ifEnvSupports(supportNode, () => { - it('should be able to schedule microTask with additional arguments', () => { - const process = global['process']; - const nextTick = process && process['nextTick']; - if (!nextTick) { - return; - } - fakeAsyncTestZone.run(() => { - let tickRun = false; - let cbArgRun = false; - nextTick( - (strArg: string, cbArg: Function) => { - tickRun = true; - expect(strArg).toEqual('stringArg'); - cbArg(); - }, - 'stringArg', - () => { - cbArgRun = true; - }); - expect(tickRun).toEqual(false); + expect(ran).toEqual(false); - testZoneSpec.flushMicrotasks(); - expect(tickRun).toEqual(true); - expect(cbArgRun).toEqual(true); - }); + testZoneSpec.tick(); + expect(ran).toEqual(true); + }); + }); - }); - })); - - describe('should allow user define which macroTask fakeAsyncTest', () => { - let FakeAsyncTestZoneSpec = (Zone as any)['FakeAsyncTestZoneSpec']; - let testZoneSpec: any; - let fakeAsyncTestZone: Zone; - it('should support custom non perodic macroTask', () => { - testZoneSpec = new FakeAsyncTestZoneSpec( - 'name', false, [{source: 'TestClass.myTimeout', callbackArgs: ['test']}]); - class TestClass { - myTimeout(callback: Function) {} - } - fakeAsyncTestZone = Zone.current.fork(testZoneSpec); - fakeAsyncTestZone.run(() => { - let ran = false; - patchMacroTask( - TestClass.prototype, 'myTimeout', - (self: any, args: any[]) => - ({name: 'TestClass.myTimeout', target: self, callbackIndex: 0, args: args})); - - const testClass = new TestClass(); - testClass.myTimeout(function(callbackArgs: any) { - ran = true; - expect(callbackArgs).toEqual('test'); - }); - - expect(ran).toEqual(false); - - testZoneSpec.tick(); - expect(ran).toEqual(true); - }); - }); - - it('should support custom non perodic macroTask by global flag', () => { - testZoneSpec = new FakeAsyncTestZoneSpec('name'); - class TestClass { - myTimeout(callback: Function) {} - } - fakeAsyncTestZone = Zone.current.fork(testZoneSpec); - fakeAsyncTestZone.run(() => { - let ran = false; - patchMacroTask( - TestClass.prototype, 'myTimeout', - (self: any, args: any[]) => - ({name: 'TestClass.myTimeout', target: self, callbackIndex: 0, args: args})); - - const testClass = new TestClass(); - testClass.myTimeout(() => { - ran = true; - }); - - expect(ran).toEqual(false); - - testZoneSpec.tick(); - expect(ran).toEqual(true); - }); - }); - - - it('should support custom perodic macroTask', () => { - testZoneSpec = new FakeAsyncTestZoneSpec( - 'name', false, [{source: 'TestClass.myInterval', isPeriodic: true}]); - fakeAsyncTestZone = Zone.current.fork(testZoneSpec); - fakeAsyncTestZone.run(() => { - let cycle = 0; - class TestClass { - myInterval(callback: Function, interval: number): any { - return null; - } - } - patchMacroTask( - TestClass.prototype, 'myInterval', - (self: any, args: any[]) => - ({name: 'TestClass.myInterval', target: self, callbackIndex: 0, args: args})); - const testClass = new TestClass(); - const id = testClass.myInterval(() => { - cycle++; - }, 10); + it('should support custom perodic macroTask', () => { + testZoneSpec = new FakeAsyncTestZoneSpec( + 'name', false, [{source: 'TestClass.myInterval', isPeriodic: true}]); + fakeAsyncTestZone = Zone.current.fork(testZoneSpec); + fakeAsyncTestZone.run(() => { + let cycle = 0; + class TestClass { + myInterval(callback: Function, interval: number): any { + return null; + } + } + patchMacroTask( + TestClass.prototype, 'myInterval', + (self: any, args: any[]) => + ({name: 'TestClass.myInterval', target: self, callbackIndex: 0, args: args})); + + const testClass = new TestClass(); + const id = testClass.myInterval(() => { + cycle++; + }, 10); - expect(cycle).toEqual(0); + expect(cycle).toEqual(0); - testZoneSpec.tick(10); - expect(cycle).toEqual(1); + testZoneSpec.tick(10); + expect(cycle).toEqual(1); - testZoneSpec.tick(10); - expect(cycle).toEqual(2); - clearInterval(id); + testZoneSpec.tick(10); + expect(cycle).toEqual(2); + clearInterval(id); + }); + }); }); - }); - }); -}); + })); diff --git a/test/zone-spec/long-stack-trace-zone.spec.ts b/test/zone-spec/long-stack-trace-zone.spec.ts index 540128928..57bd5073c 100644 --- a/test/zone-spec/long-stack-trace-zone.spec.ts +++ b/test/zone-spec/long-stack-trace-zone.spec.ts @@ -112,7 +112,7 @@ describe( setTimeout(function() { expectElapsed(log[0].stack, 5); done(); - }, 0); + }, 50); }, 0); }, 0); }); From 4c596376205355b6c615d3374c6a1f5ce7374bdb Mon Sep 17 00:00:00 2001 From: "JiaLi.Passion" Date: Thu, 4 Jan 2018 00:35:25 +0900 Subject: [PATCH 4/7] feat(asynchooks): update travis to test node/asynchooks/es2017 async/await --- .travis.yml | 4 +- gulpfile.js | 7 +- lib/common/es2017/promise.ts | 456 ++++++++++++++++++++++++++++ lib/common/promise.ts | 4 - lib/node/es2017/rollup-main.ts | 13 + test/node_entry_point_asynchooks.ts | 39 +++ test/node_entry_point_es2017.ts | 30 +- tsconfig-esm-node.json | 6 +- tsconfig-esm.json | 6 +- tsconfig-node.es2017.json | 16 + tsconfig-node.json | 6 +- tsconfig.json | 4 + 12 files changed, 550 insertions(+), 41 deletions(-) create mode 100644 lib/common/es2017/promise.ts create mode 100644 lib/node/es2017/rollup-main.ts create mode 100644 test/node_entry_point_asynchooks.ts diff --git a/.travis.yml b/.travis.yml index ee2d8bdd7..862c8c8bf 100644 --- a/.travis.yml +++ b/.travis.yml @@ -1,7 +1,7 @@ language: node_js sudo: false node_js: - - '6.3.1' + - '8' env: global: - BROWSER_PROVIDER_READY_FILE=/tmp/sauce-connect-ready @@ -34,5 +34,7 @@ script: - node_modules/.bin/karma start karma-dist-sauce-selenium3-jasmine.conf.js --single-run - node_modules/.bin/karma start karma-build-sauce-selenium3-mocha.conf.js --single-run - node_modules/.bin/gulp test/node + - node_modules/.bin/gulp test/node/asynchooks + - node_modules/.bin/gulp test/node/es2017 - node simple-server.js 2>&1> server.log& - node ./test/webdriver/test.sauce.js \ No newline at end of file diff --git a/gulpfile.js b/gulpfile.js index 27743d49a..d0e451498 100644 --- a/gulpfile.js +++ b/gulpfile.js @@ -340,13 +340,12 @@ function runJasmineTest(specFiles, cb) { jrunner.execute(); } -gulp.task('test/node2017', ['compile-node-es2017'], function(cb) { +gulp.task('test/node/es2017', ['compile-node-es2017'], function(cb) { runJasmineTest(['build/test/node_entry_point_es2017.js'], cb); }); -gulp.task('test/debug2017', ['compile-node-es2017'], function(cb) { - var test = require('./build/test/node_async_test').test; - test(); +gulp.task('test/node/asynchooks', ['compile-node'], function(cb) { + runJasmineTest(['build/test/node_entry_point_asynchooks.js'], cb); }); gulp.task('test/node', ['compile-node'], function(cb) { diff --git a/lib/common/es2017/promise.ts b/lib/common/es2017/promise.ts new file mode 100644 index 000000000..ead0c6fa8 --- /dev/null +++ b/lib/common/es2017/promise.ts @@ -0,0 +1,456 @@ +/** + * @license + * Copyright Google Inc. All Rights Reserved. + * + * Use of this source code is governed by an MIT-style license that can be + * found in the LICENSE file at https://angular.io/license + */ +Zone.__load_patch('ZoneAwarePromise', (global: any, Zone: ZoneType, api: _ZonePrivate) => { + function readableObjectToString(obj: any) { + if (obj && obj.toString === Object.prototype.toString) { + const className = obj.constructor && obj.constructor.name; + return (className ? className : '') + ': ' + JSON.stringify(obj); + } + + return obj ? obj.toString() : Object.prototype.toString.call(obj); + } + + interface UncaughtPromiseError extends Error { + zone: AmbientZone; + task: Task; + promise: ZoneAwarePromise; + rejection: any; + } + + const __symbol__ = api.symbol; + const _uncaughtPromiseErrors: UncaughtPromiseError[] = []; + const symbolPromise = __symbol__('Promise'); + const symbolThen = __symbol__('then'); + const creationTrace = '__creationTrace__'; + + api.onUnhandledError = (e: any) => { + if (api.showUncaughtError()) { + const rejection = e && e.rejection; + if (rejection) { + console.error( + 'Unhandled Promise rejection:', + rejection instanceof Error ? rejection.message : rejection, '; Zone:', + (e.zone).name, '; Task:', e.task && (e.task).source, '; Value:', rejection, + rejection instanceof Error ? rejection.stack : undefined); + } else { + console.error(e); + } + } + }; + + api.microtaskDrainDone = () => { + while (_uncaughtPromiseErrors.length) { + while (_uncaughtPromiseErrors.length) { + const uncaughtPromiseError: UncaughtPromiseError = _uncaughtPromiseErrors.shift(); + try { + uncaughtPromiseError.zone.runGuarded(() => { + throw uncaughtPromiseError; + }); + } catch (error) { + handleUnhandledRejection(error); + } + } + } + }; + + const UNHANDLED_PROMISE_REJECTION_HANDLER_SYMBOL = __symbol__('unhandledPromiseRejectionHandler'); + + function handleUnhandledRejection(e: any) { + api.onUnhandledError(e); + try { + const handler = (Zone as any)[UNHANDLED_PROMISE_REJECTION_HANDLER_SYMBOL]; + if (handler && typeof handler === 'function') { + handler.apply(this, [e]); + } + } catch (err) { + } + } + + function isThenable(value: any): boolean { + return value && value.then; + } + + function forwardResolution(value: any): any { + return value; + } + + function forwardRejection(rejection: any): any { + return ZoneAwarePromise.reject(rejection); + } + + const symbolState: string = __symbol__('state'); + const symbolValue: string = __symbol__('value'); + const source: string = 'Promise.then'; + const UNRESOLVED: null = null; + const RESOLVED = true; + const REJECTED = false; + const REJECTED_NO_CATCH = 0; + + function makeResolver(promise: ZoneAwarePromise, state: boolean): (value: any) => void { + return (v) => { + try { + resolvePromise(promise, state, v); + } catch (err) { + resolvePromise(promise, false, err); + } + // Do not return value or you will break the Promise spec. + }; + } + + const once = function() { + let wasCalled = false; + + return function wrapper(wrappedFunction: Function) { + return function() { + if (wasCalled) { + return; + } + wasCalled = true; + wrappedFunction.apply(null, arguments); + }; + }; + }; + + const TYPE_ERROR = 'Promise resolved with itself'; + const OBJECT = 'object'; + const FUNCTION = 'function'; + const CURRENT_TASK_TRACE_SYMBOL = __symbol__('currentTaskTrace'); + + // Promise Resolution + function resolvePromise( + promise: ZoneAwarePromise, state: boolean, value: any): ZoneAwarePromise { + const onceWrapper = once(); + if (promise === value) { + throw new TypeError(TYPE_ERROR); + } + if ((promise as any)[symbolState] === UNRESOLVED) { + // should only get value.then once based on promise spec. + let then: any = null; + try { + if (typeof value === OBJECT || typeof value === FUNCTION) { + then = value && value.then; + } + } catch (err) { + onceWrapper(() => { + resolvePromise(promise, false, err); + })(); + return promise; + } + // if (value instanceof ZoneAwarePromise) { + if (state !== REJECTED && value instanceof ZoneAwarePromise && + value.hasOwnProperty(symbolState) && value.hasOwnProperty(symbolValue) && + (value as any)[symbolState] !== UNRESOLVED) { + clearRejectedNoCatch(>value); + resolvePromise(promise, (value as any)[symbolState], (value as any)[symbolValue]); + } else if (state !== REJECTED && typeof then === FUNCTION) { + try { + then.apply(value, [ + onceWrapper(makeResolver(promise, state)), onceWrapper(makeResolver(promise, false)) + ]); + } catch (err) { + onceWrapper(() => { + resolvePromise(promise, false, err); + })(); + } + } else { + (promise as any)[symbolState] = state; + const queue = (promise as any)[symbolValue]; + (promise as any)[symbolValue] = value; + + // record task information in value when error occurs, so we can + // do some additional work such as render longStackTrace + if (state === REJECTED && value instanceof Error) { + // check if longStackTraceZone is here + const trace = Zone.currentTask && Zone.currentTask.data && + (Zone.currentTask.data as any)[creationTrace]; + if (trace) { + // only keep the long stack trace into error when in longStackTraceZone + Object.defineProperty( + value, CURRENT_TASK_TRACE_SYMBOL, + {configurable: true, enumerable: false, writable: true, value: trace}); + } + } + + for (let i = 0; i < queue.length;) { + scheduleResolveOrReject(promise, queue[i++], queue[i++], queue[i++], queue[i++]); + } + if (queue.length == 0 && state == REJECTED) { + (promise as any)[symbolState] = REJECTED_NO_CATCH; + try { + // try to print more readable error log + throw new Error( + 'Uncaught (in promise): ' + readableObjectToString(value) + + (value && value.stack ? '\n' + value.stack : '')); + } catch (err) { + const error: UncaughtPromiseError = err; + error.rejection = value; + error.promise = promise; + error.zone = Zone.current; + error.task = Zone.currentTask; + _uncaughtPromiseErrors.push(error); + api.scheduleMicroTask(); // to make sure that it is running + } + } + } + } + // Resolving an already resolved promise is a noop. + return promise; + } + + const REJECTION_HANDLED_HANDLER = __symbol__('rejectionHandledHandler'); + function clearRejectedNoCatch(promise: ZoneAwarePromise): void { + if ((promise as any)[symbolState] === REJECTED_NO_CATCH) { + // if the promise is rejected no catch status + // and queue.length > 0, means there is a error handler + // here to handle the rejected promise, we should trigger + // windows.rejectionhandled eventHandler or nodejs rejectionHandled + // eventHandler + try { + const handler = (Zone as any)[REJECTION_HANDLED_HANDLER]; + if (handler && typeof handler === FUNCTION) { + handler.apply(this, [{rejection: (promise as any)[symbolValue], promise: promise}]); + } + } catch (err) { + } + (promise as any)[symbolState] = REJECTED; + for (let i = 0; i < _uncaughtPromiseErrors.length; i++) { + if (promise === _uncaughtPromiseErrors[i].promise) { + _uncaughtPromiseErrors.splice(i, 1); + } + } + } + } + + function scheduleResolveOrReject( + promise: ZoneAwarePromise, zone: AmbientZone, chainPromise: ZoneAwarePromise, + onFulfilled?: (value: R) => U1, onRejected?: (error: any) => U2): void { + clearRejectedNoCatch(promise); + const delegate = (promise as any)[symbolState] ? + (typeof onFulfilled === FUNCTION) ? onFulfilled : forwardResolution : + (typeof onRejected === FUNCTION) ? onRejected : forwardRejection; + + zone.scheduleMicroTask(source, () => { + try { + resolvePromise( + chainPromise, true, zone.run(delegate, undefined, [(promise as any)[symbolValue]])); + } catch (error) { + resolvePromise(chainPromise, false, error); + } + }); + } + + const ZONE_AWARE_PROMISE_TO_STRING = 'function ZoneAwarePromise() { [native code] }'; + type PROMISE = 'Promise'; + + class ZoneAwarePromise implements Promise { + static toString() { + return ZONE_AWARE_PROMISE_TO_STRING; + } + + get[Symbol.toStringTag]() { + return 'Promise' as PROMISE; + } + + static resolve(value: R): Promise { + return resolvePromise(>new this(null), RESOLVED, value); + } + + static reject(error: U): Promise { + return resolvePromise(>new this(null), REJECTED, error); + } + + static race(values: PromiseLike[]): Promise { + let resolve: (v: any) => void; + let reject: (v: any) => void; + let promise: any = new this((res, rej) => { + resolve = res; + reject = rej; + }); + function onResolve(value: any) { + promise && (promise = null || resolve(value)); + } + function onReject(error: any) { + promise && (promise = null || reject(error)); + } + + for (let value of values) { + if (!isThenable(value)) { + value = this.resolve(value); + } + value.then(onResolve, onReject); + } + return promise; + } + + static all(values: any): Promise { + let resolve: (v: any) => void; + let reject: (v: any) => void; + let promise = new this((res, rej) => { + resolve = res; + reject = rej; + }); + let count = 0; + const resolvedValues: any[] = []; + for (let value of values) { + if (!isThenable(value)) { + value = this.resolve(value); + } + value.then( + ((index) => (value: any) => { + resolvedValues[index] = value; + count--; + if (!count) { + resolve(resolvedValues); + } + })(count), + reject); + count++; + } + if (!count) resolve(resolvedValues); + return promise; + } + + constructor( + executor: + (resolve: (value?: R|PromiseLike) => void, reject: (error?: any) => void) => void) { + const promise: ZoneAwarePromise = this; + if (!(promise instanceof ZoneAwarePromise)) { + throw new Error('Must be an instanceof Promise.'); + } + (promise as any)[symbolState] = UNRESOLVED; + (promise as any)[symbolValue] = []; // queue; + try { + executor && executor(makeResolver(promise, RESOLVED), makeResolver(promise, REJECTED)); + } catch (error) { + resolvePromise(promise, false, error); + } + } + + then( + onFulfilled?: ((value: R) => TResult1 | PromiseLike)|undefined|null, + onRejected?: ((reason: any) => TResult2 | PromiseLike)|undefined| + null): Promise { + const chainPromise: Promise = + new (this.constructor as typeof ZoneAwarePromise)(null); + const zone = Zone.current; + if ((this as any)[symbolState] == UNRESOLVED) { + ((this as any)[symbolValue]).push(zone, chainPromise, onFulfilled, onRejected); + } else { + scheduleResolveOrReject(this, zone, chainPromise, onFulfilled, onRejected); + } + return chainPromise; + } + + catch(onRejected?: ((reason: any) => TResult | PromiseLike)|undefined| + null): Promise { + return this.then(null, onRejected); + } + } + // Protect against aggressive optimizers dropping seemingly unused properties. + // E.g. Closure Compiler in advanced mode. + ZoneAwarePromise['resolve'] = ZoneAwarePromise.resolve; + ZoneAwarePromise['reject'] = ZoneAwarePromise.reject; + ZoneAwarePromise['race'] = ZoneAwarePromise.race; + ZoneAwarePromise['all'] = ZoneAwarePromise.all; + + const NativePromise = global[symbolPromise] = global['Promise']; + const ZONE_AWARE_PROMISE = Zone.__symbol__('ZoneAwarePromise'); + + let desc = Object.getOwnPropertyDescriptor(global, 'Promise'); + if (!desc || desc.configurable) { + desc && delete desc.writable; + desc && delete desc.value; + if (!desc) { + desc = {configurable: true, enumerable: true}; + } + desc.get = function() { + // if we already set ZoneAwarePromise, use patched one + // otherwise return native one. + return global[ZONE_AWARE_PROMISE] ? global[ZONE_AWARE_PROMISE] : global[symbolPromise]; + }; + desc.set = function(NewNativePromise) { + if (NewNativePromise === ZoneAwarePromise) { + // if the NewNativePromise is ZoneAwarePromise + // save to global + global[ZONE_AWARE_PROMISE] = NewNativePromise; + } else { + // if the NewNativePromise is not ZoneAwarePromise + // for example: after load zone.js, some library just + // set es6-promise to global, if we set it to global + // directly, assertZonePatched will fail and angular + // will not loaded, so we just set the NewNativePromise + // to global[symbolPromise], so the result is just like + // we load ES6 Promise before zone.js + global[symbolPromise] = NewNativePromise; + if (!NewNativePromise.prototype[symbolThen]) { + patchThen(NewNativePromise); + } + api.setNativePromise(NewNativePromise); + } + }; + + Object.defineProperty(global, 'Promise', desc); + } + + global['Promise'] = ZoneAwarePromise; + + const symbolThenPatched = __symbol__('thenPatched'); + + function patchThen(Ctor: Function) { + const proto = Ctor.prototype; + const originalThen = proto.then; + // Keep a reference to the original method. + proto[symbolThen] = originalThen; + + // check Ctor.prototype.then propertyDescritor is writable or not + // in meteor env, writable is false, we have to make it to be true. + const prop = Object.getOwnPropertyDescriptor(Ctor.prototype, 'then'); + if (prop && prop.writable === false && prop.configurable) { + Object.defineProperty(Ctor.prototype, 'then', {writable: true}); + } + + Ctor.prototype.then = function(onResolve: any, onReject: any) { + const zone = this.zone; + const wrapped = new ZoneAwarePromise((resolve, reject) => { + originalThen.call(this, resolve, reject); + }); + if (zone) { + (wrapped as any).zone = zone; + } + return wrapped.then(onResolve, onReject); + }; + (Ctor as any)[symbolThenPatched] = true; + } + + function zoneify(fn: Function) { + return function() { + let resultPromise = fn.apply(this, arguments); + if (resultPromise instanceof ZoneAwarePromise) { + return resultPromise; + } + let ctor = resultPromise.constructor; + if (!ctor[symbolThenPatched]) { + patchThen(ctor); + } + return resultPromise; + }; + } + + if (NativePromise) { + patchThen(NativePromise); + + let fetch = global['fetch']; + if (typeof fetch == FUNCTION) { + global['fetch'] = zoneify(fetch); + } + } + + // This is not part of public API, but it is useful for tests, so we expose it. + (Promise as any)[Zone.__symbol__('uncaughtPromiseErrors')] = _uncaughtPromiseErrors; + return ZoneAwarePromise; +}); diff --git a/lib/common/promise.ts b/lib/common/promise.ts index ead0c6fa8..214e4296d 100644 --- a/lib/common/promise.ts +++ b/lib/common/promise.ts @@ -252,10 +252,6 @@ Zone.__load_patch('ZoneAwarePromise', (global: any, Zone: ZoneType, api: _ZonePr return ZONE_AWARE_PROMISE_TO_STRING; } - get[Symbol.toStringTag]() { - return 'Promise' as PROMISE; - } - static resolve(value: R): Promise { return resolvePromise(>new this(null), RESOLVED, value); } diff --git a/lib/node/es2017/rollup-main.ts b/lib/node/es2017/rollup-main.ts new file mode 100644 index 000000000..dae05bd4d --- /dev/null +++ b/lib/node/es2017/rollup-main.ts @@ -0,0 +1,13 @@ +/** + * @license + * Copyright Google Inc. All Rights Reserved. + * + * Use of this source code is governed by an MIT-style license that can be + * found in the LICENSE file at https://angular.io/license + */ + +import '../../zone'; +import '../../common/es2017/promise'; +import '../../common/to-string'; +import '../async_hooks_promise'; +import '../node'; \ No newline at end of file diff --git a/test/node_entry_point_asynchooks.ts b/test/node_entry_point_asynchooks.ts new file mode 100644 index 000000000..c4405b24b --- /dev/null +++ b/test/node_entry_point_asynchooks.ts @@ -0,0 +1,39 @@ +/** + * @license + * Copyright Google Inc. All Rights Reserved. + * + * Use of this source code is governed by an MIT-style license that can be + * found in the LICENSE file at https://angular.io/license + */ + +// Must be loaded before zone loads, so that zone can detect WTF. +import './wtf_mock'; +import './test_fake_polyfill'; + +// Setup tests for Zone without microtask support +import '../lib/zone'; +import '../lib/common/promise'; +import '../lib/common/to-string'; +const _global = global as any; +_global.__Zone_disable_node_timers = true; +_global.__Zone_disable_nextTick = true; +_global.__Zone_disable_handleUnhandledPromiseRejection = true; +_global.__Zone_disable_crypto = true; +_global.__Zone_disable_fs = true; +import '../lib/node/node'; +import '../lib/node/async_hooks'; +import '../lib/zone-spec/async-test'; +import '../lib/zone-spec/fake-async-test'; +import '../lib/zone-spec/long-stack-trace'; +import '../lib/zone-spec/proxy'; +import '../lib/zone-spec/sync-test'; +import '../lib/zone-spec/task-tracking'; +import '../lib/zone-spec/wtf'; +import '../lib/rxjs/rxjs'; + +// Setup test environment +import './test-env-setup-jasmine'; + +// List all tests here: +import './common_tests'; +import './node_tests'; diff --git a/test/node_entry_point_es2017.ts b/test/node_entry_point_es2017.ts index 81f056621..33a56b0e0 100644 --- a/test/node_entry_point_es2017.ts +++ b/test/node_entry_point_es2017.ts @@ -6,35 +6,7 @@ * found in the LICENSE file at https://angular.io/license */ -// Must be loaded before zone loads, so that zone can detect WTF. -import './wtf_mock'; -import './test_fake_polyfill'; - -// Setup tests for Zone without microtask support import '../lib/zone'; -import '../lib/common/promise'; -const _global = global as any; -_global.__Zone_disable_node_timers = true; -_global.__Zone_disable_nextTick = true; -_global.__Zone_disable_handleUnhandledPromiseRejection = true; -_global.__Zone_disable_crypto = true; -_global.__Zone_disable_fs = true; -//_global.__Zone_disable_console = true; -import '../lib/node/node'; +import '../lib/common/es2017/promise'; import '../lib/node/async_hooks'; import './asynchooks/await.spec'; -import '../lib/zone-spec/async-test'; -import '../lib/zone-spec/fake-async-test'; -import '../lib/zone-spec/long-stack-trace'; -import '../lib/zone-spec/proxy'; -import '../lib/zone-spec/sync-test'; -import '../lib/zone-spec/task-tracking'; -import '../lib/zone-spec/wtf'; -import '../lib/rxjs/rxjs'; - -// Setup test environment -import './test-env-setup-jasmine'; - -// List all tests here: -import './common_tests'; -import './node_tests'; diff --git a/tsconfig-esm-node.json b/tsconfig-esm-node.json index 39662cc22..55bce8f8a 100644 --- a/tsconfig-esm-node.json +++ b/tsconfig-esm-node.json @@ -22,6 +22,10 @@ "build-esm", "dist", "lib/closure", - "test/node_async.ts" + "test/node_entry_point_es2017.ts", + "lib/common/es2017", + "test/node_entry_point_es2017.ts", + "lib/node/es2017", + "test/asynchooks" ] } diff --git a/tsconfig-esm.json b/tsconfig-esm.json index 459979641..cd34de440 100644 --- a/tsconfig-esm.json +++ b/tsconfig-esm.json @@ -22,6 +22,10 @@ "build-esm", "dist", "lib/closure", - "test/node_async.ts" + "test/node_entry_point_es2017.ts", + "lib/common/es2017", + "test/node_entry_point_es2017.ts", + "lib/node/es2017", + "test/asynchooks" ] } diff --git a/tsconfig-node.es2017.json b/tsconfig-node.es2017.json index 61f35c7db..306077891 100644 --- a/tsconfig-node.es2017.json +++ b/tsconfig-node.es2017.json @@ -14,11 +14,27 @@ "stripInternal": false, "lib": ["es5", "dom", "es2017", "es2015.symbol"] }, + "include": [ + "lib/common", + "lib/jasmine", + "lib/node", + "lib/zone-spec", + "lib/zone.ts", + "test/asynchooks", + "test/node_entry_point_es2017.ts" + ], "exclude": [ "node_modules", "build", "build-esm", "dist", + "lib/common/promise.ts", + "test/node_entry_point.ts", + "test/node_entry_point_asynchooks.ts", + "lib/browser/rollup-main.ts", + "lib/node/rollup-main.ts", + "lib/node/rollup-test-main.ts", + "lib/mix/rollup-mix.ts", "lib/closure" ] } diff --git a/tsconfig-node.json b/tsconfig-node.json index 6829b0d4c..cad13d93c 100644 --- a/tsconfig-node.json +++ b/tsconfig-node.json @@ -20,6 +20,10 @@ "build-esm", "dist", "lib/closure", - "test/asynchooks" + "test/asynchooks", + "test/node_entry_point_es2017.ts", + "lib/common/es2017", + "test/node_entry_point_es2017.ts", + "lib/node/es2017" ] } diff --git a/tsconfig.json b/tsconfig.json index f132bbdc5..f021b866e 100644 --- a/tsconfig.json +++ b/tsconfig.json @@ -21,6 +21,10 @@ "build-esm", "dist", "lib/closure", + "test/node_entry_point_es2017.ts", + "lib/common/es2017", + "test/node_entry_point_es2017.ts", + "lib/node/es2017", "test/asynchooks" ] } \ No newline at end of file From d3d39543e0983b0aafe4d0f37d2bcec65634c695 Mon Sep 17 00:00:00 2001 From: "JiaLi.Passion" Date: Thu, 4 Jan 2018 00:47:17 +0900 Subject: [PATCH 5/7] feat(asynchooks): add gulpfiles for build --- gulpfile.js | 11 +++++++++++ lib/node/es2017/rollup-main.ts | 2 +- lib/node/rollup-main-asynchooks.ts | 12 ++++++++++++ lib/node/rollup-main.ts | 1 - 4 files changed, 24 insertions(+), 2 deletions(-) create mode 100644 lib/node/rollup-main-asynchooks.ts diff --git a/gulpfile.js b/gulpfile.js index d0e451498..2dafc574b 100644 --- a/gulpfile.js +++ b/gulpfile.js @@ -132,6 +132,15 @@ gulp.task('build/zone-mix.js', ['compile-esm-node'], function(cb) { return generateScript('./lib/mix/rollup-mix.ts', 'zone-mix.js', false, cb); }); +// Zone for node asynchooks environment. +gulp.task('build/zone-node-asynchooks.js', ['compile-esm-node'], function(cb) { + return generateScript('./lib/node/rollup-main-asynchooks.ts', 'zone-node-asynchooks.js', false, cb); +}); + +gulp.task('build/zone-node-asynchooks.min.js', ['compile-esm-node'], function(cb) { + return generateScript('./lib/node/rollup-main-asynchooks.ts', 'zone-node-asynchooks.js', true, cb); +}); + gulp.task('build/zone-error.js', ['compile-esm'], function(cb) { return generateScript('./lib/common/error-rewrite.ts', 'zone-error.js', false, cb); }); @@ -291,6 +300,8 @@ gulp.task('build', [ 'build/zone-patch-cordova.min.js', 'build/zone-patch-electron.js', 'build/zone-patch-electron.min.js', + 'build/zone-node-asynchooks.js', + 'build/zone-node-asynchooks.min.js', 'build/zone-mix.js', 'build/bluebird.js', 'build/bluebird.min.js', diff --git a/lib/node/es2017/rollup-main.ts b/lib/node/es2017/rollup-main.ts index dae05bd4d..379e84ebe 100644 --- a/lib/node/es2017/rollup-main.ts +++ b/lib/node/es2017/rollup-main.ts @@ -9,5 +9,5 @@ import '../../zone'; import '../../common/es2017/promise'; import '../../common/to-string'; -import '../async_hooks_promise'; +import '../async_hooks'; import '../node'; \ No newline at end of file diff --git a/lib/node/rollup-main-asynchooks.ts b/lib/node/rollup-main-asynchooks.ts new file mode 100644 index 000000000..cffaf8bc7 --- /dev/null +++ b/lib/node/rollup-main-asynchooks.ts @@ -0,0 +1,12 @@ +/** + * @license + * Copyright Google Inc. All Rights Reserved. + * + * Use of this source code is governed by an MIT-style license that can be + * found in the LICENSE file at https://angular.io/license + */ + +import '../zone'; +import '../common/promise'; +import '../common/to-string'; +import './async_hooks'; \ No newline at end of file diff --git a/lib/node/rollup-main.ts b/lib/node/rollup-main.ts index ec3149485..136714b1e 100644 --- a/lib/node/rollup-main.ts +++ b/lib/node/rollup-main.ts @@ -9,5 +9,4 @@ import '../zone'; import '../common/promise'; import '../common/to-string'; -import './async_hooks_promise'; import './node'; \ No newline at end of file From 7afb895ac27e04cf77b671870b6a25cb8fd6cf1d Mon Sep 17 00:00:00 2001 From: "JiaLi.Passion" Date: Thu, 4 Jan 2018 01:08:23 +0900 Subject: [PATCH 6/7] feat(asynchooks): add examples --- example/benchmarks/timeout_benchmarks.js | 18 ++++++++++++++++++ lib/node/rollup-main-asynchooks.ts | 8 +++++++- 2 files changed, 25 insertions(+), 1 deletion(-) create mode 100644 example/benchmarks/timeout_benchmarks.js diff --git a/example/benchmarks/timeout_benchmarks.js b/example/benchmarks/timeout_benchmarks.js new file mode 100644 index 000000000..0bc62d4a8 --- /dev/null +++ b/example/benchmarks/timeout_benchmarks.js @@ -0,0 +1,18 @@ +/** + * @license + * Copyright Google Inc. All Rights Reserved. + * + * Use of this source code is governed by an MIT-style license that can be + * found in the LICENSE file at https://angular.io/license + */ +require('../../dist/zone-node-asynchooks'); + +const start = Date.now(); +for (let i = 0; i < 10000; i ++) { + setTimeout(() => {}, 0); +} + +setTimeout(() => { + const end = Date.now(); + console.log('cost: ', (end - start)); +}, 100); diff --git a/lib/node/rollup-main-asynchooks.ts b/lib/node/rollup-main-asynchooks.ts index cffaf8bc7..32443d759 100644 --- a/lib/node/rollup-main-asynchooks.ts +++ b/lib/node/rollup-main-asynchooks.ts @@ -5,8 +5,14 @@ * Use of this source code is governed by an MIT-style license that can be * found in the LICENSE file at https://angular.io/license */ - import '../zone'; import '../common/promise'; import '../common/to-string'; +const _global = global as any; +_global.__Zone_disable_node_timers = true; +_global.__Zone_disable_nextTick = true; +_global.__Zone_disable_handleUnhandledPromiseRejection = true; +_global.__Zone_disable_crypto = true; +_global.__Zone_disable_fs = true; +import './node'; import './async_hooks'; \ No newline at end of file From adcb3600dad36a3f4e979c17a05cba1bdd6b99fc Mon Sep 17 00:00:00 2001 From: "JiaLi.Passion" Date: Thu, 4 Jan 2018 14:22:31 +0900 Subject: [PATCH 7/7] feat(asynchooks): don't clear periodic macrotask after invoke --- example/benchmarks/timeout_benchmarks.js | 2 +- lib/node/async_hooks.ts | 63 ++++++++++++++++-------- lib/node/rollup-main-asynchooks.ts | 10 +--- tsconfig-node.es2017.json | 7 ++- 4 files changed, 51 insertions(+), 31 deletions(-) diff --git a/example/benchmarks/timeout_benchmarks.js b/example/benchmarks/timeout_benchmarks.js index 0bc62d4a8..266b832c5 100644 --- a/example/benchmarks/timeout_benchmarks.js +++ b/example/benchmarks/timeout_benchmarks.js @@ -8,7 +8,7 @@ require('../../dist/zone-node-asynchooks'); const start = Date.now(); -for (let i = 0; i < 10000; i ++) { +for (let i = 0; i < 100000; i ++) { setTimeout(() => {}, 0); } diff --git a/lib/node/async_hooks.ts b/lib/node/async_hooks.ts index 32c5b7ec0..ff14a2b50 100644 --- a/lib/node/async_hooks.ts +++ b/lib/node/async_hooks.ts @@ -15,6 +15,11 @@ Zone.__load_patch('node_async_hooks', (global: any, Zone: ZoneType, api: _ZonePr try { async_hooks = require('async_hooks'); Zone.__mode__ = 'asynchooks'; + global.__Zone_disable_node_timers = true; + global.__Zone_disable_nextTick = true; + global.__Zone_disable_handleUnhandledPromiseRejection = true; + global.__Zone_disable_crypto = true; + global.__Zone_disable_fs = true; } catch (err) { print(err.message); return; @@ -29,6 +34,8 @@ Zone.__load_patch('node_async_hooks', (global: any, Zone: ZoneType, api: _ZonePr const SET_TIMEOUT = 'setTimeout'; const NEXT_TICK = 'process.nextTick'; + const periodicProviders = ['STATWATCHER', 'HTTPPARSER', 'TCPWRAP']; + const NUMBER = 'number'; const noop = function() {}; @@ -39,6 +46,7 @@ Zone.__load_patch('node_async_hooks', (global: any, Zone: ZoneType, api: _ZonePr zone?: Zone; id: number; task?: Task; + isDeleted?: boolean; } const idPromise: AsyncHooksContext[] = []; @@ -57,18 +65,12 @@ Zone.__load_patch('node_async_hooks', (global: any, Zone: ZoneType, api: _ZonePr (zone as any)._zoneDelegate.handleError(zone, error); }); - function binarySearch( - array: AsyncHooksContext[], id: number, isDeleteNonPeriodic = false, - isForceDelete = false): AsyncHooksContext { + function binarySearch(array: AsyncHooksContext[], id: number): AsyncHooksContext { let low = 0, high = array.length - 1, mid; while (low <= high) { mid = Math.floor((low + high) / 2); const midCtx = array[mid]; if (midCtx.id === id) { - if (isForceDelete || (isDeleteNonPeriodic && midCtx.task && midCtx.task.data && - !midCtx.task.data.isPeriodic)) { - array.splice(mid, 1); - } return midCtx; } else if (midCtx.id < id) low = mid + 1; @@ -78,6 +80,12 @@ Zone.__load_patch('node_async_hooks', (global: any, Zone: ZoneType, api: _ZonePr return null; } + function clearAsyncHooksContext(ctx: AsyncHooksContext) { + ctx.task = null; + ctx.zone = null; + ctx.isDeleted = true; + } + function cancelTask(task: Task, id: number) { if (task.source === SET_TIMEOUT) { clearTimeout((task.data as any).args); @@ -100,11 +108,12 @@ Zone.__load_patch('node_async_hooks', (global: any, Zone: ZoneType, api: _ZonePr if (!DEBUG) { return; } - print(Object.keys(obj) - .map((key: string) => { + /*print(Object.keys(obj) + .map((key: any) => { return key + ':' + obj[key]; }) .join(',')); + */ } api.setPromiseTick = (flag: boolean) => { @@ -148,16 +157,13 @@ Zone.__load_patch('node_async_hooks', (global: any, Zone: ZoneType, api: _ZonePr data.args = parentHandle; let source: string = provider; if (isPeriodic) { - source = SET_INTERVAL; + source = provider === TIMER_PROVIDER ? SET_INTERVAL : provider; } else if (provider === TIMER_PROVIDER) { source = SET_TIMEOUT; } const task = zone.scheduleMacroTask(source, noop, data, noop, () => { cancelTask(task, id); }); - if (id === 51) { - print('task state', task.state); - } idMacroTasks.push({id, zone, task}); } @@ -174,9 +180,12 @@ Zone.__load_patch('node_async_hooks', (global: any, Zone: ZoneType, api: _ZonePr promiseInit(id, triggerId, parentHandle); } else if (provider === TICKOBJ_PROVIDER) { scheduleMicroTask(id, provider, triggerId, parentHandle); + return; } else { if (provider === TIMER_PROVIDER && parentHandle && typeof parentHandle._repeat === NUMBER) { scheduleMacroTask(id, provider, triggerId, parentHandle, parentHandle._idleTimeout, true); + } else if (periodicProviders.indexOf(provider) >= 0) { + scheduleMacroTask(id, provider, triggerId, parentHandle, undefined, true); } else { printObj(parentHandle); scheduleMacroTask( @@ -198,38 +207,50 @@ Zone.__load_patch('node_async_hooks', (global: any, Zone: ZoneType, api: _ZonePr currentAsyncContext = binarySearch(idMacroTasks, id); } if (currentAsyncContext) { - print('before ', currentAsyncContext && currentAsyncContext.task.source, id.toString()); + print( + 'before ', + currentAsyncContext && currentAsyncContext.task && currentAsyncContext.task.source, + id.toString()); api.beforeRunTask(currentAsyncContext.zone, currentAsyncContext.task); (currentAsyncContext.zone as any).invokeTask(currentAsyncContext.task, null, null); } } function after(id: number) { - let currentAsyncContext = binarySearch(idPromise, id, true); + let currentAsyncContext = binarySearch(idPromise, id); if (currentAsyncContext) { api.setAsyncContext(null); + clearAsyncHooksContext(currentAsyncContext); return; } - currentAsyncContext = binarySearch(idMicroTasks, id, true); + currentAsyncContext = binarySearch(idMicroTasks, id); if (!currentAsyncContext) { - currentAsyncContext = binarySearch(idMacroTasks, id, true); + currentAsyncContext = binarySearch(idMacroTasks, id); } + print( + 'after ', + currentAsyncContext && currentAsyncContext.task && currentAsyncContext.task.source, + id.toString()); if (currentAsyncContext) { api.afterRunTask(currentAsyncContext.zone, currentAsyncContext.task); + if (!(currentAsyncContext.task && currentAsyncContext.task.data && + currentAsyncContext.task.data.isPeriodic)) { + clearAsyncHooksContext(currentAsyncContext); + } } - print('after ', currentAsyncContext && currentAsyncContext.task.source, id.toString()); } function destroy(id: number) { - const currentAsyncContext = binarySearch(idMacroTasks, id, true, true); - if (currentAsyncContext) { + const currentAsyncContext = binarySearch(idMacroTasks, id); + if (currentAsyncContext && currentAsyncContext.task) { print('cancel async context', currentAsyncContext.task.source, id.toString()); printObj((currentAsyncContext.task.data as any).args); if (currentAsyncContext.task.state !== 'notScheduled') { currentAsyncContext.zone.cancelTask(currentAsyncContext.task); } + clearAsyncHooksContext(currentAsyncContext); } } async_hooks.createHook({init, before, after, destroy}).enable(); -}); \ No newline at end of file +}); diff --git a/lib/node/rollup-main-asynchooks.ts b/lib/node/rollup-main-asynchooks.ts index 32443d759..3dfdda834 100644 --- a/lib/node/rollup-main-asynchooks.ts +++ b/lib/node/rollup-main-asynchooks.ts @@ -8,11 +8,5 @@ import '../zone'; import '../common/promise'; import '../common/to-string'; -const _global = global as any; -_global.__Zone_disable_node_timers = true; -_global.__Zone_disable_nextTick = true; -_global.__Zone_disable_handleUnhandledPromiseRejection = true; -_global.__Zone_disable_crypto = true; -_global.__Zone_disable_fs = true; -import './node'; -import './async_hooks'; \ No newline at end of file +import './async_hooks'; +// import './node'; diff --git a/tsconfig-node.es2017.json b/tsconfig-node.es2017.json index 306077891..cee0b3aaf 100644 --- a/tsconfig-node.es2017.json +++ b/tsconfig-node.es2017.json @@ -17,7 +17,11 @@ "include": [ "lib/common", "lib/jasmine", - "lib/node", + "lib/node/async_hooks.ts", + "lib/node/events.ts", + "lib/node/fs.ts", + "lib/node/node.ts", + "lib/node/es2017", "lib/zone-spec", "lib/zone.ts", "test/asynchooks", @@ -32,6 +36,7 @@ "test/node_entry_point.ts", "test/node_entry_point_asynchooks.ts", "lib/browser/rollup-main.ts", + "lib/browser/rollup-main-asynchooks.ts", "lib/node/rollup-main.ts", "lib/node/rollup-test-main.ts", "lib/mix/rollup-mix.ts",