Skip to content
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

events,bootstrap: make globalThis extend EventTarget #45993

Open
wants to merge 8 commits into
base: main
Choose a base branch
from
Open
Show file tree
Hide file tree
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
Expand Up @@ -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
Expand Up @@ -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
Expand All @@ -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
Expand All @@ -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
Expand Down
16 changes: 10 additions & 6 deletions lib/events.js
Original file line number Diff line number Diff line change
Expand Up @@ -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');
Expand Down Expand Up @@ -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 {
Expand Down Expand Up @@ -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) {
Expand Down
9 changes: 8 additions & 1 deletion lib/internal/bootstrap/browser.js
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@

const {
ObjectDefineProperty,
ObjectSetPrototypeOf,
globalThis,
} = primordials;

Expand Down Expand Up @@ -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();
KhafraDev marked this conversation as resolved.
Show resolved Hide resolved
exposeLazyInterfaces(globalThis, 'internal/worker/io', [
'MessageChannel', 'MessagePort', 'MessageEvent',
]);
Expand Down Expand Up @@ -103,6 +105,11 @@ function exposeGetterAndSetter(target, name, getter, setter = undefined) {
});
}

function setGlobalThisPrototype() {
initEventTarget(globalThis);
ObjectSetPrototypeOf(globalThis, EventTarget.prototype);
}

// Web Streams API
exposeLazyInterfaces(
globalThis,
Expand Down
92 changes: 47 additions & 45 deletions lib/internal/event_target.js
Original file line number Diff line number Diff line change
Expand Up @@ -24,6 +24,7 @@ const {
Symbol,
SymbolFor,
SymbolToStringTag,
globalThis,
} = primordials;

const {
Expand All @@ -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');
Expand Down Expand Up @@ -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 {
Expand All @@ -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. ' +
Expand Down Expand Up @@ -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');
Expand All @@ -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;
Expand All @@ -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;
}

Expand All @@ -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);
}

/**
Expand All @@ -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');
Expand All @@ -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;

Expand All @@ -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;
Expand All @@ -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');
Expand All @@ -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;
}
Expand All @@ -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;
Expand Down Expand Up @@ -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;
}

/**
Expand All @@ -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());
}

/**
Expand All @@ -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;
}

Expand Down Expand Up @@ -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;
Expand Down Expand Up @@ -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) {
Expand Down Expand Up @@ -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,
Expand Down Expand Up @@ -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
Expand Up @@ -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
Expand Down
Loading