Skip to content

Commit 1cb12ae

Browse files
committed
events,bootstrap: make globalThis extend EventTarget
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(...)`).
1 parent 0bd8b43 commit 1cb12ae

File tree

5 files changed

+77
-60
lines changed

5 files changed

+77
-60
lines changed

lib/events.js

+2-2
Original file line numberDiff line numberDiff line change
@@ -904,9 +904,9 @@ function getEventListeners(emitterOrTarget, type) {
904904
return emitterOrTarget.listeners(type);
905905
}
906906
// Require event target lazily to avoid always loading it
907-
const { isEventTarget, kEvents } = require('internal/event_target');
907+
const { isEventTarget, kState } = require('internal/event_target');
908908
if (isEventTarget(emitterOrTarget)) {
909-
const root = emitterOrTarget[kEvents].get(type);
909+
const root = emitterOrTarget[kState].events.get(type);
910910
const listeners = [];
911911
let handler = root?.next;
912912
while (handler?.listener !== undefined) {

lib/internal/bootstrap/browser.js

+8-1
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,7 @@
22

33
const {
44
ObjectDefineProperty,
5+
ObjectSetPrototypeOf,
56
globalThis,
67
} = primordials;
78

@@ -43,10 +44,11 @@ exposeLazyInterfaces(globalThis, 'internal/abort_controller', [
4344
'AbortController', 'AbortSignal',
4445
]);
4546
const {
46-
EventTarget, Event,
47+
EventTarget, Event, initEventTarget,
4748
} = require('internal/event_target');
4849
exposeInterface(globalThis, 'Event', Event);
4950
exposeInterface(globalThis, 'EventTarget', EventTarget);
51+
setGlobalThisPrototype();
5052
exposeLazyInterfaces(globalThis, 'internal/worker/io', [
5153
'MessageChannel', 'MessagePort', 'MessageEvent',
5254
]);
@@ -103,6 +105,11 @@ function exposeGetterAndSetter(target, name, getter, setter = undefined) {
103105
});
104106
}
105107

108+
function setGlobalThisPrototype() {
109+
initEventTarget(globalThis);
110+
ObjectSetPrototypeOf(globalThis, EventTarget.prototype);
111+
}
112+
106113
// Web Streams API
107114
exposeLazyInterfaces(
108115
globalThis,

lib/internal/event_target.js

+58-52
Original file line numberDiff line numberDiff line change
@@ -24,6 +24,7 @@ const {
2424
Symbol,
2525
SymbolFor,
2626
SymbolToStringTag,
27+
globalThis,
2728
} = primordials;
2829

2930
const {
@@ -47,16 +48,11 @@ const kIsEventTarget = SymbolFor('nodejs.event_target');
4748
const kIsNodeEventTarget = Symbol('kIsNodeEventTarget');
4849

4950
const EventEmitter = require('events');
50-
const {
51-
kMaxEventTargetListeners,
52-
kMaxEventTargetListenersWarned,
53-
} = EventEmitter;
5451

55-
const kEvents = Symbol('kEvents');
52+
const kState = Symbol('nodejs.internal.eventTargetState');
5653
const kIsBeingDispatched = Symbol('kIsBeingDispatched');
5754
const kStop = Symbol('kStop');
5855
const kTarget = Symbol('kTarget');
59-
const kHandlers = Symbol('khandlers');
6056
const kWeakHandler = Symbol('kWeak');
6157

6258
const kHybridDispatch = SymbolFor('nodejs.internal.kHybridDispatch');
@@ -489,10 +485,13 @@ class Listener {
489485
}
490486

491487
function initEventTarget(self) {
492-
self[kEvents] = new SafeMap();
493-
self[kMaxEventTargetListeners] = EventEmitter.defaultMaxListeners;
494-
self[kMaxEventTargetListenersWarned] = false;
495-
self[kHandlers] = new SafeMap();
488+
self[kState] = {
489+
__proto__: null,
490+
events: new SafeMap(),
491+
maxEventTargetListeners: EventTarget.defaultMaxListeners,
492+
maxEventTargetListenersWarned: false,
493+
handlers: new SafeMap(),
494+
};
496495
}
497496

498497
class EventTarget {
@@ -506,18 +505,19 @@ class EventTarget {
506505
}
507506

508507
[kNewListener](size, type, listener, once, capture, passive, weak) {
509-
if (this[kMaxEventTargetListeners] > 0 &&
510-
size > this[kMaxEventTargetListeners] &&
511-
!this[kMaxEventTargetListenersWarned]) {
512-
this[kMaxEventTargetListenersWarned] = true;
508+
const self = this ?? globalThis
509+
if (self[kState].maxEventTargetListeners > 0 &&
510+
size > self[kState].maxEventTargetListeners &&
511+
!self[kState].maxEventTargetListenersWarned) {
512+
self[kState].maxEventTargetListenersWarned = true;
513513
// No error code for this since it is a Warning
514514
// eslint-disable-next-line no-restricted-syntax
515515
const w = new Error('Possible EventTarget memory leak detected. ' +
516516
`${size} ${type} listeners ` +
517-
`added to ${inspect(this, { depth: -1 })}. Use ` +
517+
`added to ${inspect(self, { depth: -1 })}. Use ` +
518518
'events.setMaxListeners() to increase limit');
519519
w.name = 'MaxListenersExceededWarning';
520-
w.target = this;
520+
w.target = self;
521521
w.type = type;
522522
w.count = size;
523523
process.emitWarning(w);
@@ -545,7 +545,8 @@ class EventTarget {
545545
* }} [options]
546546
*/
547547
addEventListener(type, listener, options = kEmptyObject) {
548-
if (!isEventTarget(this))
548+
const self = this ?? globalThis
549+
if (!isEventTarget(self))
549550
throw new ERR_INVALID_THIS('EventTarget');
550551
if (arguments.length < 2)
551552
throw new ERR_MISSING_ARGS('type', 'listener');
@@ -568,7 +569,7 @@ class EventTarget {
568569
const w = new Error(`addEventListener called with ${listener}` +
569570
' which has no effect.');
570571
w.name = 'AddEventListenerArgumentTypeWarning';
571-
w.target = this;
572+
w.target = self;
572573
w.type = type;
573574
process.emitWarning(w);
574575
return;
@@ -584,26 +585,26 @@ class EventTarget {
584585
// TODO(benjamingr) make this weak somehow? ideally the signal would
585586
// not prevent the event target from GC.
586587
signal.addEventListener('abort', () => {
587-
this.removeEventListener(type, listener, options);
588-
}, { once: true, [kWeakHandler]: this });
588+
self.removeEventListener(type, listener, options);
589+
}, { once: true, [kWeakHandler]: self });
589590
}
590591

591-
let root = this[kEvents].get(type);
592+
let root = self[kState].events.get(type);
592593

593594
if (root === undefined) {
594595
root = { size: 1, next: undefined };
595596
// This is the first handler in our linked list.
596597
new Listener(root, listener, once, capture, passive,
597598
isNodeStyleListener, weak);
598-
this[kNewListener](
599+
self[kNewListener](
599600
root.size,
600601
type,
601602
listener,
602603
once,
603604
capture,
604605
passive,
605606
weak);
606-
this[kEvents].set(type, root);
607+
self[kState].events.set(type, root);
607608
return;
608609
}
609610

@@ -623,7 +624,7 @@ class EventTarget {
623624
new Listener(previous, listener, once, capture, passive,
624625
isNodeStyleListener, weak);
625626
root.size++;
626-
this[kNewListener](root.size, type, listener, once, capture, passive, weak);
627+
self[kNewListener](root.size, type, listener, once, capture, passive, weak);
627628
}
628629

629630
/**
@@ -634,7 +635,8 @@ class EventTarget {
634635
* }} [options]
635636
*/
636637
removeEventListener(type, listener, options = kEmptyObject) {
637-
if (!isEventTarget(this))
638+
const self = this ?? globalThis
639+
if (!isEventTarget(self))
638640
throw new ERR_INVALID_THIS('EventTarget');
639641
if (arguments.length < 2)
640642
throw new ERR_MISSING_ARGS('type', 'listener');
@@ -644,7 +646,7 @@ class EventTarget {
644646
type = String(type);
645647
const capture = options?.capture === true;
646648

647-
const root = this[kEvents].get(type);
649+
const root = self[kState].events.get(type);
648650
if (root === undefined || root.next === undefined)
649651
return;
650652

@@ -654,8 +656,8 @@ class EventTarget {
654656
handler.remove();
655657
root.size--;
656658
if (root.size === 0)
657-
this[kEvents].delete(type);
658-
this[kRemoveListener](root.size, type, listener, capture);
659+
self[kState].events.delete(type);
660+
self[kRemoveListener](root.size, type, listener, capture);
659661
break;
660662
}
661663
handler = handler.next;
@@ -666,7 +668,8 @@ class EventTarget {
666668
* @param {Event} event
667669
*/
668670
dispatchEvent(event) {
669-
if (!isEventTarget(this))
671+
const self = this ?? globalThis
672+
if (!isEventTarget(self))
670673
throw new ERR_INVALID_THIS('EventTarget');
671674
if (arguments.length < 1)
672675
throw new ERR_MISSING_ARGS('event');
@@ -677,7 +680,7 @@ class EventTarget {
677680
if (event[kIsBeingDispatched])
678681
throw new ERR_EVENT_RECURSION(event.type);
679682

680-
this[kHybridDispatch](event, event.type, event);
683+
self[kHybridDispatch](event, event.type, event);
681684

682685
return event.defaultPrevented !== true;
683686
}
@@ -696,7 +699,7 @@ class EventTarget {
696699
event[kIsBeingDispatched] = true;
697700
}
698701

699-
const root = this[kEvents].get(type);
702+
const root = this[kState].events.get(type);
700703
if (root === undefined || root.next === undefined) {
701704
if (event !== undefined)
702705
event[kIsBeingDispatched] = false;
@@ -757,9 +760,10 @@ class EventTarget {
757760
return new NodeCustomEvent(type, { detail: nodeValue });
758761
}
759762
[customInspectSymbol](depth, options) {
760-
if (!isEventTarget(this))
763+
const self = this ?? globalThis
764+
if (!isEventTarget(self))
761765
throw new ERR_INVALID_THIS('EventTarget');
762-
const name = this.constructor.name;
766+
const name = 'EventTarget';
763767
if (depth < 0)
764768
return name;
765769

@@ -812,7 +816,7 @@ class NodeEventTarget extends EventTarget {
812816
getMaxListeners() {
813817
if (!isNodeEventTarget(this))
814818
throw new ERR_INVALID_THIS('NodeEventTarget');
815-
return this[kMaxEventTargetListeners];
819+
return this[kState].maxEventTargetListeners;
816820
}
817821

818822
/**
@@ -821,7 +825,7 @@ class NodeEventTarget extends EventTarget {
821825
eventNames() {
822826
if (!isNodeEventTarget(this))
823827
throw new ERR_INVALID_THIS('NodeEventTarget');
824-
return ArrayFrom(this[kEvents].keys());
828+
return ArrayFrom(this[kState].events.keys());
825829
}
826830

827831
/**
@@ -831,7 +835,7 @@ class NodeEventTarget extends EventTarget {
831835
listenerCount(type) {
832836
if (!isNodeEventTarget(this))
833837
throw new ERR_INVALID_THIS('NodeEventTarget');
834-
const root = this[kEvents].get(String(type));
838+
const root = this[kState].events.get(String(type));
835839
return root !== undefined ? root.size : 0;
836840
}
837841

@@ -924,9 +928,9 @@ class NodeEventTarget extends EventTarget {
924928
if (!isNodeEventTarget(this))
925929
throw new ERR_INVALID_THIS('NodeEventTarget');
926930
if (type !== undefined) {
927-
this[kEvents].delete(String(type));
931+
this[kState].events.delete(String(type));
928932
} else {
929-
this[kEvents].clear();
933+
this[kState].events.clear();
930934
}
931935

932936
return this;
@@ -991,7 +995,7 @@ function validateEventListenerOptions(options) {
991995
// It stands in its current implementation as a compromise.
992996
// Ref: https://github.com/nodejs/node/pull/33661
993997
function isEventTarget(obj) {
994-
return obj?.constructor?.[kIsEventTarget];
998+
return obj.constructor?.[kIsEventTarget] || obj === globalThis;
995999
}
9961000

9971001
function isNodeEventTarget(obj) {
@@ -1030,34 +1034,36 @@ function defineEventHandler(emitter, name, event = name) {
10301034
// 8.1.5.1 Event handlers - basically `on[eventName]` attributes
10311035
const propName = `on${name}`;
10321036
function get() {
1033-
validateInternalField(this, kHandlers, 'EventTarget');
1034-
return this[kHandlers]?.get(event)?.handler ?? null;
1037+
const self = this ?? globalThis;
1038+
validateInternalField(self[kState], 'handlers', 'EventTarget');
1039+
return self[kState].handlers?.get(event)?.handler ?? null;
10351040
}
10361041
ObjectDefineProperty(get, 'name', {
10371042
__proto__: null,
10381043
value: `get ${propName}`,
10391044
});
10401045

10411046
function set(value) {
1042-
validateInternalField(this, kHandlers, 'EventTarget');
1043-
let wrappedHandler = this[kHandlers]?.get(event);
1047+
const self = this ?? globalThis;
1048+
validateInternalField(self[kState], 'handlers', 'EventTarget');
1049+
let wrappedHandler = self[kState].handlers?.get(event);
10441050
if (wrappedHandler) {
10451051
if (typeof wrappedHandler.handler === 'function') {
1046-
this[kEvents].get(event).size--;
1047-
const size = this[kEvents].get(event).size;
1048-
this[kRemoveListener](size, event, wrappedHandler.handler, false);
1052+
self[kState].events.get(event).size--;
1053+
const size = self[kState].events.get(event).size;
1054+
self[kRemoveListener](size, event, wrappedHandler.handler, false);
10491055
}
10501056
wrappedHandler.handler = value;
10511057
if (typeof wrappedHandler.handler === 'function') {
1052-
this[kEvents].get(event).size++;
1053-
const size = this[kEvents].get(event).size;
1054-
this[kNewListener](size, event, value, false, false, false, false);
1058+
self[kState].events.get(event).size++;
1059+
const size = self[kState].events.get(event).size;
1060+
self[kNewListener](size, event, value, false, false, false, false);
10551061
}
10561062
} else {
10571063
wrappedHandler = makeEventHandler(value);
1058-
this.addEventListener(event, wrappedHandler);
1064+
self.addEventListener(event, wrappedHandler);
10591065
}
1060-
this[kHandlers].set(event, wrappedHandler);
1066+
self[kState].handlers.set(event, wrappedHandler);
10611067
}
10621068
ObjectDefineProperty(set, 'name', {
10631069
__proto__: null,
@@ -1106,7 +1112,7 @@ module.exports = {
11061112
kNewListener,
11071113
kTrustEvent,
11081114
kRemoveListener,
1109-
kEvents,
1115+
kState,
11101116
kWeakHandler,
11111117
isEventTarget,
11121118
};

test/common/index.js

+4
Original file line numberDiff line numberDiff line change
@@ -276,6 +276,10 @@ let knownGlobals = [
276276
setInterval,
277277
setTimeout,
278278
queueMicrotask,
279+
// EventTarget prototype
280+
addEventListener,
281+
removeEventListener,
282+
dispatchEvent,
279283
];
280284

281285
// TODO(@jasnell): This check can be temporary. AbortController is

0 commit comments

Comments
 (0)