Skip to content

Commit

Permalink
events,bootstrap: make globalThis extend EventTarget
Browse files Browse the repository at this point in the history
Fixes nodejs#45981

Changes:
- Moves all EventTarget symbols to one symbol, to prevent exposing 4
  symbols to globalThis.
- Fallback to `globalThis` as the this value in EventTarget if this is
  null or undefined. This is needed to make the "floating" methods work
  (ie. `addEventListener(...)`).
  • Loading branch information
KhafraDev committed Dec 27, 2022
1 parent 0bd8b43 commit 1cb12ae
Show file tree
Hide file tree
Showing 5 changed files with 77 additions and 60 deletions.
4 changes: 2 additions & 2 deletions lib/events.js
Original file line number Diff line number Diff line change
Expand Up @@ -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) {
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();
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
110 changes: 58 additions & 52 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: EventTarget.defaultMaxListeners,
maxEventTargetListenersWarned: false,
handlers: new SafeMap(),
};
}

class EventTarget {
Expand All @@ -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);
Expand Down Expand Up @@ -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');
Expand All @@ -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;
Expand All @@ -584,26 +585,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 +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);
}

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

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

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

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

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

Expand Down Expand Up @@ -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;
Expand Down Expand Up @@ -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) {
Expand Down Expand Up @@ -1030,34 +1034,36 @@ 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,
value: `get ${propName}`,
});

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,
Expand Down Expand Up @@ -1106,7 +1112,7 @@ module.exports = {
kNewListener,
kTrustEvent,
kRemoveListener,
kEvents,
kState,
kWeakHandler,
isEventTarget,
};
4 changes: 4 additions & 0 deletions test/common/index.js
Original file line number Diff line number Diff line change
Expand Up @@ -276,6 +276,10 @@ let knownGlobals = [
setInterval,
setTimeout,
queueMicrotask,
// EventTarget prototype
addEventListener,
removeEventListener,
dispatchEvent,
];

// TODO(@jasnell): This check can be temporary. AbortController is
Expand Down
Loading

0 comments on commit 1cb12ae

Please sign in to comment.