From 4b52ea0c8d146db39395b3493dc145c2d54d3794 Mon Sep 17 00:00:00 2001 From: "JiaLi.Passion" Date: Mon, 1 Jan 2018 12:13:11 +0900 Subject: [PATCH] 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