From 34effdc3ea755a61b9b6e130b3fe737f38840130 Mon Sep 17 00:00:00 2001 From: "JiaLi.Passion" Date: Mon, 25 Dec 2017 11:27:44 +0900 Subject: [PATCH] WIP(core): use asynchooks to resolve es2017 native async/await issue --- gulpfile.js | 9 ++++ lib/browser/define-property.ts | 4 +- lib/common/promise.ts | 5 ++ lib/node/async_promise.ts | 91 ++++++++++++++++++++++++++++++++++ lib/zone.ts | 14 ++++++ test/node_async.ts | 71 ++++++++++++++++++++++++++ tsconfig-esm-node.json | 3 +- tsconfig-esm.json | 3 +- tsconfig-node.es2017.json | 24 +++++++++ tsconfig-node.json | 2 +- tsconfig.json | 3 +- 11 files changed, 223 insertions(+), 6 deletions(-) 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 09f65c987..baf329485 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); }); @@ -282,6 +286,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 22a286449..b66dddfdc 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 f2d4e6c98..1ccbc19bb 100644 --- a/lib/common/promise.ts +++ b/lib/common/promise.ts @@ -225,12 +225,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); } 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/zone.ts b/lib/zone.ts index ef6d65a9f..e8faa7ef1 100644 --- a/lib/zone.ts +++ b/lib/zone.ts @@ -327,6 +327,7 @@ interface _ZonePrivate { (target: any, name: string, patchFn: (delegate: Function, delegateName: string, name: string) => (self: any, args: any[]) => any) => Function; + setAsyncContext: (asyncContext: any) => void; } /** @internal */ @@ -741,6 +742,9 @@ const Zone: ZoneType = (function(global: any) { try { return this._zoneDelegate.invoke(this, callback, applyThis, applyArgs, source); } finally { + if (currentAsyncContext) { + return; + } _currentZoneFrame = _currentZoneFrame.parent; } } @@ -1309,6 +1313,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 +1333,14 @@ const Zone: ZoneType = (function(global: any) { nativeMicroTaskQueuePromise = NativePromise.resolve(0); } }, + 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; 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/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 c347251a7..af0d33bd7 100644 --- a/tsconfig.json +++ b/tsconfig.json @@ -18,6 +18,7 @@ "build", "build-esm", "dist", - "lib/closure" + "lib/closure", + "test/node_async.ts" ] } \ No newline at end of file