Skip to content

Commit fe21607

Browse files
jasnelltargos
authored andcommitted
events: add EventEmitterAsyncResource to core
Signd-off-by: James M Snell <[email protected]> PR-URL: #41246 Reviewed-By: Gerhard Stöbich <[email protected]> Reviewed-By: Anna Henningsen <[email protected]> Reviewed-By: Antoine du Hamel <[email protected]>
1 parent c546cef commit fe21607

File tree

3 files changed

+347
-0
lines changed

3 files changed

+347
-0
lines changed

doc/api/events.md

+85
Original file line numberDiff line numberDiff line change
@@ -1166,6 +1166,89 @@ const emitter = new EventEmitter();
11661166
setMaxListeners(5, target, emitter);
11671167
```
11681168

1169+
## Class: `events.EventEmitterAsyncResource extends EventEmitter`
1170+
1171+
<!-- YAML
1172+
added: REPLACEME
1173+
-->
1174+
1175+
Integrates `EventEmitter` with {AsyncResource} for `EventEmitter`s that
1176+
require manual async tracking. Specifically, all events emitted by instances
1177+
of `events.EventEmitterAsyncResource` will run within its [async context][].
1178+
1179+
```js
1180+
const { EventEmitterAsyncResource } = require('events');
1181+
const { notStrictEqual, strictEqual } = require('assert');
1182+
const { executionAsyncId } = require('async_hooks');
1183+
1184+
// Async tracking tooling will identify this as 'Q'.
1185+
const ee1 = new EventEmitterAsyncResource({ name: 'Q' });
1186+
1187+
// 'foo' listeners will run in the EventEmitters async context.
1188+
ee1.on('foo', () => {
1189+
strictEqual(executionAsyncId(), ee1.asyncId);
1190+
strictEqual(triggerAsyncId(), ee1.triggerAsyncId);
1191+
});
1192+
1193+
const ee2 = new EventEmitter();
1194+
1195+
// 'foo' listeners on ordinary EventEmitters that do not track async
1196+
// context, however, run in the same async context as the emit().
1197+
ee2.on('foo', () => {
1198+
notStrictEqual(executionAsyncId(), ee2.asyncId);
1199+
notStrictEqual(triggerAsyncId(), ee2.triggerAsyncId);
1200+
});
1201+
1202+
Promise.resolve().then(() => {
1203+
ee1.emit('foo');
1204+
ee2.emit('foo');
1205+
});
1206+
```
1207+
1208+
The `EventEmitterAsyncResource` class has the same methods and takes the
1209+
same options as `EventEmitter` and `AsyncResource` themselves.
1210+
1211+
### `new events.EventEmitterAsyncResource(options)`
1212+
1213+
* `options` {Object}
1214+
* `captureRejections` {boolean} It enables
1215+
[automatic capturing of promise rejection][capturerejections].
1216+
**Default:** `false`.
1217+
* `name` {string} The type of async event. **Default::**
1218+
[`new.target.name`][].
1219+
* `triggerAsyncId` {number} The ID of the execution context that created this
1220+
async event. **Default:** `executionAsyncId()`.
1221+
* `requireManualDestroy` {boolean} If set to `true`, disables `emitDestroy`
1222+
when the object is garbage collected. This usually does not need to be set
1223+
(even if `emitDestroy` is called manually), unless the resource's `asyncId`
1224+
is retrieved and the sensitive API's `emitDestroy` is called with it.
1225+
When set to `false`, the `emitDestroy` call on garbage collection
1226+
will only take place if there is at least one active `destroy` hook.
1227+
**Default:** `false`.
1228+
1229+
### `eventemitterasyncresource.asyncId`
1230+
1231+
* Type: {number} The unique `asyncId` assigned to the resource.
1232+
1233+
### `eventemitterasyncresource.asyncResource`
1234+
1235+
* Type: The underlying {AsyncResource}.
1236+
1237+
The returned `AsyncResource` object has an additional `eventEmitter` property
1238+
that provides a reference to this `EventEmitterAsyncResource`.
1239+
1240+
### `eventemitterasyncresource.emitDestroy()`
1241+
1242+
Call all `destroy` hooks. This should only ever be called once. An error will
1243+
be thrown if it is called more than once. This **must** be manually called. If
1244+
the resource is left to be collected by the GC then the `destroy` hooks will
1245+
never be called.
1246+
1247+
### `eventemitterasyncresource.triggerAsyncId`
1248+
1249+
* Type: {number} The same `triggerAsyncId` that is passed to the
1250+
`AsyncResource` constructor.
1251+
11691252
<a id="event-target-and-event-api"></a>
11701253

11711254
## `EventTarget` and `Event` API
@@ -1706,7 +1789,9 @@ to the `EventTarget`.
17061789
[`events.defaultMaxListeners`]: #eventsdefaultmaxlisteners
17071790
[`fs.ReadStream`]: fs.md#class-fsreadstream
17081791
[`net.Server`]: net.md#class-netserver
1792+
[`new.target.name`]: https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Operators/new.target
17091793
[`process.on('warning')`]: process.md#event-warning
1794+
[async context]: async_context.md
17101795
[capturerejections]: #capture-rejections-of-promises
17111796
[error]: #error-events
17121797
[rejection]: #emittersymbolfornodejsrejectionerr-eventname-args

lib/events.js

+130
Original file line numberDiff line numberDiff line change
@@ -27,6 +27,7 @@ const {
2727
ArrayPrototypeShift,
2828
ArrayPrototypeSlice,
2929
ArrayPrototypeSplice,
30+
ArrayPrototypeUnshift,
3031
Boolean,
3132
Error,
3233
ErrorCaptureStackTrace,
@@ -42,6 +43,7 @@ const {
4243
Promise,
4344
PromiseReject,
4445
PromiseResolve,
46+
ReflectApply,
4547
ReflectOwnKeys,
4648
String,
4749
StringPrototypeSplit,
@@ -59,6 +61,7 @@ const {
5961
kEnhanceStackBeforeInspector,
6062
codes: {
6163
ERR_INVALID_ARG_TYPE,
64+
ERR_INVALID_THIS,
6265
ERR_OUT_OF_RANGE,
6366
ERR_UNHANDLED_ERROR
6467
},
@@ -68,6 +71,7 @@ const {
6871
validateAbortSignal,
6972
validateBoolean,
7073
validateFunction,
74+
validateString,
7175
} = require('internal/validators');
7276

7377
const kCapture = Symbol('kCapture');
@@ -76,6 +80,125 @@ const kMaxEventTargetListeners = Symbol('events.maxEventTargetListeners');
7680
const kMaxEventTargetListenersWarned =
7781
Symbol('events.maxEventTargetListenersWarned');
7882

83+
let EventEmitterAsyncResource;
84+
// The EventEmitterAsyncResource has to be initialized lazily because event.js
85+
// is loaded so early in the bootstrap process, before async_hooks is available.
86+
//
87+
// This implementation was adapted straight from addaleax's
88+
// eventemitter-asyncresource MIT-licensed userland module.
89+
// https://github.com/addaleax/eventemitter-asyncresource
90+
function lazyEventEmitterAsyncResource() {
91+
if (EventEmitterAsyncResource === undefined) {
92+
const {
93+
AsyncResource
94+
} = require('async_hooks');
95+
96+
const kEventEmitter = Symbol('kEventEmitter');
97+
const kAsyncResource = Symbol('kAsyncResource');
98+
class EventEmitterReferencingAsyncResource extends AsyncResource {
99+
/**
100+
* @param {EventEmitter} ee
101+
* @param {string} [type]
102+
* @param {{
103+
* triggerAsyncId?: number,
104+
* requireManualDestroy?: boolean,
105+
* }} [options]
106+
*/
107+
constructor(ee, type, options) {
108+
super(type, options);
109+
this[kEventEmitter] = ee;
110+
}
111+
112+
/**
113+
* @type {EventEmitter}
114+
*/
115+
get eventEmitter() {
116+
if (this[kEventEmitter] === undefined)
117+
throw new ERR_INVALID_THIS('EventEmitterReferencingAsyncResource');
118+
return this[kEventEmitter];
119+
}
120+
}
121+
122+
EventEmitterAsyncResource =
123+
class EventEmitterAsyncResource extends EventEmitter {
124+
/**
125+
* @param {{
126+
* name?: string,
127+
* triggerAsyncId?: number,
128+
* requireManualDestroy?: boolean,
129+
* }} [options]
130+
*/
131+
constructor(options = undefined) {
132+
let name;
133+
if (typeof options === 'string') {
134+
name = options;
135+
options = undefined;
136+
} else {
137+
if (new.target === EventEmitterAsyncResource) {
138+
validateString(options?.name, 'options.name');
139+
}
140+
name = options?.name || new.target.name;
141+
}
142+
super(options);
143+
144+
this[kAsyncResource] =
145+
new EventEmitterReferencingAsyncResource(this, name, options);
146+
}
147+
148+
/**
149+
* @param {symbol,string} event
150+
* @param {...any} args
151+
* @returns {boolean}
152+
*/
153+
emit(event, ...args) {
154+
if (this[kAsyncResource] === undefined)
155+
throw new ERR_INVALID_THIS('EventEmitterAsyncResource');
156+
const { asyncResource } = this;
157+
ArrayPrototypeUnshift(args, super.emit, this, event);
158+
return ReflectApply(asyncResource.runInAsyncScope, asyncResource,
159+
args);
160+
}
161+
162+
/**
163+
* @returns {void}
164+
*/
165+
emitDestroy() {
166+
if (this[kAsyncResource] === undefined)
167+
throw new ERR_INVALID_THIS('EventEmitterAsyncResource');
168+
this.asyncResource.emitDestroy();
169+
}
170+
171+
/**
172+
* @type {number}
173+
*/
174+
get asyncId() {
175+
if (this[kAsyncResource] === undefined)
176+
throw new ERR_INVALID_THIS('EventEmitterAsyncResource');
177+
return this.asyncResource.asyncId();
178+
}
179+
180+
/**
181+
* @type {number}
182+
*/
183+
get triggerAsyncId() {
184+
if (this[kAsyncResource] === undefined)
185+
throw new ERR_INVALID_THIS('EventEmitterAsyncResource');
186+
return this.asyncResource.triggerAsyncId();
187+
}
188+
189+
/**
190+
* @type {EventEmitterReferencingAsyncResource}
191+
*/
192+
get asyncResource() {
193+
if (this[kAsyncResource] === undefined)
194+
throw new ERR_INVALID_THIS('EventEmitterAsyncResource');
195+
return this[kAsyncResource];
196+
}
197+
};
198+
}
199+
return EventEmitterAsyncResource;
200+
}
201+
79202
/**
80203
* Creates a new `EventEmitter` instance.
81204
* @param {{ captureRejections?: boolean; }} [opts]
@@ -106,6 +229,13 @@ ObjectDefineProperty(EventEmitter, 'captureRejections', {
106229
enumerable: true
107230
});
108231

232+
ObjectDefineProperty(EventEmitter, 'EventEmitterAsyncResource', {
233+
enumerable: true,
234+
get: lazyEventEmitterAsyncResource,
235+
set: undefined,
236+
configurable: true,
237+
});
238+
109239
EventEmitter.errorMonitor = kErrorMonitor;
110240

111241
// The default for captureRejections is false

0 commit comments

Comments
 (0)