Skip to content

Commit cc5c8bb

Browse files
committed
lib: add weak event handlers
1 parent f0a0e3c commit cc5c8bb

File tree

5 files changed

+96
-20
lines changed

5 files changed

+96
-20
lines changed

.eslintrc.js

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -299,12 +299,14 @@ module.exports = {
299299
BigUint64Array: 'readable',
300300
Event: 'readable',
301301
EventTarget: 'readable',
302+
FinalizationRegistry: 'readable',
302303
MessageChannel: 'readable',
303304
MessageEvent: 'readable',
304305
MessagePort: 'readable',
305306
TextEncoder: 'readable',
306307
TextDecoder: 'readable',
307308
queueMicrotask: 'readable',
309+
WeakRef: 'readable',
308310
globalThis: 'readable',
309311
},
310312
};

lib/events.js

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -695,7 +695,9 @@ function getEventListeners(emitterOrTarget, type) {
695695
const listeners = [];
696696
let handler = root?.next;
697697
while (handler?.listener !== undefined) {
698-
ArrayPrototypePush(listeners, handler.listener);
698+
const listener = handler.listener?.deref ?
699+
handler.listener.deref() : handler.listener;
700+
ArrayPrototypePush(listeners, listener);
699701
handler = handler.next;
700702
}
701703
return listeners;

lib/internal/event_target.js

Lines changed: 44 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -45,6 +45,7 @@ const kEvents = Symbol('kEvents');
4545
const kStop = Symbol('kStop');
4646
const kTarget = Symbol('kTarget');
4747
const kHandlers = Symbol('khandlers');
48+
const kWeakHandler = Symbol('kWeak');
4849

4950
const kHybridDispatch = SymbolFor('nodejs.internal.kHybridDispatch');
5051
const kCreateEvent = Symbol('kCreateEvent');
@@ -188,6 +189,19 @@ class NodeCustomEvent extends Event {
188189
}
189190
}
190191
}
192+
193+
// Weak listener cleanup
194+
// This has to be lazy for snapshots to work
195+
let weakListenersState = null;
196+
function weakListeners() {
197+
if (!weakListenersState) {
198+
weakListenersState = new FinalizationRegistry(
199+
(listener) => listener.remove()
200+
);
201+
}
202+
return weakListenersState;
203+
}
204+
191205
// The listeners for an EventTarget are maintained as a linked list.
192206
// Unfortunately, the way EventTarget is defined, listeners are accounted
193207
// using the tuple [handler,capture], and even if we don't actually make
@@ -196,27 +210,36 @@ class NodeCustomEvent extends Event {
196210
// the linked list makes dispatching faster, even if adding/removing is
197211
// slower.
198212
class Listener {
199-
constructor(previous, listener, once, capture, passive, isNodeStyleListener) {
213+
constructor(previous, listener, once, capture, passive,
214+
isNodeStyleListener, weak) {
200215
this.next = undefined;
201216
if (previous !== undefined)
202217
previous.next = this;
203218
this.previous = previous;
204-
this.listener = listener;
205219
// TODO(benjamingr) these 4 can be 'flags' to save 3 slots
206220
this.once = once;
207221
this.capture = capture;
208222
this.passive = passive;
209223
this.isNodeStyleListener = isNodeStyleListener;
210224
this.removed = false;
211-
212-
this.callback =
213-
typeof listener === 'function' ?
214-
listener :
215-
listener.handleEvent.bind(listener);
225+
this.weak = weak;
226+
227+
if (this.weak) {
228+
this.callback = new WeakRef(listener);
229+
weakListeners().register(listener, this, this);
230+
this.listener = this.callback;
231+
} else if (typeof listener === 'function') {
232+
this.callback = listener;
233+
this.listener = listener;
234+
} else {
235+
this.callback = listener.handleEvent.bind(listener);
236+
this.listener = listener;
237+
}
216238
}
217239

218240
same(listener, capture) {
219-
return this.listener === listener && this.capture === capture;
241+
const myListener = this.weak ? this.listener.deref() : this.listener;
242+
return myListener === listener && this.capture === capture;
220243
}
221244

222245
remove() {
@@ -225,6 +248,8 @@ class Listener {
225248
if (this.next !== undefined)
226249
this.next.previous = this.previous;
227250
this.removed = true;
251+
if (this.weak)
252+
weakListeners().unregister(this);
228253
}
229254
}
230255

@@ -275,7 +300,8 @@ class EventTarget {
275300
capture,
276301
passive,
277302
signal,
278-
isNodeStyleListener
303+
isNodeStyleListener,
304+
weak,
279305
} = validateEventListenerOptions(options);
280306

281307
if (!shouldAddListener(listener)) {
@@ -296,19 +322,18 @@ class EventTarget {
296322
if (signal.aborted) {
297323
return false;
298324
}
299-
// TODO(benjamingr) make this weak somehow? ideally the signal would
300-
// not prevent the event target from GC.
301325
signal.addEventListener('abort', () => {
302326
this.removeEventListener(type, listener, options);
303-
}, { once: true });
327+
}, { once: true, [kWeakHandler]: true });
304328
}
305329

306330
let root = this[kEvents].get(type);
307331

308332
if (root === undefined) {
309333
root = { size: 1, next: undefined };
310334
// This is the first handler in our linked list.
311-
new Listener(root, listener, once, capture, passive, isNodeStyleListener);
335+
new Listener(root, listener, once, capture, passive,
336+
isNodeStyleListener, weak);
312337
this[kNewListener](root.size, type, listener, once, capture, passive);
313338
this[kEvents].set(type, root);
314339
return;
@@ -328,7 +353,7 @@ class EventTarget {
328353
}
329354

330355
new Listener(previous, listener, once, capture, passive,
331-
isNodeStyleListener);
356+
isNodeStyleListener, weak);
332357
root.size++;
333358
this[kNewListener](root.size, type, listener, once, capture, passive);
334359
}
@@ -419,7 +444,9 @@ class EventTarget {
419444
} else {
420445
arg = createEvent();
421446
}
422-
const result = handler.callback.call(this, arg);
447+
const callback = handler.weak ?
448+
handler.callback.deref() : handler.callback;
449+
const result = callback?.call(this, arg);
423450
if (result !== undefined && result !== null)
424451
addCatch(this, result, createEvent());
425452
} catch (err) {
@@ -573,6 +600,7 @@ function validateEventListenerOptions(options) {
573600
once: Boolean(options.once),
574601
capture: Boolean(options.capture),
575602
passive: Boolean(options.passive),
603+
weak: Boolean(options[kWeakHandler]),
576604
signal: options.signal,
577605
isNodeStyleListener: Boolean(options[kIsNodeStyleListener])
578606
};
@@ -675,5 +703,6 @@ module.exports = {
675703
kTrustEvent,
676704
kRemoveListener,
677705
kEvents,
706+
kWeakHandler,
678707
isEventTarget,
679708
};

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

Lines changed: 10 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
11
'use strict';
2-
2+
// Flags: --expose-internals --no-warnings
33
const common = require('../common');
4-
4+
const { kWeakHandler } = require('internal/event_target');
55
const {
66
deepStrictEqual,
77
} = require('assert');
@@ -34,3 +34,11 @@ const { getEventListeners, EventEmitter } = require('events');
3434
deepStrictEqual(getEventListeners(target, 'bar'), []);
3535
deepStrictEqual(getEventListeners(target, 'baz'), [fn1]);
3636
}
37+
{
38+
// Test weak listeners
39+
const target = new EventTarget();
40+
const fn = common.mustNotCall();
41+
target.addEventListener('foo', fn, { [kWeakHandler]: true });
42+
const listeners = getEventListeners(target, 'foo');
43+
deepStrictEqual(listeners, [fn]);
44+
}

test/parallel/test-eventtarget.js

Lines changed: 37 additions & 2 deletions
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,
@@ -541,3 +544,35 @@ let asyncTest = Promise.resolve();
541544
et.addEventListener('foo', listener);
542545
et.dispatchEvent(new Event('foo'));
543546
}
547+
{
548+
// Weak event handlers work
549+
const et = new EventTarget();
550+
const listener = common.mustCall();
551+
et.addEventListener('foo', listener, { [kWeakHandler]: true });
552+
et.dispatchEvent(new Event('foo'));
553+
}
554+
{
555+
// Weak event handlers can be removed and weakness is not part of the key
556+
const et = new EventTarget();
557+
const listener = common.mustNotCall();
558+
et.addEventListener('foo', listener, { [kWeakHandler]: true });
559+
et.removeEventListener('foo', listener);
560+
et.dispatchEvent(new Event('foo'));
561+
}
562+
{
563+
// Weak event handlers can be removed and weakness is not part of the key
564+
const et = new EventTarget();
565+
const listener = common.mustNotCall();
566+
et.addEventListener('foo', listener, { [kWeakHandler]: true });
567+
et.removeEventListener('foo', listener);
568+
et.dispatchEvent(new Event('foo'));
569+
}
570+
{
571+
// Test listeners are held weakly
572+
const et = new EventTarget();
573+
et.addEventListener('foo', common.mustNotCall(), { [kWeakHandler]: true });
574+
setImmediate(() => {
575+
global.gc();
576+
et.dispatchEvent(new Event('foo'));
577+
});
578+
}

0 commit comments

Comments
 (0)