Skip to content

Commit acabe08

Browse files
benjamingrdanielleadams
authored andcommitted
lib: add weak event handlers
PR-URL: #36607 Reviewed-By: James M Snell <jasnell@gmail.com> Reviewed-By: Gus Caplan <me@gus.host>
1 parent 9dac99a commit acabe08

File tree

4 files changed

+95
-17
lines changed

4 files changed

+95
-17
lines changed

lib/events.js

+3-1
Original file line numberDiff line numberDiff line change
@@ -697,7 +697,9 @@ function getEventListeners(emitterOrTarget, type) {
697697
const listeners = [];
698698
let handler = root?.next;
699699
while (handler?.listener !== undefined) {
700-
ArrayPrototypePush(listeners, handler.listener);
700+
const listener = handler.listener?.deref ?
701+
handler.listener.deref() : handler.listener;
702+
ArrayPrototypePush(listeners, listener);
701703
handler = handler.next;
702704
}
703705
return listeners;

lib/internal/event_target.js

+53-13
Original file line numberDiff line numberDiff line change
@@ -15,11 +15,12 @@ const {
1515
ReflectApply,
1616
SafeArrayIterator,
1717
SafeMap,
18+
SafeWeakMap,
19+
SafeWeakSet,
1820
String,
1921
Symbol,
2022
SymbolFor,
2123
SymbolToStringTag,
22-
SafeWeakSet,
2324
} = primordials;
2425

2526
const {
@@ -47,6 +48,7 @@ const kEvents = Symbol('kEvents');
4748
const kStop = Symbol('kStop');
4849
const kTarget = Symbol('kTarget');
4950
const kHandlers = Symbol('khandlers');
51+
const kWeakHandler = Symbol('kWeak');
5052

5153
const kHybridDispatch = SymbolFor('nodejs.internal.kHybridDispatch');
5254
const kCreateEvent = Symbol('kCreateEvent');
@@ -190,6 +192,21 @@ class NodeCustomEvent extends Event {
190192
}
191193
}
192194
}
195+
196+
// Weak listener cleanup
197+
// This has to be lazy for snapshots to work
198+
let weakListenersState = null;
199+
// The resource needs to retain the callback so that it doesn't
200+
// get garbage collected now that it's weak.
201+
let objectToWeakListenerMap = null;
202+
function weakListeners() {
203+
weakListenersState ??= new globalThis.FinalizationRegistry(
204+
(listener) => listener.remove()
205+
);
206+
objectToWeakListenerMap ??= new SafeWeakMap();
207+
return { registry: weakListenersState, map: objectToWeakListenerMap };
208+
}
209+
193210
// The listeners for an EventTarget are maintained as a linked list.
194211
// Unfortunately, the way EventTarget is defined, listeners are accounted
195212
// using the tuple [handler,capture], and even if we don't actually make
@@ -198,7 +215,8 @@ class NodeCustomEvent extends Event {
198215
// the linked list makes dispatching faster, even if adding/removing is
199216
// slower.
200217
class Listener {
201-
constructor(previous, listener, once, capture, passive, isNodeStyleListener) {
218+
constructor(previous, listener, once, capture, passive,
219+
isNodeStyleListener, weak) {
202220
this.next = undefined;
203221
if (previous !== undefined)
204222
previous.next = this;
@@ -210,15 +228,26 @@ class Listener {
210228
this.passive = passive;
211229
this.isNodeStyleListener = isNodeStyleListener;
212230
this.removed = false;
213-
214-
this.callback =
215-
typeof listener === 'function' ?
216-
listener :
217-
FunctionPrototypeBind(listener.handleEvent, listener);
231+
this.weak = Boolean(weak); // Don't retain the object
232+
233+
if (this.weak) {
234+
this.callback = new globalThis.WeakRef(listener);
235+
weakListeners().registry.register(listener, this, this);
236+
// Make the retainer retain the listener in a WeakMap
237+
weakListeners().map.set(weak, listener);
238+
this.listener = this.callback;
239+
} else if (typeof listener === 'function') {
240+
this.callback = listener;
241+
this.listener = listener;
242+
} else {
243+
this.callback = FunctionPrototypeBind(listener.handleEvent, listener);
244+
this.listener = listener;
245+
}
218246
}
219247

220248
same(listener, capture) {
221-
return this.listener === listener && this.capture === capture;
249+
const myListener = this.weak ? this.listener.deref() : this.listener;
250+
return myListener === listener && this.capture === capture;
222251
}
223252

224253
remove() {
@@ -227,6 +256,8 @@ class Listener {
227256
if (this.next !== undefined)
228257
this.next.previous = this.previous;
229258
this.removed = true;
259+
if (this.weak)
260+
weakListeners().registry.unregister(this);
230261
}
231262
}
232263

@@ -277,7 +308,8 @@ class EventTarget {
277308
capture,
278309
passive,
279310
signal,
280-
isNodeStyleListener
311+
isNodeStyleListener,
312+
weak,
281313
} = validateEventListenerOptions(options);
282314

283315
if (!shouldAddListener(listener)) {
@@ -302,15 +334,16 @@ class EventTarget {
302334
// not prevent the event target from GC.
303335
signal.addEventListener('abort', () => {
304336
this.removeEventListener(type, listener, options);
305-
}, { once: true });
337+
}, { once: true, [kWeakHandler]: this });
306338
}
307339

308340
let root = this[kEvents].get(type);
309341

310342
if (root === undefined) {
311343
root = { size: 1, next: undefined };
312344
// This is the first handler in our linked list.
313-
new Listener(root, listener, once, capture, passive, isNodeStyleListener);
345+
new Listener(root, listener, once, capture, passive,
346+
isNodeStyleListener, weak);
314347
this[kNewListener](root.size, type, listener, once, capture, passive);
315348
this[kEvents].set(type, root);
316349
return;
@@ -330,7 +363,7 @@ class EventTarget {
330363
}
331364

332365
new Listener(previous, listener, once, capture, passive,
333-
isNodeStyleListener);
366+
isNodeStyleListener, weak);
334367
root.size++;
335368
this[kNewListener](root.size, type, listener, once, capture, passive);
336369
}
@@ -418,7 +451,12 @@ class EventTarget {
418451
} else {
419452
arg = createEvent();
420453
}
421-
const result = FunctionPrototypeCall(handler.callback, this, arg);
454+
const callback = handler.weak ?
455+
handler.callback.deref() : handler.callback;
456+
let result;
457+
if (callback) {
458+
result = FunctionPrototypeCall(callback, this, arg);
459+
}
422460
if (result !== undefined && result !== null)
423461
addCatch(this, result, createEvent());
424462
} catch (err) {
@@ -569,6 +607,7 @@ function validateEventListenerOptions(options) {
569607
capture: Boolean(options.capture),
570608
passive: Boolean(options.passive),
571609
signal: options.signal,
610+
weak: options[kWeakHandler],
572611
isNodeStyleListener: Boolean(options[kIsNodeStyleListener])
573612
};
574613
}
@@ -671,5 +710,6 @@ module.exports = {
671710
kTrustEvent,
672711
kRemoveListener,
673712
kEvents,
713+
kWeakHandler,
674714
isEventTarget,
675715
};

test/parallel/test-events-static-geteventlisteners.js

+10-1
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
'use strict';
2-
2+
// Flags: --expose-internals --no-warnings
33
const common = require('../common');
4+
const { kWeakHandler } = require('internal/event_target');
45

56
const {
67
deepStrictEqual,
@@ -41,3 +42,11 @@ const { getEventListeners, EventEmitter } = require('events');
4142
getEventListeners('INVALID_EMITTER');
4243
}, /ERR_INVALID_ARG_TYPE/);
4344
}
45+
{
46+
// Test weak listeners
47+
const target = new EventTarget();
48+
const fn = common.mustNotCall();
49+
target.addEventListener('foo', fn, { [kWeakHandler]: {} });
50+
const listeners = getEventListeners(target, 'foo');
51+
deepStrictEqual(listeners, [fn]);
52+
}

test/parallel/test-eventtarget.js

+29-2
Original file line numberDiff line numberDiff line change
@@ -1,8 +1,11 @@
1-
// Flags: --expose-internals --no-warnings
1+
// Flags: --expose-internals --no-warnings --expose-gc
22
'use strict';
33

44
const common = require('../common');
5-
const { defineEventHandler } = require('internal/event_target');
5+
const {
6+
defineEventHandler,
7+
kWeakHandler,
8+
} = require('internal/event_target');
69

710
const {
811
ok,
@@ -570,3 +573,27 @@ let asyncTest = Promise.resolve();
570573
const et = new EventTarget();
571574
strictEqual(et.constructor.name, 'EventTarget');
572575
}
576+
{
577+
// Weak event handlers work
578+
const et = new EventTarget();
579+
const listener = common.mustCall();
580+
et.addEventListener('foo', listener, { [kWeakHandler]: et });
581+
et.dispatchEvent(new Event('foo'));
582+
}
583+
{
584+
// Weak event handlers can be removed and weakness is not part of the key
585+
const et = new EventTarget();
586+
const listener = common.mustNotCall();
587+
et.addEventListener('foo', listener, { [kWeakHandler]: et });
588+
et.removeEventListener('foo', listener);
589+
et.dispatchEvent(new Event('foo'));
590+
}
591+
{
592+
// Test listeners are held weakly
593+
const et = new EventTarget();
594+
et.addEventListener('foo', common.mustNotCall(), { [kWeakHandler]: {} });
595+
setImmediate(() => {
596+
global.gc();
597+
et.dispatchEvent(new Event('foo'));
598+
});
599+
}

0 commit comments

Comments
 (0)