diff options
author | Bartek IwaĆczuk <biwanczuk@gmail.com> | 2023-02-14 17:38:45 +0100 |
---|---|---|
committer | GitHub <noreply@github.com> | 2023-02-14 17:38:45 +0100 |
commit | d47147fb6ad229b1c039aff9d0959b6e281f4df5 (patch) | |
tree | 6e9e790f2b9bc71b5f0c9c7e64b95cae31579d58 /ext/node/polyfills/internal/event_target.mjs | |
parent | 1d00bbe47e2ca14e2d2151518e02b2324461a065 (diff) |
feat(ext/node): embed std/node into the snapshot (#17724)
This commit moves "deno_std/node" in "ext/node" crate. The code is
transpiled and snapshotted during the build process.
During the first pass a minimal amount of work was done to create the
snapshot, a lot of code in "ext/node" depends on presence of "Deno"
global. This code will be gradually fixed in the follow up PRs to migrate
it to import relevant APIs from "internal:" modules.
Currently the code from snapshot is not used in any way, and all
Node/npm compatibility still uses code from
"https://deno.land/std/node" (or from the location specified by
"DENO_NODE_COMPAT_URL"). This will also be handled in a follow
up PRs.
---------
Co-authored-by: crowlkats <crowlkats@toaxl.com>
Co-authored-by: Divy Srivastava <dj.srivastava23@gmail.com>
Co-authored-by: Yoshiya Hinosawa <stibium121@gmail.com>
Diffstat (limited to 'ext/node/polyfills/internal/event_target.mjs')
-rw-r--r-- | ext/node/polyfills/internal/event_target.mjs | 1111 |
1 files changed, 1111 insertions, 0 deletions
diff --git a/ext/node/polyfills/internal/event_target.mjs b/ext/node/polyfills/internal/event_target.mjs new file mode 100644 index 000000000..d542fba94 --- /dev/null +++ b/ext/node/polyfills/internal/event_target.mjs @@ -0,0 +1,1111 @@ +// Copyright 2018-2023 the Deno authors. All rights reserved. MIT license. +// Copyright Node.js contributors. All rights reserved. MIT License. + +import { + ERR_EVENT_RECURSION, + ERR_INVALID_ARG_TYPE, + ERR_INVALID_THIS, + ERR_MISSING_ARGS, +} from "internal:deno_node/polyfills/internal/errors.ts"; +import { validateObject, validateString } from "internal:deno_node/polyfills/internal/validators.mjs"; +import { emitWarning } from "internal:deno_node/polyfills/process.ts"; +import { nextTick } from "internal:deno_node/polyfills/_next_tick.ts"; +import { Event as WebEvent, EventTarget as WebEventTarget } from "internal:deno_web/02_event.js"; + +import { + customInspectSymbol, + kEmptyObject, + kEnumerableProperty, +} from "internal:deno_node/polyfills/internal/util.mjs"; +import { inspect } from "internal:deno_node/polyfills/util.ts"; + +const kIsEventTarget = Symbol.for("nodejs.event_target"); +const kIsNodeEventTarget = Symbol("kIsNodeEventTarget"); + +import { EventEmitter } from "internal:deno_node/polyfills/events.ts"; +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 kHybridDispatch = Symbol.for("nodejs.internal.kHybridDispatch"); +const kCreateEvent = Symbol("kCreateEvent"); +const kNewListener = Symbol("kNewListener"); +const kRemoveListener = Symbol("kRemoveListener"); +const kIsNodeStyleListener = Symbol("kIsNodeStyleListener"); +const kTrustEvent = Symbol("kTrustEvent"); + +const kType = Symbol("type"); +const kDetail = Symbol("detail"); +const kDefaultPrevented = Symbol("defaultPrevented"); +const kCancelable = Symbol("cancelable"); +const kTimestamp = Symbol("timestamp"); +const kBubbles = Symbol("bubbles"); +const kComposed = Symbol("composed"); +const kPropagationStopped = Symbol("propagationStopped"); + +function isEvent(value) { + return typeof value?.[kType] === "string"; +} + +class Event extends WebEvent { + /** + * @param {string} type + * @param {{ + * bubbles?: boolean, + * cancelable?: boolean, + * composed?: boolean, + * }} [options] + */ + constructor(type, options = null) { + super(type, options); + if (arguments.length === 0) { + throw new ERR_MISSING_ARGS("type"); + } + validateObject(options, "options", { + allowArray: true, + allowFunction: true, + nullable: true, + }); + const { cancelable, bubbles, composed } = { ...options }; + this[kCancelable] = !!cancelable; + this[kBubbles] = !!bubbles; + this[kComposed] = !!composed; + this[kType] = `${type}`; + this[kDefaultPrevented] = false; + this[kTimestamp] = performance.now(); + this[kPropagationStopped] = false; + this[kTarget] = null; + this[kIsBeingDispatched] = false; + } + + [customInspectSymbol](depth, options) { + if (!isEvent(this)) { + throw new ERR_INVALID_THIS("Event"); + } + const name = this.constructor.name; + if (depth < 0) { + return name; + } + + const opts = Object.assign({}, options, { + depth: NumberIsInteger(options.depth) ? options.depth - 1 : options.depth, + }); + + return `${name} ${ + inspect({ + type: this[kType], + defaultPrevented: this[kDefaultPrevented], + cancelable: this[kCancelable], + timeStamp: this[kTimestamp], + }, 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[kDefaultPrevented] = 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[kCancelable]; + } + + /** + * @type {boolean} + */ + get defaultPrevented() { + if (!isEvent(this)) { + throw new ERR_INVALID_THIS("Event"); + } + return this[kCancelable] && this[kDefaultPrevented]; + } + + /** + * @type {number} + */ + get timeStamp() { + if (!isEvent(this)) { + throw new ERR_INVALID_THIS("Event"); + } + return this[kTimestamp]; + } + + // 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.defaultPrevented; + } + + /** + * @type {boolean} + */ + get bubbles() { + if (!isEvent(this)) { + throw new ERR_INVALID_THIS("Event"); + } + return this[kBubbles]; + } + + /** + * @type {boolean} + */ + get composed() { + if (!isEvent(this)) { + throw new ERR_INVALID_THIS("Event"); + } + return this[kComposed]; + } + + /** + * @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[kPropagationStopped]; + } + + /** + * @type {boolean} + */ + set cancelBubble(value) { + if (!isEvent(this)) { + throw new ERR_INVALID_THIS("Event"); + } + if (value) { + this.stopPropagation(); + } + } + + stopPropagation() { + if (!isEvent(this)) { + throw new ERR_INVALID_THIS("Event"); + } + this[kPropagationStopped] = true; + } + + static NONE = 0; + static CAPTURING_PHASE = 1; + static AT_TARGET = 2; + static BUBBLING_PHASE = 3; +} + +Object.defineProperties( + Event.prototype, + { + [Symbol.toStringTag]: { + writable: true, + enumerable: false, + configurable: true, + value: "Event", + }, + 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, + }, +); + +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]; + } +} + +Object.defineProperties(CustomEvent.prototype, { + [Symbol.toStringTag]: { + __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 FinalizationRegistry( + (listener) => listener.remove(), + ); + objectToWeakListenerMap ??= new WeakMap(); + return { registry: weakListenersState, map: objectToWeakListenerMap }; +} + +// 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( + previous, + listener, + once, + capture, + passive, + isNodeStyleListener, + weak, + ) { + this.next = undefined; + if (previous !== undefined) { + previous.next = this; + } + this.previous = previous; + this.listener = listener; + // TODO(benjamingr) these 4 can be 'flags' to save 3 slots + this.once = once; + this.capture = capture; + this.passive = passive; + this.isNodeStyleListener = isNodeStyleListener; + this.removed = false; + this.weak = Boolean(weak); // Don't retain the object + + if (this.weak) { + this.callback = new WeakRef(listener); + weakListeners().registry.register(listener, this, 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 = Function.prototype.bind.call( + listener.handleEvent, + listener, + ); + this.listener = listener; + } + } + + 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 Map(); + self[kMaxEventTargetListeners] = EventEmitter.defaultMaxListeners; + self[kMaxEventTargetListenersWarned] = false; +} + +class EventTarget extends WebEventTarget { + // 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() { + super(); + 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; + 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 = {}) { + if (!isEventTarget(this)) { + throw new ERR_INVALID_THIS("EventTarget"); + } + if (arguments.length < 2) { + throw new ERR_MISSING_ARGS("type", "listener"); + } + + // We validateOptions before the shouldAddListeners check because the spec + // requires us to hit getters. + const { + once, + capture, + passive, + signal, + isNodeStyleListener, + weak, + } = validateEventListenerOptions(options); + + if (!shouldAddListener(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; + emitWarning(w); + return; + } + type = String(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); + }, { once: true, [kWeakHandler]: this }); + } + + 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, + isNodeStyleListener, + weak, + ); + 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( + previous, + listener, + once, + capture, + passive, + isNodeStyleListener, + weak, + ); + root.size++; + 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 = {}) { + if (!isEventTarget(this)) { + throw new ERR_INVALID_THIS("EventTarget"); + } + if (!shouldAddListener(listener)) { + return; + } + + type = String(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; + } + } + + /** + * @param {Event} event + */ + dispatchEvent(event) { + if (!isEventTarget(this)) { + throw new ERR_INVALID_THIS("EventTarget"); + } + + if (!(event instanceof globalThis.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; + + 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.removed) { + // Deal with the case an event is removed while event handlers are + // Being processed (removeEventListener called from a listener) + 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 = callback.call(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: Number.isInteger(options.depth) + ? options.depth - 1 + : options.depth, + }); + + return `${name} ${inspect({}, opts)}`; + } +} + +Object.defineProperties(EventTarget.prototype, { + addEventListener: kEnumerableProperty, + removeEventListener: kEnumerableProperty, + dispatchEvent: kEnumerableProperty, + [Symbol.toStringTag]: { + writable: true, + 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 Array.from(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; + } +} + +Object.defineProperties(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 shouldAddListener(listener) { + if ( + typeof listener === "function" || + typeof listener?.handleEvent === "function" + ) { + return true; + } + + if (listener == null) { + return false; + } + + throw new ERR_INVALID_ARG_TYPE("listener", "EventListener", listener); +} + +function validateEventListenerOptions(options) { + if (typeof options === "boolean") { + return { capture: options }; + } + + if (options === null) { + return {}; + } + validateObject(options, "options", { + allowArray: true, + allowFunction: true, + }); + return { + once: Boolean(options.once), + capture: Boolean(options.capture), + passive: Boolean(options.passive), + signal: options.signal, + weak: options[kWeakHandler], + isNodeStyleListener: Boolean(options[kIsNodeStyleListener]), + }; +} + +function isEventTarget(obj) { + return obj instanceof globalThis.EventTarget; +} + +function isNodeEventTarget(obj) { + return obj?.constructor?.[kIsNodeEventTarget]; +} + +function addCatch(promise) { + 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. + emitUncaughtException(err); + }); + } +} + +function emitUncaughtException(err) { + 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 Reflect.apply(eventHandler.handler, this, args); + } + eventHandler.handler = handler; + return eventHandler; +} + +function defineEventHandler(emitter, name) { + // 8.1.5.1 Event handlers - basically `on[eventName]` attributes + Object.defineProperty(emitter, `on${name}`, { + get() { + return this[kHandlers]?.get(name)?.handler ?? null; + }, + set(value) { + if (!this[kHandlers]) { + this[kHandlers] = new Map(); + } + let wrappedHandler = this[kHandlers]?.get(name); + if (wrappedHandler) { + if (typeof wrappedHandler.handler === "function") { + this[kEvents].get(name).size--; + const size = this[kEvents].get(name).size; + this[kRemoveListener](size, name, wrappedHandler.handler, false); + } + wrappedHandler.handler = value; + if (typeof wrappedHandler.handler === "function") { + this[kEvents].get(name).size++; + const size = this[kEvents].get(name).size; + this[kNewListener](size, name, value, false, false, false, false); + } + } else { + wrappedHandler = makeEventHandler(value); + this.addEventListener(name, wrappedHandler); + } + this[kHandlers].set(name, wrappedHandler); + }, + configurable: true, + enumerable: true, + }); +} + +const EventEmitterMixin = (Superclass) => { + class MixedEventEmitter extends Superclass { + constructor(...args) { + super(...args); + EventEmitter.call(this); + } + } + const protoProps = Object.getOwnPropertyDescriptors(EventEmitter.prototype); + delete protoProps.constructor; + Object.defineProperties(MixedEventEmitter.prototype, protoProps); + return MixedEventEmitter; +}; + +export { + CustomEvent, + defineEventHandler, + Event, + EventEmitterMixin, + EventTarget, + initEventTarget, + initNodeEventTarget, + isEventTarget, + kCreateEvent, + kEvents, + kNewListener, + kRemoveListener, + kTrustEvent, + kWeakHandler, + NodeEventTarget, +}; + +export default { + CustomEvent, + Event, + EventEmitterMixin, + EventTarget, + NodeEventTarget, + defineEventHandler, + initEventTarget, + initNodeEventTarget, + kCreateEvent, + kNewListener, + kTrustEvent, + kRemoveListener, + kEvents, + kWeakHandler, + isEventTarget, +}; |