From d8320f7bd7dc036ed716d17e2cf277a332d1fd67 Mon Sep 17 00:00:00 2001 From: shirakaba <14055146+shirakaba@users.noreply.github.com> Date: Sun, 10 Mar 2024 19:00:16 +0900 Subject: [PATCH] as-is commit of Node.js EventTarget implementation from 10th March 2024 https://github.com/nodejs/node/blob/575ced813988af00478aa1e6759487888c607238/lib/internal/event_target.js --- LICENCE-nodejs | 47 ++ src/event-target.js | 1175 +++++++++++++++++++++++++++++++++++++++++++ 2 files changed, 1222 insertions(+) create mode 100644 LICENCE-nodejs create mode 100644 src/event-target.js diff --git a/LICENCE-nodejs b/LICENCE-nodejs new file mode 100644 index 0000000..2873b3b --- /dev/null +++ b/LICENCE-nodejs @@ -0,0 +1,47 @@ +Node.js is licensed for use as follows: + +""" +Copyright Node.js contributors. All rights reserved. + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to +deal in the Software without restriction, including without limitation the +rights to use, copy, modify, merge, publish, distribute, sublicense, and/or +sell copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in +all copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING +FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS +IN THE SOFTWARE. +""" + +This license applies to parts of Node.js originating from the +https://github.com/joyent/node repository: + +""" +Copyright Joyent, Inc. and other Node contributors. All rights reserved. +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to +deal in the Software without restriction, including without limitation the +rights to use, copy, modify, merge, publish, distribute, sublicense, and/or +sell copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in +all copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING +FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS +IN THE SOFTWARE. +""" diff --git a/src/event-target.js b/src/event-target.js new file mode 100644 index 0000000..01a4c01 --- /dev/null +++ b/src/event-target.js @@ -0,0 +1,1175 @@ +'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_INVALID_ARG_TYPE, + ERR_EVENT_RECURSION, + ERR_MISSING_ARGS, + ERR_INVALID_THIS, + }, +} = require('internal/errors'); +const { + validateAbortSignal, + validateObject, + validateString, + validateInternalField, + kValidateObjectAllowArray, + kValidateObjectAllowFunction, +} = 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('nodejs.internal.removeWeakListenerHelper'); +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 = kEmptyObject) { + if (arguments.length === 0) + throw new ERR_MISSING_ARGS('type'); + validateObject(options, 'options'); + const { bubbles, cancelable, composed } = options; + this.#cancelable = !!cancelable; + this.#bubbles = !!bubbles; + this.#composed = !!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'); + 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[kTarget]; + } + + /** + * @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, +}); + +class NodeCustomEvent extends Event { + constructor(type, options) { + super(type, options); + if (options?.detail) { + this.detail = options.detail; + } + } +} + +// 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 })}. 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'); + + // We validateOptions before the validateListener check because the spec + // requires us to hit getters. + const { + once, + capture, + passive, + signal, + isNodeStyleListener, + weak, + resistStopPropagation, + } = validateEventListenerOptions(options); + + validateAbortSignal(signal, 'options.signal'); + + 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); + + 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 }); + } + + 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 NodeCustomEvent(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', kValidateObjectAllowArray | kValidateObjectAllowFunction); + 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, +}; \ No newline at end of file