Skip to content

Commit c39f04c

Browse files
authored
events: remove weak listener for event target
Fixes: #48951 PR-URL: #48952 Reviewed-By: Chemi Atlow <[email protected]>
1 parent 3224527 commit c39f04c

File tree

3 files changed

+56
-8
lines changed

3 files changed

+56
-8
lines changed

lib/internal/event_target.js

Lines changed: 34 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -58,6 +58,7 @@ const kWeakHandler = Symbol('kWeak');
5858
const kResistStopPropagation = Symbol('kResistStopPropagation');
5959

6060
const kHybridDispatch = SymbolFor('nodejs.internal.kHybridDispatch');
61+
const kRemoveWeakListenerHelper = Symbol('nodejs.internal.removeWeakListenerHelper');
6162
const kCreateEvent = Symbol('kCreateEvent');
6263
const kNewListener = Symbol('kNewListener');
6364
const kRemoveListener = Symbol('kRemoveListener');
@@ -406,7 +407,7 @@ let weakListenersState = null;
406407
let objectToWeakListenerMap = null;
407408
function weakListeners() {
408409
weakListenersState ??= new SafeFinalizationRegistry(
409-
(listener) => listener.remove(),
410+
({ eventTarget, listener, eventType }) => eventTarget.deref()?.[kRemoveWeakListenerHelper](eventType, listener),
410411
);
411412
objectToWeakListenerMap ??= new SafeWeakMap();
412413
return { registry: weakListenersState, map: objectToWeakListenerMap };
@@ -428,7 +429,7 @@ const kFlagResistStopPropagation = 1 << 6;
428429
// the linked list makes dispatching faster, even if adding/removing is
429430
// slower.
430431
class Listener {
431-
constructor(previous, listener, once, capture, passive,
432+
constructor(eventTarget, eventType, previous, listener, once, capture, passive,
432433
isNodeStyleListener, weak, resistStopPropagation) {
433434
this.next = undefined;
434435
if (previous !== undefined)
@@ -455,7 +456,13 @@ class Listener {
455456

456457
if (this.weak) {
457458
this.callback = new SafeWeakRef(listener);
458-
weakListeners().registry.register(listener, this, this);
459+
weakListeners().registry.register(listener, {
460+
__proto__: null,
461+
// Weak ref so the listener won't hold the eventTarget alive
462+
eventTarget: new SafeWeakRef(eventTarget),
463+
listener: this,
464+
eventType,
465+
}, this);
459466
// Make the retainer retain the listener in a WeakMap
460467
weakListeners().map.set(weak, listener);
461468
this.listener = this.callback;
@@ -621,7 +628,7 @@ class EventTarget {
621628
if (root === undefined) {
622629
root = { size: 1, next: undefined, resistStopPropagation: Boolean(resistStopPropagation) };
623630
// This is the first handler in our linked list.
624-
new Listener(root, listener, once, capture, passive,
631+
new Listener(this, type, root, listener, once, capture, passive,
625632
isNodeStyleListener, weak, resistStopPropagation);
626633
this[kNewListener](
627634
root.size,
@@ -648,7 +655,7 @@ class EventTarget {
648655
return;
649656
}
650657

651-
new Listener(previous, listener, once, capture, passive,
658+
new Listener(this, type, previous, listener, once, capture, passive,
652659
isNodeStyleListener, weak, resistStopPropagation);
653660
root.size++;
654661
root.resistStopPropagation ||= Boolean(resistStopPropagation);
@@ -691,6 +698,28 @@ class EventTarget {
691698
}
692699
}
693700

701+
[kRemoveWeakListenerHelper](type, listener) {
702+
const root = this[kEvents].get(type);
703+
if (root === undefined || root.next === undefined)
704+
return;
705+
706+
const capture = listener.capture === true;
707+
708+
let handler = root.next;
709+
while (handler !== undefined) {
710+
if (handler === listener) {
711+
handler.remove();
712+
root.size--;
713+
if (root.size === 0)
714+
this[kEvents].delete(type);
715+
// Undefined is passed as the listener as the listener was GCed
716+
this[kRemoveListener](root.size, type, undefined, capture);
717+
break;
718+
}
719+
handler = handler.next;
720+
}
721+
}
722+
694723
/**
695724
* @param {Event} event
696725
*/

test/parallel/test-eventtarget-memoryleakwarning.js

Lines changed: 20 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,11 +1,13 @@
1-
// Flags: --no-warnings
1+
// Flags: --expose-internals --no-warnings --expose-gc
22
'use strict';
33
const common = require('../common');
44
const {
55
setMaxListeners,
66
EventEmitter,
77
} = require('events');
88
const assert = require('assert');
9+
const { kWeakHandler } = require('internal/event_target');
10+
const { setTimeout } = require('timers/promises');
911

1012
common.expectWarning({
1113
MaxListenersExceededWarning: [
@@ -73,3 +75,20 @@ common.expectWarning({
7375
setMaxListeners(2, ee);
7476
assert.strictEqual(ee.getMaxListeners(), 2);
7577
}
78+
79+
{
80+
(async () => {
81+
// Test that EventTarget listener don't emit MaxListenersExceededWarning for weak listeners that GCed
82+
const et = new EventTarget();
83+
setMaxListeners(2, et);
84+
85+
for (let i = 0; i <= 3; i++) {
86+
et.addEventListener('foo', () => {}, {
87+
[kWeakHandler]: {},
88+
});
89+
90+
await setTimeout(0);
91+
global.gc();
92+
}
93+
})().then(common.mustCall(), common.mustNotCall());
94+
}

test/parallel/test-eventtarget.js

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -16,8 +16,8 @@ const {
1616

1717
const { once } = require('events');
1818

19-
const { promisify, inspect } = require('util');
20-
const delay = promisify(setTimeout);
19+
const { inspect } = require('util');
20+
const { setTimeout: delay } = require('timers/promises');
2121

2222
// The globals are defined.
2323
ok(Event);

0 commit comments

Comments
 (0)