Skip to content

events,bootstrap: make globalThis extend EventTarget #45993

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Open
wants to merge 8 commits into
base: main
Choose a base branch
from
Open
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 3 additions & 0 deletions .eslintrc.js
Original file line number Diff line number Diff line change
@@ -342,5 +342,8 @@ module.exports = {
WritableStream: 'readable',
WritableStreamDefaultWriter: 'readable',
WritableStreamDefaultController: 'readable',
addEventListener: 'readable',
removeEventListener: 'readable',
dispatchEvent: 'readable',
},
};
6 changes: 6 additions & 0 deletions lib/.eslintrc.yaml
Original file line number Diff line number Diff line change
@@ -142,6 +142,8 @@ rules:
message: Use `const { WritableStreamDefaultWriter } = require('internal/webstreams/writablestream')` instead of the global.
- name: WritableStreamDefaultController
message: Use `const { WritableStreamDefaultController } = require('internal/webstreams/writablestream')` instead of the global.
- name: addEventListener
message: Use `const { addEventListener } = EventTarget.prototype` instead of the global.
- name: atob
message: Use `const { atob } = require('buffer');` instead of the global.
- name: btoa
@@ -160,6 +162,8 @@ rules:
message: Use `const { Crypto } = require('internal/crypto/webcrypto');` instead of the global.
- name: CryptoKey
message: Use `const { CryptoKey } = require('internal/crypto/webcrypto');` instead of the global.
- name: dispatchEvent
message: Use `const { dispatchEvent } = EventTarget.prototype` instead of the global.
- name: fetch
message: Use `const { fetch } = require('internal/deps/undici/undici');` instead of the global.
- name: global
@@ -170,6 +174,8 @@ rules:
message: Use `const { performance } = require('perf_hooks');` instead of the global.
- name: queueMicrotask
message: Use `const { queueMicrotask } = require('internal/process/task_queues');` instead of the global.
- name: removeEventListener
message: Use `const { removeEventListener } = EventTarget.prototype` instead of the global.
- name: setImmediate
message: Use `const { setImmediate } = require('timers');` instead of the global.
- name: setInterval
16 changes: 10 additions & 6 deletions lib/events.js
Original file line number Diff line number Diff line change
@@ -267,6 +267,7 @@ EventEmitter.prototype._maxListeners = undefined;
// added to it. This is a useful default which helps finding memory leaks.
let defaultMaxListeners = 10;
let isEventTarget;
let eventTargetStateSymbol;

function checkListener(listener) {
validateFunction(listener, 'listener');
@@ -318,14 +319,17 @@ EventEmitter.setMaxListeners =
if (eventTargets.length === 0) {
defaultMaxListeners = n;
} else {
if (isEventTarget === undefined)
isEventTarget = require('internal/event_target').isEventTarget;
if (isEventTarget === undefined) {
const eventTarget = require('internal/event_target');
isEventTarget = eventTarget.isEventTarget;
eventTargetStateSymbol = eventTarget.kState;
}

for (let i = 0; i < eventTargets.length; i++) {
const target = eventTargets[i];
if (isEventTarget(target)) {
target[kMaxEventTargetListeners] = n;
target[kMaxEventTargetListenersWarned] = false;
target[eventTargetStateSymbol].maxEventTargetListeners = n;
target[eventTargetStateSymbol].maxEventTargetListenersWarned = false;
} else if (typeof target.setMaxListeners === 'function') {
target.setMaxListeners(n);
} else {
@@ -904,9 +908,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) {
9 changes: 8 additions & 1 deletion lib/internal/bootstrap/browser.js
Original file line number Diff line number Diff line change
@@ -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,
92 changes: 47 additions & 45 deletions lib/internal/event_target.js
Original file line number Diff line number Diff line change
@@ -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: EventEmitter.defaultMaxListeners,
maxEventTargetListenersWarned: false,
handlers: new SafeMap(),
};
}

class EventTarget {
@@ -506,10 +505,10 @@ class EventTarget {
}

[kNewListener](size, type, listener, once, capture, passive, weak) {
if (this[kMaxEventTargetListeners] > 0 &&
size > this[kMaxEventTargetListeners] &&
!this[kMaxEventTargetListenersWarned]) {
this[kMaxEventTargetListenersWarned] = true;
if (this[kState].maxEventTargetListeners > 0 &&
size > this[kState].maxEventTargetListeners &&
!this[kState].maxEventTargetListenersWarned) {
this[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. ' +
@@ -545,7 +544,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 +568,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,26 +584,26 @@ 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,
once,
capture,
passive,
weak);
this[kEvents].set(type, root);
self[kState].events.set(type, root);
return;
}

@@ -623,7 +623,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 +634,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 +645,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 +655,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 +667,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 +679,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 +698,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;
@@ -812,7 +814,7 @@ class NodeEventTarget extends EventTarget {
getMaxListeners() {
if (!isNodeEventTarget(this))
throw new ERR_INVALID_THIS('NodeEventTarget');
return this[kMaxEventTargetListeners];
return this[kState].maxEventTargetListeners;
}

/**
@@ -821,7 +823,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 +833,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 +926,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 +993,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,34 +1032,34 @@ 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;
validateInternalField(this?.[kState], 'handlers', 'EventTarget');
return this[kState].handlers?.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);
validateInternalField(this?.[kState], 'handlers', 'EventTarget');
let wrappedHandler = this[kState].handlers?.get(event);
if (wrappedHandler) {
if (typeof wrappedHandler.handler === 'function') {
this[kEvents].get(event).size--;
const size = this[kEvents].get(event).size;
this[kState].events.get(event).size--;
const size = this[kState].events.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[kState].events.get(event).size++;
const size = this[kState].events.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);
this[kState].handlers.set(event, wrappedHandler);
}
ObjectDefineProperty(set, 'name', {
__proto__: null,
@@ -1106,7 +1108,7 @@ module.exports = {
kNewListener,
kTrustEvent,
kRemoveListener,
kEvents,
kState,
kWeakHandler,
isEventTarget,
};
3 changes: 3 additions & 0 deletions test/common/index.js
Original file line number Diff line number Diff line change
@@ -276,6 +276,9 @@ let knownGlobals = [
setInterval,
setTimeout,
queueMicrotask,
EventTarget.prototype.addEventListener,
EventTarget.prototype.removeEventListener,
EventTarget.prototype.dispatchEvent,
];

// TODO(@jasnell): This check can be temporary. AbortController is
10 changes: 5 additions & 5 deletions test/parallel/test-events-once.js
Original file line number Diff line number Diff line change
@@ -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() {
2 changes: 0 additions & 2 deletions test/parallel/test-eventtarget.js
Original file line number Diff line number Diff line change
@@ -479,8 +479,6 @@ let asyncTest = Promise.resolve();
{},
[],
1,
null,
undefined,
false,
Symbol(),
/a/,
3 changes: 0 additions & 3 deletions test/wpt/status/dom/events.json
Original file line number Diff line number Diff line change
@@ -29,9 +29,6 @@
"EventListener-addEventListener.sub.window.js": {
"skip": "document is not defined"
},
"EventTarget-removeEventListener.any.js": {
"skip": "globalThis.removeEventListener is not a function"
},
"event-global-extra.window.js": {
"skip": "document is not defined"
},