Skip to content
This repository was archived by the owner on Feb 26, 2024. It is now read-only.

WIP(core): use asynchooks to resolve es2017 native async/await issue #984

Closed
wants to merge 1 commit into from
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
9 changes: 9 additions & 0 deletions gulpfile.js
Original file line number Diff line number Diff line change
Expand Up @@ -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);
});
Expand Down Expand Up @@ -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();
Expand Down
4 changes: 2 additions & 2 deletions lib/browser/define-property.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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);
}
Expand All @@ -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;
Expand Down
5 changes: 5 additions & 0 deletions lib/common/promise.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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<R> implements Promise<R> {
static toString() {
return ZONE_AWARE_PROMISE_TO_STRING;
}

get[Symbol.toStringTag]() {
return 'Promise' as PROMISE;
}

static resolve<R>(value: R): Promise<R> {
return resolvePromise(<ZoneAwarePromise<R>>new this(null), RESOLVED, value);
}
Expand Down
91 changes: 91 additions & 0 deletions lib/node/async_promise.ts
Original file line number Diff line number Diff line change
@@ -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();
});
14 changes: 14 additions & 0 deletions lib/zone.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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 */
Expand Down Expand Up @@ -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;
}
}
Expand Down Expand Up @@ -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,
Expand All @@ -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;
Expand Down
71 changes: 71 additions & 0 deletions test/node_async.ts
Original file line number Diff line number Diff line change
@@ -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');
});
};
3 changes: 2 additions & 1 deletion tsconfig-esm-node.json
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,7 @@
"build",
"build-esm",
"dist",
"lib/closure"
"lib/closure",
"test/node_async.ts"
]
}
3 changes: 2 additions & 1 deletion tsconfig-esm.json
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,7 @@
"build",
"build-esm",
"dist",
"lib/closure"
"lib/closure",
"test/node_async.ts"
]
}
24 changes: 24 additions & 0 deletions tsconfig-node.es2017.json
Original file line number Diff line number Diff line change
@@ -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"
]
}
2 changes: 1 addition & 1 deletion tsconfig-node.json
Original file line number Diff line number Diff line change
Expand Up @@ -12,7 +12,7 @@
"downlevelIteration": true,
"noEmitOnError": false,
"stripInternal": false,
"lib": ["es5", "dom", "es2015.promise"]
"lib": ["es5", "dom", "es2017", "es2015.symbol"]
},
"exclude": [
"node_modules",
Expand Down
3 changes: 2 additions & 1 deletion tsconfig.json
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,7 @@
"build",
"build-esm",
"dist",
"lib/closure"
"lib/closure",
"test/node_async.ts"
]
}