events: initial implementation of experimental EventTarget

See documentation changes for details

Signed-off-by: James M Snell <jasnell@gmail.com>

PR-URL: https://github.com/nodejs/node/pull/33556
Refs: https://github.com/nodejs/node/pull/33527
Reviewed-By: Benjamin Gruenbaum <benjamingr@gmail.com>
Reviewed-By: Matteo Collina <matteo.collina@gmail.com>
Reviewed-By: Bradley Farias <bradley.meck@gmail.com>
This commit is contained in:
James M Snell committed 2020-05-28 06:31:58 -07:00
1 parent 47044a91c6
commit 785842a053
8 files changed
+1316

No files matched your search

+24
View File
@@ -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);
}
+5
View File
@@ -903,6 +903,11 @@ Encoding provided to `TextDecoder()` API was not one of the
`--print` cannot be used with ESM input.
<a id="ERR_EVENT_RECURSION"></a>
### `ERR_EVENT_RECURSION`
Thrown when an attempt is made to recursively dispatch an event on `EventTarget`.
<a id="ERR_EXECUTION_ENVIRONMENT_NOT_AVAILABLE"></a>
### `ERR_EXECUTION_ENVIRONMENT_NOT_AVAILABLE`
+459
View File
@@ -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
<!-- YAML
added: REPLACEME
-->
> 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`
<!-- YAML
added: REPLACEME
-->
The `Event` object is an adaptation of the [`Event` Web API][]. Instances
are created internally by Node.js.
#### `event.bubbles`
<!-- YAML
added: REPLACEME
-->
* Type: {boolean} Always returns `false`.
This is not used in Node.js and is provided purely for completeness.
#### `event.cancelBubble()`
<!-- YAML
added: REPLACEME
-->
Alias for `event.stopPropagation()`. This is not used in Node.js and is
provided purely for completeness.
#### `event.cancelable`
<!-- YAML
added: REPLACEME
-->
* Type: {boolean} True if the event was created with the `cancelable` option.
#### `event.composed`
<!-- YAML
added: REPLACEME
-->
* Type: {boolean} Always returns `false`.
This is not used in Node.js and is provided purely for completeness.
#### `event.composedPath()`
<!-- YAML
added: REPLACEME
-->
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`
<!-- YAML
added: REPLACEME
-->
* Type: {EventTarget} The `EventTarget` currently dispatching the event.
Alias for `event.target`.
#### `event.defaultPrevented`
<!-- YAML
added: REPLACEME
-->
* Type: {boolean}
Will be `true` if `cancelable` is `true` and `event.preventDefault()` has been
called.
#### `event.eventPhase`
<!-- YAML
added: REPLACEME
-->
* 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`
<!-- YAML
added: REPLACEME
-->
* Type: {boolean} Always returns `false`.
This is not used in Node.js and is provided purely for completeness.
#### `event.preventDefault()`
<!-- YAML
added: REPLACEME
-->
Sets the `defaultPrevented` property to `true` if `cancelable` is `true`.
#### `event.returnValue`
<!-- YAML
added: REPLACEME
-->
* 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`
<!-- YAML
added: REPLACEME
-->
* Type: {EventTarget} The `EventTarget` currently dispatching the event.
Alias for `event.target`.
#### `event.stopImmediatePropagation()`
<!-- YAML
added: REPLACEME
-->
Stops the invocation of event listeners after the current one completes.
#### `event.stopPropagation()`
<!-- YAML
added: REPLACEME
-->
This is not used in Node.js and is provided purely for completeness.
#### `event.target`
<!-- YAML
added: REPLACEME
-->
* Type: {EventTarget} The `EventTarget` currently dispatching the event.
#### `event.timeStamp`
<!-- YAML
added: REPLACEME
-->
* Type: {number}
The millisecond timestamp when the `Event` object was created.
#### `event.type`
<!-- YAML
added: REPLACEME
-->
* Type: {string}
The event type identifier.
### Class: `EventTarget`
<!-- YAML
added: REPLACEME
-->
#### `eventTarget.addEventListener(type, listener[, options])`
<!-- YAML
added: REPLACEME
-->
* `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)`
<!-- YAML
added: REPLACEME
-->
* `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)`
<!-- YAML
added: REPLACEME
-->
* `type` {string}
* `listener` {Function|EventListener}
* `options` {Object}
* `capture` {boolean}
Removes the `listener` from the list of handlers for event `type`.
### Class: `NodeEventTarget extends EventTarget`
<!-- YAML
added: REPLACEME
-->
The `NodeEventTarget` is a Node.js-specific extension to `EventTarget`
that emulates a subset of the `EventEmitter` API.
#### `nodeEventTarget.addListener(type, listener[, options])`
<!-- YAML
added: REPLACEME
-->
* `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()`
<!-- YAML
added: REPLACEME
-->
* 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)`
<!-- YAML
added: REPLACEME
-->
* `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)`
<!-- YAML
added: REPLACEME
-->
* `type` {string}
* `listener` {Function|EventListener}
* Returns: {EventTarget} this
Node.js-speciic alias for `eventTarget.removeListener()`.
#### `nodeEventTarget.on(type, listener[, options])`
<!-- YAML
added: REPLACEME
-->
* `type` {string}
* `listener` {Function|EventListener}
* `options` {Object}
* `once` {boolean}
* Returns: {EventTarget} this
Node.js-specific alias for `eventTarget.addListener()`.
#### `nodeEventTarget.once(type, listener[, options])`
<!-- YAML
added: REPLACEME
-->
* `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])`
<!-- YAML
added: REPLACEME
-->
* `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)`
<!-- YAML
added: REPLACEME
-->
* `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
+1
View File
@@ -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';
+434
View File
@@ -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,
};
+1
View File
@@ -130,6 +130,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',
+386
View File
@@ -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'));
}
+6
View File
@@ -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',