diff --git a/benchmark/events/eventtarget.js b/benchmark/events/eventtarget.js
new file mode 100644
index 00000000000000..7a7253aefb1347
--- /dev/null
+++ b/benchmark/events/eventtarget.js
@@ -0,0 +1,24 @@
+'use strict';
+const common = require('../common.js');
+
+const bench = common.createBenchmark(main, {
+ n: [2e7],
+ listeners: [1, 5, 10]
+}, { flags: ['--expose-internals'] });
+
+function main({ n, listeners }) {
+ const { EventTarget, Event } = require('internal/event_target');
+ const target = new EventTarget();
+
+ for (let n = 0; n < listeners; n++)
+ target.addEventListener('foo', () => {});
+
+ const event = new Event('foo');
+
+ bench.start();
+ for (let i = 0; i < n; i++) {
+ target.dispatchEvent(event);
+ }
+ bench.end(n);
+
+}
diff --git a/doc/api/errors.md b/doc/api/errors.md
index c322ddb1b5a51b..ff585fda03e01b 100644
--- a/doc/api/errors.md
+++ b/doc/api/errors.md
@@ -903,6 +903,11 @@ Encoding provided to `TextDecoder()` API was not one of the
`--print` cannot be used with ESM input.
+
+### `ERR_EVENT_RECURSION`
+
+Thrown when an attempt is made to recursively dispatch an event on `EventTarget`.
+
### `ERR_EXECUTION_ENVIRONMENT_NOT_AVAILABLE`
diff --git a/doc/api/events.md b/doc/api/events.md
index a30e2abb01353c..ed13d3f5e84757 100644
--- a/doc/api/events.md
+++ b/doc/api/events.md
@@ -935,6 +935,462 @@ if the `EventEmitter` emits `'error'`. It removes all listeners when
exiting the loop. The `value` returned by each iteration is an array
composed of the emitted event arguments.
+## `EventTarget` and `Event` API
+
+
+> Stability: 1 - Experimental
+
+The `EventTarget` and `Event` objects are a Node.js-specific implementation
+of the [`EventTarget` Web API][] that are exposed by some Node.js core APIs.
+Neither the `EventTarget` nor `Event` classes are currently available for end
+user code to create.
+
+```js
+const target = getEventTargetSomehow();
+
+target.addEventListener('foo', (event) => {
+ console.log('foo event happened!');
+});
+```
+
+### Node.js `EventTarget` vs. DOM `EventTarget`
+
+There are two key differences between the Node.js `EventTarget` and the
+[`EventTarget` Web API][]:
+
+1. Whereas DOM `EventTarget` instances *may* be hierarchical, there is no
+ concept of hierarchy and event propagation in Node.js. That is, an event
+ dispatched to an `EventTarget` does not propagate through a hierarchy of
+ nested target objects that may each have their own set of handlers for the
+ event.
+2. In the Node.js `EventTarget`, if an event listener is an async function
+ or returns a `Promise`, and the returned `Promise` rejects, the rejection
+ will be automatically captured and handled the same way as a listener that
+ throws synchronously (see [`EventTarget` Error Handling][] for details).
+
+### `NodeEventTarget` vs. `EventEmitter`
+
+The `NodeEventTarget` object implements a modified subset of the
+`EventEmitter` API that allows it to closely *emulate* an `EventEmitter` in
+certain situations. It is important to understand, however, that an
+`NodeEventTarget` is *not* an instance of `EventEmitter` and cannot be used in
+place of an `EventEmitter` in most cases.
+
+1. Unlike `EventEmitter`, any given `listener` can be registered at most once
+ per event `type`. Attempts to register a `listener` multiple times will be
+ ignored.
+2. The `NodeEventTarget` does not emulate the full `EventEmitter` API.
+ Specifically the `prependListener()`, `prependOnceListener()`,
+ `rawListeners()`, `setMaxListeners()`, `getMaxListeners()`, and
+ `errorMonitor` APIs are not emulated. The `'newListener'` and
+ `'removeListener'` events will also not be emitted.
+3. The `NodeEventTarget` does not implement any special default behavior
+ for events with type `'error'`.
+3. The `NodeEventTarget` supports `EventListener` objects as well as
+ functions as handlers for all event types.
+
+### Event Listener
+
+Event listeners registered for an event `type` may either be JavaScript
+functions or objects with a `handleEvent` property whose value is a function.
+
+In either case, the handler function will be invoked with the `event` argument
+passed to the `eventTarget.dispatchEvent()` function.
+
+Async functions may be used as event listeners. If an async handler function
+rejects, the rejection will be captured and be will handled as described in
+[`EventTarget` Error Handling][].
+
+An error thrown by one handler function will not prevent the other handlers
+from being invoked.
+
+The return value of a handler function will be ignored.
+
+Handlers are always invoked in the order they were added.
+
+Handler functions may mutate the `event` object.
+
+```js
+function handler1(event) {
+ console.log(event.type); // Prints 'foo'
+ event.a = 1;
+}
+
+async function handler2(event) {
+ console.log(event.type); // Prints 'foo'
+ console.log(event.a); // Prints 1
+}
+
+const handler3 = {
+ handleEvent(event) {
+ console.log(event.type); // Prints 'foo'
+ }
+};
+
+const handler4 = {
+ async handleEvent(event) {
+ console.log(event.type); // Prints 'foo'
+ }
+};
+
+const target = getEventTargetSomehow();
+
+target.addEventListener('foo', handler1);
+target.addEventListener('foo', handler2);
+target.addEventListener('foo', handler3);
+target.addEventListener('foo', handler4, { once: true });
+```
+
+### `EventTarget` Error Handling
+
+When a registered event listener throws (or returns a Promise that rejects),
+by default the error will be forwarded to the `process.on('error')` event
+on `process.nextTick()`. Throwing within an event listener will *not* stop
+the other registered handlers from being invoked.
+
+The `EventTarget` does not implement any special default handling for
+`'error'` type events.
+
+### Class: `Event`
+
+
+The `Event` object is an adaptation of the [`Event` Web API][]. Instances
+are created internally by Node.js.
+
+#### `event.bubbles`
+
+
+* Type: {boolean} Always returns `false`.
+
+This is not used in Node.js and is provided purely for completeness.
+
+#### `event.cancelBubble()`
+
+
+Alias for `event.stopPropagation()`. This is not used in Node.js and is
+provided purely for completeness.
+
+#### `event.cancelable`
+
+
+* Type: {boolean} True if the event was created with the `cancelable` option.
+
+#### `event.composed`
+
+
+* Type: {boolean} Always returns `false`.
+
+This is not used in Node.js and is provided purely for completeness.
+
+#### `event.composedPath()`
+
+
+Returns an array containing the current `EventTarget` as the only entry or
+empty if the event is not currently being dispatched. This is not used in
+Node.js and is provided purely for completeness.
+
+#### `event.currentTarget`
+
+
+* Type: {EventTarget} The `EventTarget` currently dispatching the event.
+
+Alias for `event.target`.
+
+#### `event.defaultPrevented`
+
+
+* Type: {boolean}
+
+Will be `true` if `cancelable` is `true` and `event.preventDefault()` has been
+called.
+
+#### `event.eventPhase`
+
+
+* Type: {number} Returns `0` while an event is not being dispatched, `2` while
+ it is being dispatched.
+
+This is not used in Node.js and is provided purely for completeness.
+
+#### `event.isTrusted`
+
+
+* Type: {boolean} Always returns `false`.
+
+This is not used in Node.js and is provided purely for completeness.
+
+#### `event.preventDefault()`
+
+
+Sets the `defaultPrevented` property to `true` if `cancelable` is `true`.
+
+#### `event.returnValue`
+
+
+* Type: {boolean} True if the event has not been canceled.
+
+This is not used in Node.js and is provided purely for completeness.
+
+#### `event.srcElement`
+
+
+* Type: {EventTarget} The `EventTarget` currently dispatching the event.
+
+Alias for `event.target`.
+
+#### `event.stopImmediatePropagation()`
+
+
+Stops the invocation of event listeners after the current one completes.
+
+#### `event.stopPropagation()`
+
+
+This is not used in Node.js and is provided purely for completeness.
+
+#### `event.target`
+
+
+* Type: {EventTarget} The `EventTarget` currently dispatching the event.
+
+#### `event.timeStamp`
+
+
+* Type: {number}
+
+The millisecond timestamp when the `Event` object was created.
+
+#### `event.type`
+
+
+* Type: {string}
+
+The event type identifier.
+
+### Class: `EventTarget`
+
+
+#### `eventTarget.addEventListener(type, listener[, options])`
+
+
+* `type` {string}
+* `listener` {Function|EventListener}
+* `options` {Object}
+ * `once` {boolean} When `true`, the listener will be automatically removed
+ when it is first invoked. *Default*: `false`
+ * `passive` {boolean} When `true`, serves as a hint that the listener will
+ not call the `Event` object's `preventDefault()` method. *Default*: `false`
+ * `capture` {boolean} Not directly used by Node.js. Added for API
+ completeness. *Default*: `false`
+
+Adds a new handler for the `type` event. Any given `listener` will be added
+only once per `type` and per `capture` option value.
+
+If the `once` option is `true`, the `listener` will be removed after the
+next time a `type` event is dispatched.
+
+The `capture` option is not used by Node.js in any functional way other than
+tracking registered event listeners per the `EventTarget` specification.
+Specifically, the `capture` option is used as part of the key when registering
+a `listener`. Any individual `listener` may be added once with
+`capture = false`, and once with `capture = true`.
+
+```js
+function handler(event) {}
+
+const target = getEventTargetSomehow();
+target.addEventListener('foo', handler, { capture: true }); // first
+target.addEventListener('foo', handler, { capture: false }); // second
+
+// Removes the second instance of handler
+target.removeEventListener('foo', handler);
+
+// Removes the first instance of handler
+target.removeEventListener('foo', handler, { capture: true });
+```
+
+#### `eventTarget.dispatchEvent(event)`
+
+
+* `event` {Object|Event}
+
+Dispatches the `event` to the list of handlers for `event.type`. The `event`
+may be an `Event` object or any object with a `type` property whose value is
+a `string`.
+
+The registered event listeners will be synchronously invoked in the order they
+were registered.
+
+#### `eventTarget.removeEventListener(type, listener)`
+
+
+* `type` {string}
+* `listener` {Function|EventListener}
+* `options` {Object}
+ * `capture` {boolean}
+
+Removes the `listener` from the list of handlers for event `type`.
+
+### Class: `NodeEventTarget extends EventTarget`
+
+
+The `NodeEventTarget` is a Node.js-specific extension to `EventTarget`
+that emulates a subset of the `EventEmitter` API.
+
+#### `nodeEventTarget.addListener(type, listener[, options])`
+
+
+* `type` {string}
+* `listener` {Function|EventListener}
+* `options` {Object}
+ * `once` {boolean}
+
+* Returns: {EventTarget} this
+
+Node.js-specific extension to the `EventTarget` class that emulates the
+equivalent `EventEmitter` API. The only difference between `addListener()` and
+`addEventListener()` is that `addListener()` will return a reference to the
+`EventTarget`.
+
+#### `nodeEventTarget.eventNames()`
+
+
+* Returns: {string[]}
+
+Node.js-specific extension to the `EventTarget` class that returns an array
+of event `type` names for which event listeners are currently registered.
+
+#### `nodeEventTarget.listenerCount(type)`
+
+
+* `type` {string}
+
+* Returns: {number}
+
+Node.js-specific extension to the `EventTarget` class that returns the number
+of event listeners registered for the `type`.
+
+#### `nodeEventTarget.off(type, listener)`
+
+
+* `type` {string}
+* `listener` {Function|EventListener}
+
+* Returns: {EventTarget} this
+
+Node.js-speciic alias for `eventTarget.removeListener()`.
+
+#### `nodeEventTarget.on(type, listener[, options])`
+
+
+* `type` {string}
+* `listener` {Function|EventListener}
+* `options` {Object}
+ * `once` {boolean}
+
+* Returns: {EventTarget} this
+
+Node.js-specific alias for `eventTarget.addListener()`.
+
+#### `nodeEventTarget.once(type, listener[, options])`
+
+
+* `type` {string}
+* `listener` {Function|EventListener}
+* `options` {Object}
+
+* Returns: {EventTarget} this
+
+Node.js-specific extension to the `EventTarget` class that adds a `once`
+listener for the given event `type`. This is equivalent to calling `on`
+with the `once` option set to `true`.
+
+#### `nodeEventTarget.removeAllListeners([type])`
+
+
+* `type` {string}
+
+Node.js-specific extension to the `EventTarget` class. If `type` is specified,
+removes all registered listeners for `type`, otherwise removes all registered
+listeners.
+
+#### `nodeEventTarget.removeListener(type, listener)`
+
+
+* `type` {string}
+* `listener` {Function|EventListener}
+
+* Returns: {EventTarget} this
+
+Node.js-specific extension to the `EventTarget` class that removes the
+`listener` for the given `type`. The only difference between `removeListener()`
+and `removeEventListener()` is that `removeListener()` will return a reference
+to the `EventTarget`.
+
[WHATWG-EventTarget]: https://dom.spec.whatwg.org/#interface-eventtarget
[`--trace-warnings`]: cli.html#cli_trace_warnings
[`EventEmitter.defaultMaxListeners`]: #events_eventemitter_defaultmaxlisteners
@@ -942,6 +1398,9 @@ composed of the emitted event arguments.
[`emitter.listenerCount()`]: #events_emitter_listenercount_eventname
[`emitter.removeListener()`]: #events_emitter_removelistener_eventname_listener
[`emitter.setMaxListeners(n)`]: #events_emitter_setmaxlisteners_n
+[`Event` Web API]: https://dom.spec.whatwg.org/#event
+[`EventTarget` Error Handling]: #events_eventtarget_error_handling
+[`EventTarget` Web API]: https://dom.spec.whatwg.org/#eventtarget
[`fs.ReadStream`]: fs.html#fs_class_fs_readstream
[`net.Server`]: net.html#net_class_net_server
[`process.on('warning')`]: process.html#process_event_warning
diff --git a/lib/internal/errors.js b/lib/internal/errors.js
index b1c434e25c3f1d..b4a6b0858b567b 100644
--- a/lib/internal/errors.js
+++ b/lib/internal/errors.js
@@ -825,6 +825,7 @@ E('ERR_ENCODING_INVALID_ENCODED_DATA', function(encoding, ret) {
E('ERR_ENCODING_NOT_SUPPORTED', 'The "%s" encoding is not supported',
RangeError);
E('ERR_EVAL_ESM_CANNOT_PRINT', '--print cannot be used with ESM input', Error);
+E('ERR_EVENT_RECURSION', 'The event "%s" is already being dispatched', Error);
E('ERR_FALSY_VALUE_REJECTION', function(reason) {
this.reason = reason;
return 'Promise was rejected with falsy value';
diff --git a/lib/internal/event_target.js b/lib/internal/event_target.js
new file mode 100644
index 00000000000000..4cc561f95c0bba
--- /dev/null
+++ b/lib/internal/event_target.js
@@ -0,0 +1,434 @@
+'use strict';
+
+const {
+ ArrayFrom,
+ Error,
+ Map,
+ Object,
+ Set,
+ Symbol,
+ NumberIsNaN,
+} = primordials;
+
+const {
+ codes: {
+ ERR_INVALID_ARG_TYPE,
+ ERR_EVENT_RECURSION,
+ ERR_OUT_OF_RANGE,
+ }
+} = require('internal/errors');
+
+const perf_hooks = require('perf_hooks');
+const { customInspectSymbol } = require('internal/util');
+const { inspect } = require('util');
+
+const kEvents = Symbol('kEvents');
+const kStop = Symbol('kStop');
+const kTarget = Symbol('kTarget');
+
+const kNewListener = Symbol('kNewListener');
+const kRemoveListener = Symbol('kRemoveListener');
+
+class Event {
+ #type = undefined;
+ #defaultPrevented = false;
+ #cancelable = false;
+ #timestamp = perf_hooks.performance.now();
+
+ // Neither of these are currently used in the Node.js implementation
+ // of EventTarget because there is no concept of bubbling or
+ // composition. We preserve their values in Event but they are
+ // non-ops and do not carry any semantics in Node.js
+ #bubbles = false;
+ #composed = false;
+
+
+ constructor(type, options) {
+ if (options != null && typeof options !== 'object')
+ throw new ERR_INVALID_ARG_TYPE('options', 'object', options);
+ const { cancelable, bubbles, composed } = { ...options };
+ this.#cancelable = !!cancelable;
+ this.#bubbles = !!bubbles;
+ this.#composed = !!composed;
+ this.#type = String(type);
+ }
+
+ [customInspectSymbol](depth, options) {
+ const name = this.constructor.name;
+ if (depth < 0)
+ return name;
+
+ const opts = Object.assign({}, options, {
+ dept: options.depth === null ? null : options.depth - 1
+ });
+
+ return `${name} ${inspect({
+ type: this.#type,
+ defaultPrevented: this.#defaultPrevented,
+ cancelable: this.#cancelable,
+ timeStamp: this.#timestamp,
+ }, opts)}`;
+ }
+
+ stopImmediatePropagation() {
+ this[kStop] = true;
+ }
+
+ preventDefault() {
+ this.#defaultPrevented = true;
+ }
+
+ get target() { return this[kTarget]; }
+ get currentTarget() { return this[kTarget]; }
+ get srcElement() { return this[kTarget]; }
+
+ get type() { return this.#type; }
+
+ get cancelable() { return this.#cancelable; }
+
+ get defaultPrevented() { return this.#cancelable && this.#defaultPrevented; }
+
+ get timeStamp() { return this.#timestamp; }
+
+
+ // The following are non-op and unused properties/methods from Web API Event.
+ // These are not supported in Node.js and are provided purely for
+ // API completeness.
+
+ composedPath() { return this[kTarget] ? [this[kTarget]] : []; }
+ get returnValue() { return !this.defaultPrevented; }
+ get bubbles() { return this.#bubbles; }
+ get composed() { return this.#composed; }
+ get isTrusted() { return false; }
+ get eventPhase() {
+ return this[kTarget] ? 2 : 0; // Equivalent to AT_TARGET or NONE
+ }
+ cancelBubble() {
+ // Non-op in Node.js. Alias for stopPropagation
+ }
+ stopPropagation() {
+ // Non-op in Node.js
+ }
+
+ get [Symbol.toStringTag]() { return 'Event'; }
+}
+
+// The listeners for an EventTarget are maintained as a linked list.
+// Unfortunately, the way EventTarget is defined, listeners are accounted
+// using the tuple [handler,capture], and even if we don't actually make
+// use of capture or bubbling, in order to be spec compliant we have to
+// take on the additional complexity of supporting it. Fortunately, using
+// the linked list makes dispatching faster, even if adding/removing is
+// slower.
+class Listener {
+ next;
+ previous;
+ listener;
+ callback;
+ once;
+ capture;
+ passive;
+
+ constructor(previous, listener, once, capture, passive) {
+ if (previous !== undefined)
+ previous.next = this;
+ this.previous = previous;
+ this.listener = listener;
+ this.once = once;
+ this.capture = capture;
+ this.passive = passive;
+
+ this.callback =
+ typeof listener === 'function' ?
+ listener :
+ listener.handleEvent.bind(listener);
+ }
+
+ same(listener, capture) {
+ return this.listener === listener && this.capture === capture;
+ }
+
+ remove() {
+ if (this.previous !== undefined)
+ this.previous.next = this.next;
+ if (this.next !== undefined)
+ this.next.previous = this.previous;
+ }
+}
+
+class EventTarget {
+ [kEvents] = new Map();
+ #emitting = new Set();
+
+ [kNewListener](size, type, listener, once, capture, passive) {}
+ [kRemoveListener](size, type, listener, capture) {}
+
+ addEventListener(type, listener, options = {}) {
+ validateListener(listener);
+ type = String(type);
+
+ const {
+ once,
+ capture,
+ passive
+ } = validateEventListenerOptions(options);
+
+ let root = this[kEvents].get(type);
+
+ if (root === undefined) {
+ root = { size: 1, next: undefined };
+ // This is the first handler in our linked list.
+ new Listener(root, listener, once, capture, passive);
+ this[kNewListener](root.size, type, listener, once, capture, passive);
+ this[kEvents].set(type, root);
+ return;
+ }
+
+ let handler = root.next;
+ let previous;
+
+ // We have to walk the linked list to see if we have a match
+ while (handler !== undefined && !handler.same(listener, capture)) {
+ previous = handler;
+ handler = handler.next;
+ }
+
+ if (handler !== undefined) { // Duplicate! Ignore
+ return;
+ }
+
+ new Listener(previous, listener, once, capture, passive);
+ root.size++;
+ this[kNewListener](root.size, type, listener, once, capture, passive);
+ }
+
+ removeEventListener(type, listener, options = {}) {
+ validateListener(listener);
+ type = String(type);
+ const { capture } = validateEventListenerOptions(options);
+ const root = this[kEvents].get(type);
+ if (root === undefined || root.next === undefined)
+ return;
+
+ let handler = root.next;
+ while (handler !== undefined) {
+ if (handler.same(listener, capture)) {
+ handler.remove();
+ root.size--;
+ if (root.size === 0)
+ this[kEvents].delete(type);
+ this[kRemoveListener](root.size, type, listener, capture);
+ break;
+ }
+ handler = handler.next;
+ }
+ }
+
+ dispatchEvent(event) {
+ if (!(event instanceof Event)) {
+ throw new ERR_INVALID_ARG_TYPE('event', 'Event', event);
+ }
+
+ if (this.#emitting.has(event.type) ||
+ event[kTarget] !== undefined) {
+ throw new ERR_EVENT_RECURSION(event.type);
+ }
+
+ const root = this[kEvents].get(event.type);
+ if (root === undefined || root.next === undefined)
+ return true;
+
+ event[kTarget] = this;
+ this.#emitting.add(event.type);
+
+ let handler = root.next;
+ let next;
+
+ while (handler !== undefined &&
+ (handler.passive || event[kStop] !== true)) {
+ // Cache the next item in case this iteration removes the current one
+ next = handler.next;
+
+ if (handler.once) {
+ handler.remove();
+ root.size--;
+ }
+
+ try {
+ const result = handler.callback.call(this, event);
+ if (result !== undefined && result !== null)
+ addCatch(this, result, event);
+ } catch (err) {
+ emitUnhandledRejectionOrErr(this, err, event);
+ }
+
+ handler = next;
+ }
+
+ this.#emitting.delete(event.type);
+ event[kTarget] = undefined;
+
+ return event.defaultPrevented === true ? false : true;
+ }
+
+ [customInspectSymbol](depth, options) {
+ const name = this.constructor.name;
+ if (depth < 0)
+ return name;
+
+ const opts = Object.assign({}, options, {
+ dept: options.depth === null ? null : options.depth - 1
+ });
+
+ return `${name} ${inspect({}, opts)}`;
+ }
+}
+
+Object.defineProperties(EventTarget.prototype, {
+ addEventListener: { enumerable: true },
+ removeEventListener: { enumerable: true },
+ dispatchEvent: { enumerable: true }
+});
+
+class NodeEventTarget extends EventTarget {
+ static defaultMaxListeners = 10;
+
+ #maxListeners = NodeEventTarget.defaultMaxListeners;
+ #maxListenersWarned = false;
+
+ [kNewListener](size, type, listener, once, capture, passive) {
+ if (this.#maxListeners > 0 &&
+ size > this.#maxListeners &&
+ !this.#maxListenersWarned) {
+ this.#maxListenersWarned = true;
+ // No error code for this since it is a Warning
+ // eslint-disable-next-line no-restricted-syntax
+ const w = new Error('Possible EventTarget memory leak detected. ' +
+ `${size} ${type} listeners ` +
+ `added to ${inspect(this, { depth: -1 })}. Use ` +
+ 'setMaxListeners() to increase limit');
+ w.name = 'MaxListenersExceededWarning';
+ w.target = this;
+ w.type = type;
+ w.count = size;
+ process.emitWarning(w);
+ }
+ }
+
+ setMaxListeners(n) {
+ if (typeof n !== 'number' || n < 0 || NumberIsNaN(n)) {
+ throw new ERR_OUT_OF_RANGE('n', 'a non-negative number', n);
+ }
+ this.#maxListeners = n;
+ return this;
+ }
+
+ getMaxListeners() {
+ return this.#maxListeners;
+ }
+
+ eventNames() {
+ return ArrayFrom(this[kEvents].keys());
+ }
+
+ listenerCount(type) {
+ const root = this[kEvents].get(String(type));
+ return root !== undefined ? root.size : 0;
+ }
+
+ off(type, listener, options) {
+ this.removeEventListener(type, listener, options);
+ return this;
+ }
+
+ removeListener(type, listener, options) {
+ this.removeEventListener(type, listener, options);
+ return this;
+ }
+
+ on(type, listener) {
+ this.addEventListener(type, listener);
+ return this;
+ }
+
+ addListener(type, listener) {
+ this.addEventListener(type, listener);
+ return this;
+ }
+
+ once(type, listener) {
+ this.addEventListener(type, listener, { once: true });
+ return this;
+ }
+
+ removeAllListeners(type) {
+ if (type !== undefined) {
+ this[kEvents].delete(String(type));
+ } else {
+ this[kEvents].clear();
+ }
+ }
+}
+
+Object.defineProperties(NodeEventTarget.prototype, {
+ setMaxListeners: { enumerable: true },
+ getMaxListeners: { enumerable: true },
+ eventNames: { enumerable: true },
+ listenerCount: { enumerable: true },
+ off: { enumerable: true },
+ removeListener: { enumerable: true },
+ on: { enumerable: true },
+ addListener: { enumerable: true },
+ once: { enumerable: true },
+ removeAllListeners: { enumerable: true },
+});
+
+// EventTarget API
+
+function validateListener(listener) {
+ if (typeof listener === 'function' ||
+ (listener != null &&
+ typeof listener === 'object' &&
+ typeof listener.handleEvent === 'function')) {
+ return;
+ }
+ throw new ERR_INVALID_ARG_TYPE('listener', 'EventListener', listener);
+}
+
+function validateEventListenerOptions(options) {
+ if (options == null || typeof options !== 'object')
+ throw new ERR_INVALID_ARG_TYPE('options', 'object', options);
+ const {
+ once = false,
+ capture = false,
+ passive = false,
+ } = options;
+ return {
+ once: !!once,
+ capture: !!capture,
+ passive: !!passive,
+ };
+}
+
+function addCatch(that, promise, event) {
+ const then = promise.then;
+ if (typeof then === 'function') {
+ then.call(promise, undefined, function(err) {
+ // The callback is called with nextTick to avoid a follow-up
+ // rejection from this promise.
+ process.nextTick(emitUnhandledRejectionOrErr, that, err, event);
+ });
+ }
+}
+
+function emitUnhandledRejectionOrErr(that, err, event) {
+ process.emit('error', err, event);
+}
+
+// EventEmitter-ish API:
+
+module.exports = {
+ Event,
+ EventTarget,
+ NodeEventTarget,
+};
diff --git a/node.gyp b/node.gyp
index 19738496277b26..7b21a5f5b3c982 100644
--- a/node.gyp
+++ b/node.gyp
@@ -129,6 +129,7 @@
'lib/internal/encoding.js',
'lib/internal/errors.js',
'lib/internal/error-serdes.js',
+ 'lib/internal/event_target.js',
'lib/internal/fixed_queue.js',
'lib/internal/freelist.js',
'lib/internal/freeze_intrinsics.js',
diff --git a/test/parallel/test-eventtarget.js b/test/parallel/test-eventtarget.js
new file mode 100644
index 00000000000000..3b44714cfbe2dc
--- /dev/null
+++ b/test/parallel/test-eventtarget.js
@@ -0,0 +1,386 @@
+// Flags: --expose-internals --no-warnings
+'use strict';
+
+const common = require('../common');
+const {
+ Event,
+ EventTarget,
+ NodeEventTarget,
+} = require('internal/event_target');
+
+const {
+ ok,
+ deepStrictEqual,
+ strictEqual,
+ throws,
+} = require('assert');
+
+const { once } = require('events');
+
+// The globals are defined.
+ok(Event);
+ok(EventTarget);
+
+// First, test Event
+{
+ const ev = new Event('foo');
+ strictEqual(ev.type, 'foo');
+ strictEqual(ev.cancelable, false);
+ strictEqual(ev.defaultPrevented, false);
+ strictEqual(typeof ev.timeStamp, 'number');
+
+ deepStrictEqual(ev.composedPath(), []);
+ strictEqual(ev.returnValue, true);
+ strictEqual(ev.bubbles, false);
+ strictEqual(ev.composed, false);
+ strictEqual(ev.isTrusted, false);
+ strictEqual(ev.eventPhase, 0);
+
+ // Not cancelable
+ ev.preventDefault();
+ strictEqual(ev.defaultPrevented, false);
+}
+
+{
+ const ev = new Event('foo', { cancelable: true });
+ strictEqual(ev.type, 'foo');
+ strictEqual(ev.cancelable, true);
+ strictEqual(ev.defaultPrevented, false);
+
+ ev.preventDefault();
+ strictEqual(ev.defaultPrevented, true);
+}
+
+{
+ const eventTarget = new EventTarget();
+
+ const ev1 = common.mustCall(function(event) {
+ strictEqual(event.type, 'foo');
+ strictEqual(this, eventTarget);
+ strictEqual(event.eventPhase, 2);
+ }, 2);
+
+ const ev2 = {
+ handleEvent: common.mustCall(function(event) {
+ strictEqual(event.type, 'foo');
+ strictEqual(this, ev2);
+ })
+ };
+
+ eventTarget.addEventListener('foo', ev1);
+ eventTarget.addEventListener('foo', ev2, { once: true });
+ ok(eventTarget.dispatchEvent(new Event('foo')));
+ eventTarget.dispatchEvent(new Event('foo'));
+
+ eventTarget.removeEventListener('foo', ev1);
+ eventTarget.dispatchEvent(new Event('foo'));
+}
+
+{
+ const eventTarget = new NodeEventTarget();
+ strictEqual(eventTarget.listenerCount('foo'), 0);
+ deepStrictEqual(eventTarget.eventNames(), []);
+
+ const ev1 = common.mustCall(function(event) {
+ strictEqual(event.type, 'foo');
+ strictEqual(this, eventTarget);
+ }, 2);
+
+ const ev2 = {
+ handleEvent: common.mustCall(function(event) {
+ strictEqual(event.type, 'foo');
+ strictEqual(this, ev2);
+ })
+ };
+
+ eventTarget.addEventListener('foo', ev1);
+ eventTarget.addEventListener('foo', ev2, { once: true });
+ strictEqual(eventTarget.listenerCount('foo'), 2);
+ ok(eventTarget.dispatchEvent(new Event('foo')));
+ strictEqual(eventTarget.listenerCount('foo'), 1);
+ eventTarget.dispatchEvent(new Event('foo'));
+
+ eventTarget.removeEventListener('foo', ev1);
+ strictEqual(eventTarget.listenerCount('foo'), 0);
+ eventTarget.dispatchEvent(new Event('foo'));
+}
+
+
+{
+ const eventTarget = new EventTarget();
+ const event = new Event('foo', { cancelable: true });
+ eventTarget.addEventListener('foo', (event) => event.preventDefault());
+ ok(!eventTarget.dispatchEvent(event));
+}
+
+{
+ const eventTarget = new NodeEventTarget();
+ strictEqual(eventTarget.listenerCount('foo'), 0);
+ deepStrictEqual(eventTarget.eventNames(), []);
+
+ const ev1 = common.mustCall((event) => {
+ strictEqual(event.type, 'foo');
+ }, 2);
+
+ const ev2 = {
+ handleEvent: common.mustCall((event) => {
+ strictEqual(event.type, 'foo');
+ })
+ };
+
+ strictEqual(eventTarget.on('foo', ev1), eventTarget);
+ strictEqual(eventTarget.once('foo', ev2, { once: true }), eventTarget);
+ strictEqual(eventTarget.listenerCount('foo'), 2);
+ eventTarget.dispatchEvent(new Event('foo'));
+ strictEqual(eventTarget.listenerCount('foo'), 1);
+ eventTarget.dispatchEvent(new Event('foo'));
+
+ strictEqual(eventTarget.off('foo', ev1), eventTarget);
+ strictEqual(eventTarget.listenerCount('foo'), 0);
+ eventTarget.dispatchEvent(new Event('foo'));
+}
+
+{
+ const eventTarget = new NodeEventTarget();
+ strictEqual(eventTarget.listenerCount('foo'), 0);
+ deepStrictEqual(eventTarget.eventNames(), []);
+
+ const ev1 = common.mustCall((event) => {
+ strictEqual(event.type, 'foo');
+ }, 2);
+
+ const ev2 = {
+ handleEvent: common.mustCall((event) => {
+ strictEqual(event.type, 'foo');
+ })
+ };
+
+ eventTarget.addListener('foo', ev1);
+ eventTarget.once('foo', ev2, { once: true });
+ strictEqual(eventTarget.listenerCount('foo'), 2);
+ eventTarget.dispatchEvent(new Event('foo'));
+ strictEqual(eventTarget.listenerCount('foo'), 1);
+ eventTarget.dispatchEvent(new Event('foo'));
+
+ eventTarget.removeListener('foo', ev1);
+ strictEqual(eventTarget.listenerCount('foo'), 0);
+ eventTarget.dispatchEvent(new Event('foo'));
+}
+
+{
+ const eventTarget = new NodeEventTarget();
+ strictEqual(eventTarget.listenerCount('foo'), 0);
+ deepStrictEqual(eventTarget.eventNames(), []);
+
+ // Won't actually be called.
+ const ev1 = () => {};
+
+ // Won't actually be called.
+ const ev2 = { handleEvent() {} };
+
+ eventTarget.addListener('foo', ev1);
+ eventTarget.addEventListener('foo', ev1);
+ eventTarget.once('foo', ev2, { once: true });
+ eventTarget.once('foo', ev2, { once: false });
+ eventTarget.on('bar', ev1);
+ strictEqual(eventTarget.listenerCount('foo'), 2);
+ strictEqual(eventTarget.listenerCount('bar'), 1);
+ deepStrictEqual(eventTarget.eventNames(), ['foo', 'bar']);
+ eventTarget.removeAllListeners('foo');
+ strictEqual(eventTarget.listenerCount('foo'), 0);
+ strictEqual(eventTarget.listenerCount('bar'), 1);
+ deepStrictEqual(eventTarget.eventNames(), ['bar']);
+ eventTarget.removeAllListeners();
+ strictEqual(eventTarget.listenerCount('foo'), 0);
+ strictEqual(eventTarget.listenerCount('bar'), 0);
+ deepStrictEqual(eventTarget.eventNames(), []);
+}
+
+{
+ const uncaughtException = common.mustCall((err, event) => {
+ strictEqual(err.message, 'boom');
+ strictEqual(event.type, 'foo');
+ }, 4);
+
+ // Whether or not the handler function is async or not, errors
+ // are routed to uncaughtException
+ process.on('error', uncaughtException);
+
+ const eventTarget = new EventTarget();
+
+ const ev1 = async () => { throw new Error('boom'); };
+ const ev2 = () => { throw new Error('boom'); };
+ const ev3 = { handleEvent() { throw new Error('boom'); } };
+ const ev4 = { async handleEvent() { throw new Error('boom'); } };
+
+ // Errors in a handler won't stop calling the others.
+ eventTarget.addEventListener('foo', ev1, { once: true });
+ eventTarget.addEventListener('foo', ev2, { once: true });
+ eventTarget.addEventListener('foo', ev3, { once: true });
+ eventTarget.addEventListener('foo', ev4, { once: true });
+
+ eventTarget.dispatchEvent(new Event('foo'));
+}
+
+{
+ const eventTarget = new EventTarget();
+
+ // Once handler only invoked once
+ const ev = common.mustCall((event) => {
+ throws(() => eventTarget.dispatchEvent(new Event('foo')), {
+ code: 'ERR_EVENT_RECURSION'
+ });
+ });
+
+ // Errors in a handler won't stop calling the others.
+ eventTarget.addEventListener('foo', ev);
+
+ eventTarget.dispatchEvent(new Event('foo'));
+}
+
+{
+ // Coercion to string works
+ strictEqual((new Event(1)).type, '1');
+ strictEqual((new Event(false)).type, 'false');
+ strictEqual((new Event({})).type, String({}));
+
+ const target = new EventTarget();
+
+ [
+ 'foo',
+ {}, // No type event
+ undefined,
+ 1,
+ false
+ ].forEach((i) => {
+ throws(() => target.dispatchEvent(i), {
+ code: 'ERR_INVALID_ARG_TYPE'
+ });
+ });
+
+ [
+ 'foo',
+ 1,
+ {}, // No handleEvent function
+ false,
+ undefined
+ ].forEach((i) => {
+ throws(() => target.addEventListener('foo', i), {
+ code: 'ERR_INVALID_ARG_TYPE'
+ });
+ });
+
+ [
+ 'foo',
+ 1,
+ {}, // No handleEvent function
+ false,
+ undefined
+ ].forEach((i) => {
+ throws(() => target.removeEventListener('foo', i), {
+ code: 'ERR_INVALID_ARG_TYPE'
+ });
+ });
+}
+
+{
+ const target = new EventTarget();
+ once(target, 'foo').then(common.mustCall());
+ target.dispatchEvent(new Event('foo'));
+}
+
+{
+ const target = new NodeEventTarget();
+
+ process.on('warning', common.mustCall((warning) => {
+ ok(warning instanceof Error);
+ strictEqual(warning.name, 'MaxListenersExceededWarning');
+ strictEqual(warning.target, target);
+ strictEqual(warning.count, 2);
+ strictEqual(warning.type, 'foo');
+ ok(warning.message.includes(
+ '2 foo listeners added to NodeEventTarget'));
+ }));
+
+ strictEqual(target.getMaxListeners(), NodeEventTarget.defaultMaxListeners);
+ target.setMaxListeners(1);
+ target.on('foo', () => {});
+ target.on('foo', () => {});
+}
+
+{
+ const target = new EventTarget();
+ const event = new Event('foo');
+ event.stopImmediatePropagation();
+ target.addEventListener('foo', common.mustNotCall());
+ target.dispatchEvent(event);
+}
+
+{
+ const target = new EventTarget();
+ const event = new Event('foo');
+ target.addEventListener('foo', common.mustCall((event) => {
+ event.stopImmediatePropagation();
+ }));
+ target.addEventListener('foo', common.mustNotCall());
+ target.dispatchEvent(event);
+}
+
+{
+ const target = new EventTarget();
+ const event = new Event('foo');
+ target.addEventListener('foo', common.mustCall((event) => {
+ event.stopImmediatePropagation();
+ }));
+ target.addEventListener('foo', common.mustNotCall());
+ target.dispatchEvent(event);
+}
+
+{
+ const target = new EventTarget();
+ const event = new Event('foo');
+ target.addEventListener('foo', common.mustCall((event) => {
+ strictEqual(event.target, target);
+ strictEqual(event.currentTarget, target);
+ strictEqual(event.srcElement, target);
+ }));
+ target.dispatchEvent(event);
+}
+
+{
+ const target1 = new EventTarget();
+ const target2 = new EventTarget();
+ const event = new Event('foo');
+ target1.addEventListener('foo', common.mustCall((event) => {
+ throws(() => target2.dispatchEvent(event), {
+ code: 'ERR_EVENT_RECURSION'
+ });
+ }));
+ target1.dispatchEvent(event);
+}
+
+{
+ const target = new EventTarget();
+ const a = common.mustCall(() => target.removeEventListener('foo', a));
+ const b = common.mustCall(2);
+
+ target.addEventListener('foo', a);
+ target.addEventListener('foo', b);
+
+ target.dispatchEvent(new Event('foo'));
+ target.dispatchEvent(new Event('foo'));
+}
+
+{
+ const target = new EventTarget();
+ const a = common.mustCall(3);
+
+ target.addEventListener('foo', a, { capture: true });
+ target.addEventListener('foo', a, { capture: false });
+
+ target.dispatchEvent(new Event('foo'));
+ target.removeEventListener('foo', a, { capture: true });
+ target.dispatchEvent(new Event('foo'));
+ target.removeEventListener('foo', a, { capture: false });
+ target.dispatchEvent(new Event('foo'));
+}
diff --git a/tools/doc/type-parser.js b/tools/doc/type-parser.js
index f35abb52f65e47..ec6c8e54ef8410 100644
--- a/tools/doc/type-parser.js
+++ b/tools/doc/type-parser.js
@@ -77,6 +77,9 @@ const customTypesMap = {
'import.meta': 'esm.html#esm_import_meta',
'EventEmitter': 'events.html#events_class_eventemitter',
+ 'EventTarget': 'events.html#events_class_eventtarget',
+ 'Event': 'events.html#events_class_event',
+ 'EventListener': 'events.html#events_event_listener',
'FileHandle': 'fs.html#fs_class_filehandle',
'fs.Dir': 'fs.html#fs_class_fs_dir',
@@ -119,6 +122,9 @@ const customTypesMap = {
'net.Server': 'net.html#net_class_net_server',
'net.Socket': 'net.html#net_class_net_socket',
+ 'NodeEventTarget':
+ 'events.html#events_class_nodeeventtarget_extends_eventtarget',
+
'os.constants.dlopen': 'os.html#os_dlopen_constants',
'Histogram': 'perf_hooks.html#perf_hooks_class_histogram',