Skip to content
Open
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
2 changes: 2 additions & 0 deletions packages/zone.js/bundles.bzl
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,7 @@ BUNDLES_ENTRY_POINTS = {
},
"zone-mix": {
"external": [
"async_hooks",
"timers",
"crypto",
"events",
Expand All @@ -21,6 +22,7 @@ BUNDLES_ENTRY_POINTS = {
},
"zone-node": {
"external": [
"async_hooks",
"timers",
"crypto",
"events",
Expand Down
88 changes: 73 additions & 15 deletions packages/zone.js/lib/common/promise.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,7 @@
* Use of this source code is governed by an MIT-style license that can be
* found in the LICENSE file at https://angular.dev/license
*/
import {ZoneType} from '../zone-impl';
import {AsyncContextRunner, ZoneType} from '../zone-impl';

import {patchMethod} from './utils';

Expand Down Expand Up @@ -55,15 +55,26 @@ export function patchPromise(Zone: ZoneType): void {
api.microtaskDrainDone = () => {
while (_uncaughtPromiseErrors.length) {
const uncaughtPromiseError: UncaughtPromiseError = _uncaughtPromiseErrors.shift()!;
try {
uncaughtPromiseError.zone.runGuarded(() => {
if (uncaughtPromiseError.throwOriginal) {
throw uncaughtPromiseError.rejection;
}
throw uncaughtPromiseError;
});
} catch (error) {
handleUnhandledRejection(error);
const report = () => {
try {
uncaughtPromiseError.zone.runGuarded(() => {
if (uncaughtPromiseError.throwOriginal) {
throw uncaughtPromiseError.rejection;
}
throw uncaughtPromiseError;
});
} catch (error) {
handleUnhandledRejection(error);
}
};
// This runs after the microtask queue has been drained, so the host is no longer in the
// async context of the operation whose promise was rejected. Restore the one captured
// where the rejection happened, which is where the host would report it.
const asyncContext = uncaughtPromiseError.asyncContext;
if (asyncContext) {
asyncContext(report);
} else {
report();
}
}
};
Expand Down Expand Up @@ -213,7 +224,14 @@ export function patchPromise(Zone: ZoneType): void {
}

for (let i = 0; i < queue.length;) {
scheduleResolveOrReject(promise, queue[i++], queue[i++], queue[i++], queue[i++]);
scheduleResolveOrReject(
promise,
queue[i++],
queue[i++],
queue[i++],
queue[i++],
queue[i++],
);
}
if (queue.length == 0 && state == REJECTED) {
(promise as any)[symbolState] = REJECTED_NO_CATCH;
Expand All @@ -239,6 +257,11 @@ export function patchPromise(Zone: ZoneType): void {
uncaughtPromiseError.promise = promise;
uncaughtPromiseError.zone = Zone.current;
uncaughtPromiseError.task = Zone.currentTask!;
// The host attributes an unhandled rejection to the context the promise was created
// in. This uses the one it was rejected in, which only differs for a promise shared
// between operations; capturing at construction would cost an async resource for
// every promise.
uncaughtPromiseError.asyncContext = api.captureAsyncContext();
_uncaughtPromiseErrors.push(uncaughtPromiseError);
api.scheduleMicroTask(); // to make sure that it is running
}
Expand Down Expand Up @@ -277,6 +300,7 @@ export function patchPromise(Zone: ZoneType): void {
chainPromise: ZoneAwarePromise<any>,
onFulfilled?: ((value: R) => U1) | null | undefined,
onRejected?: ((error: any) => U2) | null | undefined,
asyncContext?: AsyncContextRunner | null,
): void {
clearRejectedNoCatch(promise);
const promiseState = (promise as any)[symbolState];
Expand Down Expand Up @@ -314,6 +338,8 @@ export function patchPromise(Zone: ZoneType): void {
}
},
chainPromise as TaskData,
undefined,
asyncContext,
);
}

Expand Down Expand Up @@ -543,10 +569,27 @@ export function patchPromise(Zone: ZoneType): void {
}
const chainPromise: Promise<TResult1 | TResult2> = new (C as typeof ZoneAwarePromise)(noop);
const zone = Zone.current;
// The host runs a reaction in the async context in which it was registered, not in the one
// in which the promise happened to settle. Capture it here, because the reactions of every
// promise in the process are run from a shared microtask queue.
const asyncContext = api.captureAsyncContext();
if ((this as any)[symbolState] == UNRESOLVED) {
(<any[]>(this as any)[symbolValue]).push(zone, chainPromise, onFulfilled, onRejected);
(<any[]>(this as any)[symbolValue]).push(
zone,
chainPromise,
onFulfilled,
onRejected,
asyncContext,
);
} else {
scheduleResolveOrReject(this, zone, chainPromise as any, onFulfilled, onRejected);
scheduleResolveOrReject(
this,
zone,
chainPromise as any,
onFulfilled,
onRejected,
asyncContext,
);
}
return chainPromise;
}
Expand All @@ -566,10 +609,25 @@ export function patchPromise(Zone: ZoneType): void {
const chainPromise: Promise<R | never> = new (C as typeof ZoneAwarePromise)(noop);
(chainPromise as any)[symbolFinally] = symbolFinally;
const zone = Zone.current;
// See the comment on the call to `captureAsyncContext` in `then`.
const asyncContext = api.captureAsyncContext();
if ((this as any)[symbolState] == UNRESOLVED) {
(<any[]>(this as any)[symbolValue]).push(zone, chainPromise, onFinally, onFinally);
(<any[]>(this as any)[symbolValue]).push(
zone,
chainPromise,
onFinally,
onFinally,
asyncContext,
);
} else {
scheduleResolveOrReject(this, zone, chainPromise as any, onFinally, onFinally);
scheduleResolveOrReject(
this,
zone,
chainPromise as any,
onFinally,
onFinally,
asyncContext,
);
}
return chainPromise;
}
Expand Down
10 changes: 9 additions & 1 deletion packages/zone.js/lib/common/queue-microtask.ts
Original file line number Diff line number Diff line change
Expand Up @@ -13,7 +13,15 @@
export function patchQueueMicrotask(global: any, api: _ZonePrivate) {
api.patchMethod(global, 'queueMicrotask', (delegate) => {
return function (self: any, args: any[]) {
Zone.current.scheduleMicroTask('queueMicrotask', args[0]);
// Microtasks of the whole process are drained from a single native microtask; capture the
// async context of this call so that the host still runs the callback in it.
Zone.current.scheduleMicroTask(
'queueMicrotask',
args[0],
undefined,
undefined,
api.captureAsyncContext(),
);
};
});
}
23 changes: 16 additions & 7 deletions packages/zone.js/lib/extra/bluebird.ts
Original file line number Diff line number Diff line change
Expand Up @@ -31,14 +31,23 @@ export function patchBluebird(Zone: ZoneType): void {
args[i] = function () {
const argSelf: any = this;
const argArgs: any = arguments;
// Bluebird runs this callback in the async context it was registered in, which
// the shared microtask queue would otherwise lose; capture it here.
const asyncContext = api.captureAsyncContext();
return new Bluebird((res: any, rej: any) => {
zone.scheduleMicroTask('Promise.then', () => {
try {
res(func.apply(argSelf, argArgs));
} catch (error) {
rej(error);
}
});
zone.scheduleMicroTask(
'Promise.then',
() => {
try {
res(func.apply(argSelf, argArgs));
} catch (error) {
rej(error);
}
},
undefined,
undefined,
asyncContext,
);
});
};
}
Expand Down
59 changes: 59 additions & 0 deletions packages/zone.js/lib/node/async-context.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,59 @@
/**
* @license
* Copyright Google LLC 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.dev/license
*/
/**
* @fileoverview
* @suppress {missingRequire}
*/

import {ZoneType} from '../zone-impl';

function loadAsyncResource(): typeof import('async_hooks').AsyncResource | undefined {
try {
// `require` is unavailable when the Node bundle is loaded as an ES module. This synchronous
// loader works in both module formats without making the patch asynchronous.
const asyncHooks = globalThis.process?.getBuiltinModule?.('node:async_hooks') as
typeof import('async_hooks') | undefined;
if (asyncHooks) {
return asyncHooks.AsyncResource;
}
return require('async_hooks').AsyncResource;
} catch {
// `async_hooks` or a synchronous module loader is not available in every Node.js-like host.
return undefined;
}
}

/**
* Node.js keeps a context of its own for every asynchronous operation — the stores of the active
* `AsyncLocalStorage` instances and the chain of async resources — and propagates it through
* promise reactions and `queueMicrotask` callbacks. Loggers, tracers and request-context
* libraries rely on it to tell concurrent operations apart.
*
* zone.js replaces the global `Promise` and runs the reactions of unrelated operations from a
* single shared microtask, so Node.js can no longer propagate that context on its own: it only
* sees the one microtask, and runs every reaction in the context which happened to schedule it.
* As the `async_hooks` documentation prescribes for code which manages its own callback queues
* or provides a custom thenable, zone.js therefore captures the context with an `AsyncResource`
* and runs the callback inside it.
*/
export function patchAsyncContext(Zone: ZoneType): void {
Zone.__load_patch('asyncContext', (global: any, Zone: ZoneType, api: _ZonePrivate) => {
const AsyncResource = loadAsyncResource();
if (AsyncResource === undefined) {
return;
}

api.captureAsyncContext = () => {
// Creating the resource captures the current context and `runInAsyncScope` enters it again.
// This is cheaper than `AsyncResource.bind` and `AsyncLocalStorage.snapshot`, which define
// properties on a bound function for every capture.
const resource = new AsyncResource('ZoneMicrotask');
return (callback) => resource.runInAsyncScope(callback);
};
});
}
2 changes: 2 additions & 0 deletions packages/zone.js/lib/node/node.ts
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,7 @@ import {patchTimer} from '../common/timers';
import {ArraySlice, isMix, patchMacroTask, patchMicroTask} from '../common/utils';
import {ZoneType} from '../zone-impl';

import {patchAsyncContext} from './async-context';
import {patchEvents} from './events';
import {patchFs} from './fs';
import {patchNodeUtil} from './node_util';
Expand All @@ -20,6 +21,7 @@ const set = 'set';
const clear = 'clear';

export function patchNode(Zone: ZoneType): void {
patchAsyncContext(Zone);
patchNodeUtil(Zone);
patchEvents(Zone);
patchFs(Zone);
Expand Down
Loading
Loading