diff --git a/lib/events.js b/lib/events.js index e05a3bc6b8b168..61dd0d7c972784 100644 --- a/lib/events.js +++ b/lib/events.js @@ -904,9 +904,9 @@ function getEventListeners(emitterOrTarget, type) { return emitterOrTarget.listeners(type); } // Require event target lazily to avoid always loading it - const { isEventTarget, kEvents } = require('internal/event_target'); + const { isEventTarget, kState } = require('internal/event_target'); if (isEventTarget(emitterOrTarget)) { - const root = emitterOrTarget[kEvents].get(type); + const root = emitterOrTarget[kState].events.get(type); const listeners = []; let handler = root?.next; while (handler?.listener !== undefined) { diff --git a/lib/internal/bootstrap/browser.js b/lib/internal/bootstrap/browser.js index b94ac9891a399c..1a764089042806 100644 --- a/lib/internal/bootstrap/browser.js +++ b/lib/internal/bootstrap/browser.js @@ -2,6 +2,7 @@ const { ObjectDefineProperty, + ObjectSetPrototypeOf, globalThis, } = primordials; @@ -43,10 +44,11 @@ exposeLazyInterfaces(globalThis, 'internal/abort_controller', [ 'AbortController', 'AbortSignal', ]); const { - EventTarget, Event, + EventTarget, Event, initEventTarget, } = require('internal/event_target'); exposeInterface(globalThis, 'Event', Event); exposeInterface(globalThis, 'EventTarget', EventTarget); +setGlobalThisPrototype(); exposeLazyInterfaces(globalThis, 'internal/worker/io', [ 'MessageChannel', 'MessagePort', 'MessageEvent', ]); @@ -103,6 +105,11 @@ function exposeGetterAndSetter(target, name, getter, setter = undefined) { }); } +function setGlobalThisPrototype() { + initEventTarget(globalThis); + ObjectSetPrototypeOf(globalThis, EventTarget.prototype); +} + // Web Streams API exposeLazyInterfaces( globalThis, diff --git a/lib/internal/event_target.js b/lib/internal/event_target.js index 863c1c6ea8bd01..b3cfe2c5247a13 100644 --- a/lib/internal/event_target.js +++ b/lib/internal/event_target.js @@ -24,6 +24,7 @@ const { Symbol, SymbolFor, SymbolToStringTag, + globalThis, } = primordials; const { @@ -47,16 +48,11 @@ const kIsEventTarget = SymbolFor('nodejs.event_target'); const kIsNodeEventTarget = Symbol('kIsNodeEventTarget'); const EventEmitter = require('events'); -const { - kMaxEventTargetListeners, - kMaxEventTargetListenersWarned, -} = EventEmitter; -const kEvents = Symbol('kEvents'); +const kState = Symbol('nodejs.internal.eventTargetState'); const kIsBeingDispatched = Symbol('kIsBeingDispatched'); const kStop = Symbol('kStop'); const kTarget = Symbol('kTarget'); -const kHandlers = Symbol('khandlers'); const kWeakHandler = Symbol('kWeak'); const kHybridDispatch = SymbolFor('nodejs.internal.kHybridDispatch'); @@ -489,10 +485,13 @@ class Listener { } function initEventTarget(self) { - self[kEvents] = new SafeMap(); - self[kMaxEventTargetListeners] = EventEmitter.defaultMaxListeners; - self[kMaxEventTargetListenersWarned] = false; - self[kHandlers] = new SafeMap(); + self[kState] = { + __proto__: null, + events: new SafeMap(), + maxEventTargetListeners: EventTarget.defaultMaxListeners, + maxEventTargetListenersWarned: false, + handlers: new SafeMap(), + }; } class EventTarget { @@ -506,18 +505,19 @@ class EventTarget { } [kNewListener](size, type, listener, once, capture, passive, weak) { - if (this[kMaxEventTargetListeners] > 0 && - size > this[kMaxEventTargetListeners] && - !this[kMaxEventTargetListenersWarned]) { - this[kMaxEventTargetListenersWarned] = true; + const self = this ?? globalThis + if (self[kState].maxEventTargetListeners > 0 && + size > self[kState].maxEventTargetListeners && + !self[kState].maxEventTargetListenersWarned) { + self[kState].maxEventTargetListenersWarned = 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 ` + + `added to ${inspect(self, { depth: -1 })}. Use ` + 'events.setMaxListeners() to increase limit'); w.name = 'MaxListenersExceededWarning'; - w.target = this; + w.target = self; w.type = type; w.count = size; process.emitWarning(w); @@ -545,7 +545,8 @@ class EventTarget { * }} [options] */ addEventListener(type, listener, options = kEmptyObject) { - if (!isEventTarget(this)) + const self = this ?? globalThis + if (!isEventTarget(self)) throw new ERR_INVALID_THIS('EventTarget'); if (arguments.length < 2) throw new ERR_MISSING_ARGS('type', 'listener'); @@ -568,7 +569,7 @@ class EventTarget { const w = new Error(`addEventListener called with ${listener}` + ' which has no effect.'); w.name = 'AddEventListenerArgumentTypeWarning'; - w.target = this; + w.target = self; w.type = type; process.emitWarning(w); return; @@ -584,18 +585,18 @@ class EventTarget { // 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 }); + self.removeEventListener(type, listener, options); + }, { once: true, [kWeakHandler]: self }); } - let root = this[kEvents].get(type); + let root = self[kState].events.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]( + self[kNewListener]( root.size, type, listener, @@ -603,7 +604,7 @@ class EventTarget { capture, passive, weak); - this[kEvents].set(type, root); + self[kState].events.set(type, root); return; } @@ -623,7 +624,7 @@ class EventTarget { new Listener(previous, listener, once, capture, passive, isNodeStyleListener, weak); root.size++; - this[kNewListener](root.size, type, listener, once, capture, passive, weak); + self[kNewListener](root.size, type, listener, once, capture, passive, weak); } /** @@ -634,7 +635,8 @@ class EventTarget { * }} [options] */ removeEventListener(type, listener, options = kEmptyObject) { - if (!isEventTarget(this)) + const self = this ?? globalThis + if (!isEventTarget(self)) throw new ERR_INVALID_THIS('EventTarget'); if (arguments.length < 2) throw new ERR_MISSING_ARGS('type', 'listener'); @@ -644,7 +646,7 @@ class EventTarget { type = String(type); const capture = options?.capture === true; - const root = this[kEvents].get(type); + const root = self[kState].events.get(type); if (root === undefined || root.next === undefined) return; @@ -654,8 +656,8 @@ class EventTarget { handler.remove(); root.size--; if (root.size === 0) - this[kEvents].delete(type); - this[kRemoveListener](root.size, type, listener, capture); + self[kState].events.delete(type); + self[kRemoveListener](root.size, type, listener, capture); break; } handler = handler.next; @@ -666,7 +668,8 @@ class EventTarget { * @param {Event} event */ dispatchEvent(event) { - if (!isEventTarget(this)) + const self = this ?? globalThis + if (!isEventTarget(self)) throw new ERR_INVALID_THIS('EventTarget'); if (arguments.length < 1) throw new ERR_MISSING_ARGS('event'); @@ -677,7 +680,7 @@ class EventTarget { if (event[kIsBeingDispatched]) throw new ERR_EVENT_RECURSION(event.type); - this[kHybridDispatch](event, event.type, event); + self[kHybridDispatch](event, event.type, event); return event.defaultPrevented !== true; } @@ -696,7 +699,7 @@ class EventTarget { event[kIsBeingDispatched] = true; } - const root = this[kEvents].get(type); + const root = this[kState].events.get(type); if (root === undefined || root.next === undefined) { if (event !== undefined) event[kIsBeingDispatched] = false; @@ -757,9 +760,10 @@ class EventTarget { return new NodeCustomEvent(type, { detail: nodeValue }); } [customInspectSymbol](depth, options) { - if (!isEventTarget(this)) + const self = this ?? globalThis + if (!isEventTarget(self)) throw new ERR_INVALID_THIS('EventTarget'); - const name = this.constructor.name; + const name = 'EventTarget'; if (depth < 0) return name; @@ -812,7 +816,7 @@ class NodeEventTarget extends EventTarget { getMaxListeners() { if (!isNodeEventTarget(this)) throw new ERR_INVALID_THIS('NodeEventTarget'); - return this[kMaxEventTargetListeners]; + return this[kState].maxEventTargetListeners; } /** @@ -821,7 +825,7 @@ class NodeEventTarget extends EventTarget { eventNames() { if (!isNodeEventTarget(this)) throw new ERR_INVALID_THIS('NodeEventTarget'); - return ArrayFrom(this[kEvents].keys()); + return ArrayFrom(this[kState].events.keys()); } /** @@ -831,7 +835,7 @@ class NodeEventTarget extends EventTarget { listenerCount(type) { if (!isNodeEventTarget(this)) throw new ERR_INVALID_THIS('NodeEventTarget'); - const root = this[kEvents].get(String(type)); + const root = this[kState].events.get(String(type)); return root !== undefined ? root.size : 0; } @@ -924,9 +928,9 @@ class NodeEventTarget extends EventTarget { if (!isNodeEventTarget(this)) throw new ERR_INVALID_THIS('NodeEventTarget'); if (type !== undefined) { - this[kEvents].delete(String(type)); + this[kState].events.delete(String(type)); } else { - this[kEvents].clear(); + this[kState].events.clear(); } return this; @@ -991,7 +995,7 @@ function validateEventListenerOptions(options) { // It stands in its current implementation as a compromise. // Ref: https://github.com/nodejs/node/pull/33661 function isEventTarget(obj) { - return obj?.constructor?.[kIsEventTarget]; + return obj.constructor?.[kIsEventTarget] || obj === globalThis; } function isNodeEventTarget(obj) { @@ -1030,8 +1034,9 @@ 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; + const self = this ?? globalThis; + validateInternalField(self[kState], 'handlers', 'EventTarget'); + return self[kState].handlers?.get(event)?.handler ?? null; } ObjectDefineProperty(get, 'name', { __proto__: null, @@ -1039,25 +1044,26 @@ function defineEventHandler(emitter, name, event = name) { }); function set(value) { - validateInternalField(this, kHandlers, 'EventTarget'); - let wrappedHandler = this[kHandlers]?.get(event); + const self = this ?? globalThis; + validateInternalField(self[kState], 'handlers', 'EventTarget'); + let wrappedHandler = self[kState].handlers?.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); + self[kState].events.get(event).size--; + const size = self[kState].events.get(event).size; + self[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); + self[kState].events.get(event).size++; + const size = self[kState].events.get(event).size; + self[kNewListener](size, event, value, false, false, false, false); } } else { wrappedHandler = makeEventHandler(value); - this.addEventListener(event, wrappedHandler); + self.addEventListener(event, wrappedHandler); } - this[kHandlers].set(event, wrappedHandler); + self[kState].handlers.set(event, wrappedHandler); } ObjectDefineProperty(set, 'name', { __proto__: null, @@ -1106,7 +1112,7 @@ module.exports = { kNewListener, kTrustEvent, kRemoveListener, - kEvents, + kState, kWeakHandler, isEventTarget, }; diff --git a/test/common/index.js b/test/common/index.js index 06ea1fee1d9437..4f736ba5bb9fbd 100644 --- a/test/common/index.js +++ b/test/common/index.js @@ -276,6 +276,10 @@ let knownGlobals = [ setInterval, setTimeout, queueMicrotask, + // EventTarget prototype + addEventListener, + removeEventListener, + dispatchEvent, ]; // TODO(@jasnell): This check can be temporary. AbortController is diff --git a/test/parallel/test-events-once.js b/test/parallel/test-events-once.js index 0b1d5677f60109..1c4228f56e85cc 100644 --- a/test/parallel/test-events-once.js +++ b/test/parallel/test-events-once.js @@ -9,7 +9,7 @@ const { fail, rejects, } = require('assert'); -const { kEvents } = require('internal/event_target'); +const { kState } = require('internal/event_target'); async function onceAnEvent() { const ee = new EventEmitter(); @@ -80,7 +80,7 @@ async function catchesErrorsWithAbortSignal() { try { const promise = once(ee, 'myevent', { signal }); strictEqual(ee.listenerCount('error'), 1); - strictEqual(signal[kEvents].size, 1); + strictEqual(signal[kState].events.size, 1); await promise; } catch (e) { @@ -89,7 +89,7 @@ async function catchesErrorsWithAbortSignal() { strictEqual(err, expected); strictEqual(ee.listenerCount('error'), 0); strictEqual(ee.listenerCount('myevent'), 0); - strictEqual(signal[kEvents].size, 0); + strictEqual(signal[kState].events.size, 0); } async function stopListeningAfterCatchingError() { @@ -193,9 +193,9 @@ async function abortSignalAfterEvent() { ac.abort(); }); const promise = once(ee, 'foo', { signal: ac.signal }); - strictEqual(ac.signal[kEvents].size, 1); + strictEqual(ac.signal[kState].events.size, 1); await promise; - strictEqual(ac.signal[kEvents].size, 0); + strictEqual(ac.signal[kState].events.size, 0); } async function abortSignalRemoveListener() {