node/lib/internal/event_target.js
Robert Nagy 488ce99d76
events: optimize EventTarget.addEventListener
PR-URL: https://github.com/nodejs/node/pull/55312
Fixes: https://github.com/nodejs/node/issues/55311
Reviewed-By: Stephen Belanger <admin@stephenbelanger.com>
Reviewed-By: Vinícius Lourenço Claro Cardoso <contact@viniciusl.com.br>
Reviewed-By: Yagiz Nizipli <yagiz@nizipli.com>
Reviewed-By: Matteo Collina <matteo.collina@gmail.com>
Reviewed-By: Ruben Bridgewater <ruben@bridgewater.de>
Reviewed-By: Antoine du Hamel <duhamelantoine1995@gmail.com>
Reviewed-By: Benjamin Gruenbaum <benjamingr@gmail.com>
2024-10-14 10:24:32 +00:00

1181 lines
31 KiB
JavaScript

'use strict';
const {
ArrayFrom,
ArrayPrototypeReduce,
Boolean,
Error,
FunctionPrototypeCall,
NumberIsInteger,
ObjectAssign,
ObjectDefineProperties,
ObjectDefineProperty,
ObjectGetOwnPropertyDescriptor,
ReflectApply,
SafeFinalizationRegistry,
SafeMap,
SafeWeakMap,
SafeWeakRef,
SafeWeakSet,
String,
Symbol,
SymbolFor,
SymbolToStringTag,
} = primordials;
const {
codes: {
ERR_EVENT_RECURSION,
ERR_INVALID_ARG_TYPE,
ERR_INVALID_THIS,
ERR_MISSING_ARGS,
},
} = require('internal/errors');
const {
validateAbortSignal,
validateObject,
validateString,
validateInternalField,
kValidateObjectAllowObjects,
} = require('internal/validators');
const {
customInspectSymbol,
kEmptyObject,
kEnumerableProperty,
} = require('internal/util');
const { inspect } = require('util');
const webidl = require('internal/webidl');
const kIsEventTarget = SymbolFor('nodejs.event_target');
const kIsNodeEventTarget = Symbol('kIsNodeEventTarget');
const EventEmitter = require('events');
const {
kMaxEventTargetListeners,
kMaxEventTargetListenersWarned,
} = EventEmitter;
const kEvents = Symbol('kEvents');
const kIsBeingDispatched = Symbol('kIsBeingDispatched');
const kStop = Symbol('kStop');
const kTarget = Symbol('kTarget');
const kHandlers = Symbol('kHandlers');
const kWeakHandler = Symbol('kWeak');
const kResistStopPropagation = Symbol('kResistStopPropagation');
const kHybridDispatch = SymbolFor('nodejs.internal.kHybridDispatch');
const kRemoveWeakListenerHelper = Symbol('kRemoveWeakListenerHelper');
const kCreateEvent = Symbol('kCreateEvent');
const kNewListener = Symbol('kNewListener');
const kRemoveListener = Symbol('kRemoveListener');
const kIsNodeStyleListener = Symbol('kIsNodeStyleListener');
const kTrustEvent = Symbol('kTrustEvent');
const { now } = require('internal/perf/utils');
const kType = Symbol('type');
const kDetail = Symbol('detail');
const isTrustedSet = new SafeWeakSet();
const isTrusted = ObjectGetOwnPropertyDescriptor({
get isTrusted() {
return isTrustedSet.has(this);
},
}, 'isTrusted').get;
const isTrustedDescriptor = {
__proto__: null,
configurable: false,
enumerable: true,
get: isTrusted,
};
function isEvent(value) {
return typeof value?.[kType] === 'string';
}
class Event {
#cancelable = false;
#bubbles = false;
#composed = false;
#defaultPrevented = false;
#timestamp = now();
#propagationStopped = false;
/**
* @param {string} type
* @param {{
* bubbles?: boolean,
* cancelable?: boolean,
* composed?: boolean,
* }} [options]
*/
constructor(type, options = undefined) {
if (arguments.length === 0)
throw new ERR_MISSING_ARGS('type');
if (options != null)
validateObject(options, 'options');
this.#bubbles = !!options?.bubbles;
this.#cancelable = !!options?.cancelable;
this.#composed = !!options?.composed;
this[kType] = `${type}`;
if (options?.[kTrustEvent]) {
isTrustedSet.add(this);
}
this[kTarget] = null;
this[kIsBeingDispatched] = false;
}
/**
* @param {string} type
* @param {boolean} [bubbles]
* @param {boolean} [cancelable]
*/
initEvent(type, bubbles = false, cancelable = false) {
if (arguments.length === 0)
throw new ERR_MISSING_ARGS('type');
if (this[kIsBeingDispatched]) {
return;
}
this[kType] = `${type}`;
this.#bubbles = !!bubbles;
this.#cancelable = !!cancelable;
}
[customInspectSymbol](depth, options) {
if (!isEvent(this))
throw new ERR_INVALID_THIS('Event');
const name = this.constructor.name;
if (depth < 0)
return name;
const opts = ObjectAssign({}, options, {
depth: NumberIsInteger(options.depth) ? options.depth - 1 : options.depth,
});
return `${name} ${inspect({
type: this[kType],
defaultPrevented: this.#defaultPrevented,
cancelable: this.#cancelable,
timeStamp: this.#timestamp,
}, opts)}`;
}
stopImmediatePropagation() {
if (!isEvent(this))
throw new ERR_INVALID_THIS('Event');
// Spec mention "stopImmediatePropagation should set both "stop propagation"
// and "stop immediate propagation" flags"
// cf: from https://dom.spec.whatwg.org/#dom-event-stopimmediatepropagation
this.stopPropagation();
this[kStop] = true;
}
preventDefault() {
if (!isEvent(this))
throw new ERR_INVALID_THIS('Event');
this.#defaultPrevented = true;
}
/**
* @type {EventTarget}
*/
get target() {
if (!isEvent(this))
throw new ERR_INVALID_THIS('Event');
return this[kTarget];
}
/**
* @type {EventTarget}
*/
get currentTarget() {
if (!isEvent(this))
throw new ERR_INVALID_THIS('Event');
return this[kIsBeingDispatched] ? this[kTarget] : null;
}
/**
* @type {EventTarget}
*/
get srcElement() {
if (!isEvent(this))
throw new ERR_INVALID_THIS('Event');
return this[kTarget];
}
/**
* @type {string}
*/
get type() {
if (!isEvent(this))
throw new ERR_INVALID_THIS('Event');
return this[kType];
}
/**
* @type {boolean}
*/
get cancelable() {
if (!isEvent(this))
throw new ERR_INVALID_THIS('Event');
return this.#cancelable;
}
/**
* @type {boolean}
*/
get defaultPrevented() {
if (!isEvent(this))
throw new ERR_INVALID_THIS('Event');
return this.#cancelable && this.#defaultPrevented;
}
/**
* @type {number}
*/
get timeStamp() {
if (!isEvent(this))
throw new ERR_INVALID_THIS('Event');
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.
/**
* @returns {EventTarget[]}
*/
composedPath() {
if (!isEvent(this))
throw new ERR_INVALID_THIS('Event');
return this[kIsBeingDispatched] ? [this[kTarget]] : [];
}
/**
* @type {boolean}
*/
get returnValue() {
if (!isEvent(this))
throw new ERR_INVALID_THIS('Event');
return !this.#cancelable || !this.#defaultPrevented;
}
/**
* @type {boolean}
*/
get bubbles() {
if (!isEvent(this))
throw new ERR_INVALID_THIS('Event');
return this.#bubbles;
}
/**
* @type {boolean}
*/
get composed() {
if (!isEvent(this))
throw new ERR_INVALID_THIS('Event');
return this.#composed;
}
/**
* @type {number}
*/
get eventPhase() {
if (!isEvent(this))
throw new ERR_INVALID_THIS('Event');
return this[kIsBeingDispatched] ? Event.AT_TARGET : Event.NONE;
}
/**
* @type {boolean}
*/
get cancelBubble() {
if (!isEvent(this))
throw new ERR_INVALID_THIS('Event');
return this.#propagationStopped;
}
/**
* @type {boolean}
*/
set cancelBubble(value) {
if (!isEvent(this))
throw new ERR_INVALID_THIS('Event');
if (value) {
this.#propagationStopped = true;
}
}
stopPropagation() {
if (!isEvent(this))
throw new ERR_INVALID_THIS('Event');
this.#propagationStopped = true;
}
}
ObjectDefineProperties(
Event.prototype, {
[SymbolToStringTag]: {
__proto__: null,
writable: false,
enumerable: false,
configurable: true,
value: 'Event',
},
initEvent: kEnumerableProperty,
stopImmediatePropagation: kEnumerableProperty,
preventDefault: kEnumerableProperty,
target: kEnumerableProperty,
currentTarget: kEnumerableProperty,
srcElement: kEnumerableProperty,
type: kEnumerableProperty,
cancelable: kEnumerableProperty,
defaultPrevented: kEnumerableProperty,
timeStamp: kEnumerableProperty,
composedPath: kEnumerableProperty,
returnValue: kEnumerableProperty,
bubbles: kEnumerableProperty,
composed: kEnumerableProperty,
eventPhase: kEnumerableProperty,
cancelBubble: kEnumerableProperty,
stopPropagation: kEnumerableProperty,
// Don't conform to the spec with isTrusted. The spec defines it as
// LegacyUnforgeable but defining it in the constructor has a big
// performance impact and the property doesn't seem to be useful outside of
// browsers.
isTrusted: isTrustedDescriptor,
});
const staticProps = ['NONE', 'CAPTURING_PHASE', 'AT_TARGET', 'BUBBLING_PHASE'];
ObjectDefineProperties(
Event,
ArrayPrototypeReduce(staticProps, (result, staticProp, index = 0) => {
result[staticProp] = {
__proto__: null,
writable: false,
configurable: false,
enumerable: true,
value: index,
};
return result;
}, {}),
);
function isCustomEvent(value) {
return isEvent(value) && (value?.[kDetail] !== undefined);
}
class CustomEvent extends Event {
/**
* @constructor
* @param {string} type
* @param {{
* bubbles?: boolean,
* cancelable?: boolean,
* composed?: boolean,
* detail?: any,
* }} [options]
*/
constructor(type, options = kEmptyObject) {
if (arguments.length === 0)
throw new ERR_MISSING_ARGS('type');
super(type, options);
this[kDetail] = options?.detail ?? null;
}
/**
* @type {any}
*/
get detail() {
if (!isCustomEvent(this))
throw new ERR_INVALID_THIS('CustomEvent');
return this[kDetail];
}
}
ObjectDefineProperties(CustomEvent.prototype, {
[SymbolToStringTag]: {
__proto__: null,
writable: false,
enumerable: false,
configurable: true,
value: 'CustomEvent',
},
detail: kEnumerableProperty,
});
// Weak listener cleanup
// This has to be lazy for snapshots to work
let weakListenersState = null;
// The resource needs to retain the callback so that it doesn't
// get garbage collected now that it's weak.
let objectToWeakListenerMap = null;
function weakListeners() {
weakListenersState ??= new SafeFinalizationRegistry(
({ eventTarget, listener, eventType }) => eventTarget.deref()?.[kRemoveWeakListenerHelper](eventType, listener),
);
objectToWeakListenerMap ??= new SafeWeakMap();
return { registry: weakListenersState, map: objectToWeakListenerMap };
}
const kFlagOnce = 1 << 0;
const kFlagCapture = 1 << 1;
const kFlagPassive = 1 << 2;
const kFlagNodeStyle = 1 << 3;
const kFlagWeak = 1 << 4;
const kFlagRemoved = 1 << 5;
const kFlagResistStopPropagation = 1 << 6;
// 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 {
constructor(eventTarget, eventType, previous, listener, once, capture, passive,
isNodeStyleListener, weak, resistStopPropagation) {
this.next = undefined;
if (previous !== undefined)
previous.next = this;
this.previous = previous;
this.listener = listener;
let flags = 0b0;
if (once)
flags |= kFlagOnce;
if (capture)
flags |= kFlagCapture;
if (passive)
flags |= kFlagPassive;
if (isNodeStyleListener)
flags |= kFlagNodeStyle;
if (weak)
flags |= kFlagWeak;
if (resistStopPropagation)
flags |= kFlagResistStopPropagation;
this.flags = flags;
this.removed = false;
if (this.weak) {
this.callback = new SafeWeakRef(listener);
weakListeners().registry.register(listener, {
__proto__: null,
// Weak ref so the listener won't hold the eventTarget alive
eventTarget: new SafeWeakRef(eventTarget),
listener: this,
eventType,
}, this);
// Make the retainer retain the listener in a WeakMap
weakListeners().map.set(weak, listener);
this.listener = this.callback;
} else if (typeof listener === 'function') {
this.callback = listener;
this.listener = listener;
} else {
this.callback = async (...args) => {
if (listener.handleEvent)
await ReflectApply(listener.handleEvent, listener, args);
};
this.listener = listener;
}
}
get once() {
return Boolean(this.flags & kFlagOnce);
}
get capture() {
return Boolean(this.flags & kFlagCapture);
}
get passive() {
return Boolean(this.flags & kFlagPassive);
}
get isNodeStyleListener() {
return Boolean(this.flags & kFlagNodeStyle);
}
get weak() {
return Boolean(this.flags & kFlagWeak);
}
get resistStopPropagation() {
return Boolean(this.flags & kFlagResistStopPropagation);
}
get removed() {
return Boolean(this.flags & kFlagRemoved);
}
set removed(value) {
if (value)
this.flags |= kFlagRemoved;
else
this.flags &= ~kFlagRemoved;
}
same(listener, capture) {
const myListener = this.weak ? this.listener.deref() : this.listener;
return myListener === listener && this.capture === capture;
}
remove() {
if (this.previous !== undefined)
this.previous.next = this.next;
if (this.next !== undefined)
this.next.previous = this.previous;
this.removed = true;
if (this.weak)
weakListeners().registry.unregister(this);
}
}
function initEventTarget(self) {
self[kEvents] = new SafeMap();
self[kMaxEventTargetListeners] = EventEmitter.defaultMaxListeners;
self[kMaxEventTargetListenersWarned] = false;
self[kHandlers] = new SafeMap();
}
class EventTarget {
// Used in checking whether an object is an EventTarget. This is a well-known
// symbol as EventTarget may be used cross-realm.
// Ref: https://github.com/nodejs/node/pull/33661
static [kIsEventTarget] = true;
constructor() {
initEventTarget(this);
}
[kNewListener](size, type, listener, once, capture, passive, weak) {
if (this[kMaxEventTargetListeners] > 0 &&
size > this[kMaxEventTargetListeners] &&
!this[kMaxEventTargetListenersWarned]) {
this[kMaxEventTargetListenersWarned] = 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 })}. MaxListeners is ${this[kMaxEventTargetListeners]}. Use ` +
'events.setMaxListeners() to increase limit');
w.name = 'MaxListenersExceededWarning';
w.target = this;
w.type = type;
w.count = size;
process.emitWarning(w);
}
}
[kRemoveListener](size, type, listener, capture) {}
/**
* @callback EventTargetCallback
* @param {Event} event
*/
/**
* @typedef {{ handleEvent: EventTargetCallback }} EventListener
*/
/**
* @param {string} type
* @param {EventTargetCallback|EventListener} listener
* @param {{
* capture?: boolean,
* once?: boolean,
* passive?: boolean,
* signal?: AbortSignal
* }} [options]
*/
addEventListener(type, listener, options = kEmptyObject) {
if (!isEventTarget(this))
throw new ERR_INVALID_THIS('EventTarget');
if (arguments.length < 2)
throw new ERR_MISSING_ARGS('type', 'listener');
let once = false;
let capture = false;
let passive = false;
let isNodeStyleListener = false;
let weak = false;
let resistStopPropagation = false;
if (options !== kEmptyObject) {
// We validateOptions before the validateListener check because the spec
// requires us to hit getters.
options = validateEventListenerOptions(options);
once = options.once;
capture = options.capture;
passive = options.passive;
isNodeStyleListener = options.isNodeStyleListener;
weak = options.weak;
resistStopPropagation = options.resistStopPropagation;
const signal = options.signal;
validateAbortSignal(signal, 'options.signal');
if (signal) {
if (signal.aborted) {
return;
}
// TODO(benjamingr) make this weak somehow? ideally the signal would
// not prevent the event target from GC.
signal.addEventListener('abort', () => {
this.removeEventListener(type, listener, options);
}, { __proto__: null, once: true, [kWeakHandler]: this, [kResistStopPropagation]: true });
}
}
if (!validateEventListener(listener)) {
// The DOM silently allows passing undefined as a second argument
// No error code for this since it is a Warning
// eslint-disable-next-line no-restricted-syntax
const w = new Error(`addEventListener called with ${listener}` +
' which has no effect.');
w.name = 'AddEventListenerArgumentTypeWarning';
w.target = this;
w.type = type;
process.emitWarning(w);
return;
}
type = webidl.converters.DOMString(type);
let root = this[kEvents].get(type);
if (root === undefined) {
root = { size: 1, next: undefined, resistStopPropagation: Boolean(resistStopPropagation) };
// This is the first handler in our linked list.
new Listener(this, type, root, listener, once, capture, passive,
isNodeStyleListener, weak, resistStopPropagation);
this[kNewListener](
root.size,
type,
listener,
once,
capture,
passive,
weak);
this[kEvents].set(type, root);
return;
}
let handler = root.next;
let previous = root;
// 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(this, type, previous, listener, once, capture, passive,
isNodeStyleListener, weak, resistStopPropagation);
root.size++;
root.resistStopPropagation ||= Boolean(resistStopPropagation);
this[kNewListener](root.size, type, listener, once, capture, passive, weak);
}
/**
* @param {string} type
* @param {EventTargetCallback|EventListener} listener
* @param {{
* capture?: boolean,
* }} [options]
*/
removeEventListener(type, listener, options = kEmptyObject) {
if (!isEventTarget(this))
throw new ERR_INVALID_THIS('EventTarget');
if (arguments.length < 2)
throw new ERR_MISSING_ARGS('type', 'listener');
if (!validateEventListener(listener))
return;
type = webidl.converters.DOMString(type);
const capture = options?.capture === true;
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;
}
}
[kRemoveWeakListenerHelper](type, listener) {
const root = this[kEvents].get(type);
if (root === undefined || root.next === undefined)
return;
const capture = listener.capture === true;
let handler = root.next;
while (handler !== undefined) {
if (handler === listener) {
handler.remove();
root.size--;
if (root.size === 0)
this[kEvents].delete(type);
// Undefined is passed as the listener as the listener was GCed
this[kRemoveListener](root.size, type, undefined, capture);
break;
}
handler = handler.next;
}
}
/**
* @param {Event} event
*/
dispatchEvent(event) {
if (!isEventTarget(this))
throw new ERR_INVALID_THIS('EventTarget');
if (arguments.length < 1)
throw new ERR_MISSING_ARGS('event');
if (!(event instanceof Event))
throw new ERR_INVALID_ARG_TYPE('event', 'Event', event);
if (event[kIsBeingDispatched])
throw new ERR_EVENT_RECURSION(event.type);
this[kHybridDispatch](event, event.type, event);
return event.defaultPrevented !== true;
}
[kHybridDispatch](nodeValue, type, event) {
const createEvent = () => {
if (event === undefined) {
event = this[kCreateEvent](nodeValue, type);
event[kTarget] = this;
event[kIsBeingDispatched] = true;
}
return event;
};
if (event !== undefined) {
event[kTarget] = this;
event[kIsBeingDispatched] = true;
}
const root = this[kEvents].get(type);
if (root === undefined || root.next === undefined) {
if (event !== undefined)
event[kIsBeingDispatched] = false;
return true;
}
let handler = root.next;
let next;
const iterationCondition = () => {
if (handler === undefined) {
return false;
}
return root.resistStopPropagation || handler.passive || event?.[kStop] !== true;
};
while (iterationCondition()) {
// Cache the next item in case this iteration removes the current one
next = handler.next;
if (handler.removed || (event?.[kStop] === true && !handler.resistStopPropagation)) {
// Deal with the case an event is removed while event handlers are
// Being processed (removeEventListener called from a listener)
// And the case of event.stopImmediatePropagation() being called
// For events not flagged as resistStopPropagation
handler = next;
continue;
}
if (handler.once) {
handler.remove();
root.size--;
const { listener, capture } = handler;
this[kRemoveListener](root.size, type, listener, capture);
}
try {
let arg;
if (handler.isNodeStyleListener) {
arg = nodeValue;
} else {
arg = createEvent();
}
const callback = handler.weak ?
handler.callback.deref() : handler.callback;
let result;
if (callback) {
result = FunctionPrototypeCall(callback, this, arg);
if (!handler.isNodeStyleListener) {
arg[kIsBeingDispatched] = false;
}
}
if (result !== undefined && result !== null)
addCatch(result);
} catch (err) {
emitUncaughtException(err);
}
handler = next;
}
if (event !== undefined)
event[kIsBeingDispatched] = false;
}
[kCreateEvent](nodeValue, type) {
return new CustomEvent(type, { detail: nodeValue });
}
[customInspectSymbol](depth, options) {
if (!isEventTarget(this))
throw new ERR_INVALID_THIS('EventTarget');
const name = this.constructor.name;
if (depth < 0)
return name;
const opts = ObjectAssign({}, options, {
depth: NumberIsInteger(options.depth) ? options.depth - 1 : options.depth,
});
return `${name} ${inspect({}, opts)}`;
}
}
ObjectDefineProperties(EventTarget.prototype, {
addEventListener: kEnumerableProperty,
removeEventListener: kEnumerableProperty,
dispatchEvent: kEnumerableProperty,
[SymbolToStringTag]: {
__proto__: null,
writable: false,
enumerable: false,
configurable: true,
value: 'EventTarget',
},
});
function initNodeEventTarget(self) {
initEventTarget(self);
}
class NodeEventTarget extends EventTarget {
static [kIsNodeEventTarget] = true;
static defaultMaxListeners = 10;
constructor() {
super();
initNodeEventTarget(this);
}
/**
* @param {number} n
*/
setMaxListeners(n) {
if (!isNodeEventTarget(this))
throw new ERR_INVALID_THIS('NodeEventTarget');
EventEmitter.setMaxListeners(n, this);
}
/**
* @returns {number}
*/
getMaxListeners() {
if (!isNodeEventTarget(this))
throw new ERR_INVALID_THIS('NodeEventTarget');
return this[kMaxEventTargetListeners];
}
/**
* @returns {string[]}
*/
eventNames() {
if (!isNodeEventTarget(this))
throw new ERR_INVALID_THIS('NodeEventTarget');
return ArrayFrom(this[kEvents].keys());
}
/**
* @param {string} type
* @returns {number}
*/
listenerCount(type) {
if (!isNodeEventTarget(this))
throw new ERR_INVALID_THIS('NodeEventTarget');
const root = this[kEvents].get(String(type));
return root !== undefined ? root.size : 0;
}
/**
* @param {string} type
* @param {EventTargetCallback|EventListener} listener
* @param {{
* capture?: boolean,
* }} [options]
* @returns {NodeEventTarget}
*/
off(type, listener, options) {
if (!isNodeEventTarget(this))
throw new ERR_INVALID_THIS('NodeEventTarget');
this.removeEventListener(type, listener, options);
return this;
}
/**
* @param {string} type
* @param {EventTargetCallback|EventListener} listener
* @param {{
* capture?: boolean,
* }} [options]
* @returns {NodeEventTarget}
*/
removeListener(type, listener, options) {
if (!isNodeEventTarget(this))
throw new ERR_INVALID_THIS('NodeEventTarget');
this.removeEventListener(type, listener, options);
return this;
}
/**
* @param {string} type
* @param {EventTargetCallback|EventListener} listener
* @returns {NodeEventTarget}
*/
on(type, listener) {
if (!isNodeEventTarget(this))
throw new ERR_INVALID_THIS('NodeEventTarget');
this.addEventListener(type, listener, { [kIsNodeStyleListener]: true });
return this;
}
/**
* @param {string} type
* @param {EventTargetCallback|EventListener} listener
* @returns {NodeEventTarget}
*/
addListener(type, listener) {
if (!isNodeEventTarget(this))
throw new ERR_INVALID_THIS('NodeEventTarget');
this.addEventListener(type, listener, { [kIsNodeStyleListener]: true });
return this;
}
/**
* @param {string} type
* @param {any} arg
* @returns {boolean}
*/
emit(type, arg) {
if (!isNodeEventTarget(this))
throw new ERR_INVALID_THIS('NodeEventTarget');
validateString(type, 'type');
const hadListeners = this.listenerCount(type) > 0;
this[kHybridDispatch](arg, type);
return hadListeners;
}
/**
* @param {string} type
* @param {EventTargetCallback|EventListener} listener
* @returns {NodeEventTarget}
*/
once(type, listener) {
if (!isNodeEventTarget(this))
throw new ERR_INVALID_THIS('NodeEventTarget');
this.addEventListener(type, listener,
{ once: true, [kIsNodeStyleListener]: true });
return this;
}
/**
* @param {string} [type]
* @returns {NodeEventTarget}
*/
removeAllListeners(type) {
if (!isNodeEventTarget(this))
throw new ERR_INVALID_THIS('NodeEventTarget');
if (type !== undefined) {
this[kEvents].delete(String(type));
} else {
this[kEvents].clear();
}
return this;
}
}
ObjectDefineProperties(NodeEventTarget.prototype, {
setMaxListeners: kEnumerableProperty,
getMaxListeners: kEnumerableProperty,
eventNames: kEnumerableProperty,
listenerCount: kEnumerableProperty,
off: kEnumerableProperty,
removeListener: kEnumerableProperty,
on: kEnumerableProperty,
addListener: kEnumerableProperty,
once: kEnumerableProperty,
emit: kEnumerableProperty,
removeAllListeners: kEnumerableProperty,
});
// EventTarget API
function validateEventListener(listener) {
if (typeof listener === 'function' ||
typeof listener?.handleEvent === 'function') {
return true;
}
if (listener == null)
return false;
if (typeof listener === 'object') {
// Require `handleEvent` lazily.
return true;
}
throw new ERR_INVALID_ARG_TYPE('listener', 'EventListener', listener);
}
function validateEventListenerOptions(options) {
if (typeof options === 'boolean')
return { capture: options };
if (options === null)
return kEmptyObject;
validateObject(options, 'options', kValidateObjectAllowObjects);
return {
once: Boolean(options.once),
capture: Boolean(options.capture),
passive: Boolean(options.passive),
signal: options.signal,
weak: options[kWeakHandler],
resistStopPropagation: options[kResistStopPropagation] ?? false,
isNodeStyleListener: Boolean(options[kIsNodeStyleListener]),
};
}
// Test whether the argument is an event object. This is far from a fool-proof
// test, for example this input will result in a false positive:
// > isEventTarget({ constructor: EventTarget })
// It stands in its current implementation as a compromise.
// Ref: https://github.com/nodejs/node/pull/33661
function isEventTarget(obj) {
return obj?.constructor?.[kIsEventTarget];
}
function isNodeEventTarget(obj) {
return obj?.constructor?.[kIsNodeEventTarget];
}
function addCatch(promise) {
const then = promise.then;
if (typeof then === 'function') {
FunctionPrototypeCall(then, promise, undefined, function(err) {
// The callback is called with nextTick to avoid a follow-up
// rejection from this promise.
emitUncaughtException(err);
});
}
}
function emitUncaughtException(err) {
process.nextTick(() => { throw err; });
}
function makeEventHandler(handler) {
// Event handlers are dispatched in the order they were first set
// See https://github.com/nodejs/node/pull/35949#issuecomment-722496598
function eventHandler(...args) {
if (typeof eventHandler.handler !== 'function') {
return;
}
return ReflectApply(eventHandler.handler, this, args);
}
eventHandler.handler = handler;
return eventHandler;
}
function defineEventHandler(emitter, name, event = name) {
// 8.1.5.1 Event handlers - basically `on[eventName]` attributes
const propName = `on${name}`;
function get() {
validateInternalField(this, kHandlers, 'EventTarget');
return this[kHandlers]?.get(event)?.handler ?? null;
}
ObjectDefineProperty(get, 'name', {
__proto__: null,
value: `get ${propName}`,
});
function set(value) {
validateInternalField(this, kHandlers, 'EventTarget');
let wrappedHandler = this[kHandlers]?.get(event);
if (wrappedHandler) {
if (typeof wrappedHandler.handler === 'function') {
this[kEvents].get(event).size--;
const size = this[kEvents].get(event).size;
this[kRemoveListener](size, event, wrappedHandler.handler, false);
}
wrappedHandler.handler = value;
if (typeof wrappedHandler.handler === 'function') {
this[kEvents].get(event).size++;
const size = this[kEvents].get(event).size;
this[kNewListener](size, event, value, false, false, false, false);
}
} else {
wrappedHandler = makeEventHandler(value);
this.addEventListener(event, wrappedHandler);
}
this[kHandlers].set(event, wrappedHandler);
}
ObjectDefineProperty(set, 'name', {
__proto__: null,
value: `set ${propName}`,
});
ObjectDefineProperty(emitter, propName, {
__proto__: null,
get,
set,
configurable: true,
enumerable: true,
});
}
module.exports = {
Event,
CustomEvent,
EventTarget,
NodeEventTarget,
defineEventHandler,
initEventTarget,
initNodeEventTarget,
kCreateEvent,
kNewListener,
kTrustEvent,
kRemoveListener,
kEvents,
kWeakHandler,
kResistStopPropagation,
isEventTarget,
};