Skip to content

Commit 855a85c

Browse files
benjamingrdanielleadams
authored andcommitted
events: support signal in EventTarget
PR-URL: #36258 Fixes: #36073 Reviewed-By: James M Snell <jasnell@gmail.com>
1 parent f317bba commit 855a85c

File tree

3 files changed

+186
-0
lines changed

3 files changed

+186
-0
lines changed

lib/internal/event_target.js

+22
Original file line numberDiff line numberDiff line change
@@ -200,10 +200,12 @@ class Listener {
200200
previous.next = this;
201201
this.previous = previous;
202202
this.listener = listener;
203+
// TODO(benjamingr) these 4 can be 'flags' to save 3 slots
203204
this.once = once;
204205
this.capture = capture;
205206
this.passive = passive;
206207
this.isNodeStyleListener = isNodeStyleListener;
208+
this.removed = false;
207209

208210
this.callback =
209211
typeof listener === 'function' ?
@@ -220,6 +222,7 @@ class Listener {
220222
this.previous.next = this.next;
221223
if (this.next !== undefined)
222224
this.next.previous = this.previous;
225+
this.removed = true;
223226
}
224227
}
225228

@@ -269,6 +272,7 @@ class EventTarget {
269272
once,
270273
capture,
271274
passive,
275+
signal,
272276
isNodeStyleListener
273277
} = validateEventListenerOptions(options);
274278

@@ -286,6 +290,17 @@ class EventTarget {
286290
}
287291
type = String(type);
288292

293+
if (signal) {
294+
if (signal.aborted) {
295+
return false;
296+
}
297+
// TODO(benjamingr) make this weak somehow? ideally the signal would
298+
// not prevent the event target from GC.
299+
signal.addEventListener('abort', () => {
300+
this.removeEventListener(type, listener, options);
301+
}, { once: true });
302+
}
303+
289304
let root = this[kEvents].get(type);
290305

291306
if (root === undefined) {
@@ -382,6 +397,12 @@ class EventTarget {
382397
// Cache the next item in case this iteration removes the current one
383398
next = handler.next;
384399

400+
if (handler.removed) {
401+
// Deal with the case an event is removed while event handlers are
402+
// Being processed (removeEventListener called from a listener)
403+
handler = next;
404+
continue;
405+
}
385406
if (handler.once) {
386407
handler.remove();
387408
root.size--;
@@ -550,6 +571,7 @@ function validateEventListenerOptions(options) {
550571
once: Boolean(options.once),
551572
capture: Boolean(options.capture),
552573
passive: Boolean(options.passive),
574+
signal: options.signal,
553575
isNodeStyleListener: Boolean(options[kIsNodeStyleListener])
554576
};
555577
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,155 @@
1+
'use strict';
2+
3+
require('../common');
4+
5+
const {
6+
strictEqual,
7+
} = require('assert');
8+
9+
// Manually ported from: wpt@dom/events/AddEventListenerOptions-signal.any.js
10+
11+
{
12+
// Passing an AbortSignal to addEventListener does not prevent
13+
// removeEventListener
14+
let count = 0;
15+
function handler() {
16+
count++;
17+
}
18+
const et = new EventTarget();
19+
const controller = new AbortController();
20+
et.addEventListener('test', handler, { signal: controller.signal });
21+
et.dispatchEvent(new Event('test'));
22+
strictEqual(count, 1, 'Adding a signal still adds a listener');
23+
et.dispatchEvent(new Event('test'));
24+
strictEqual(count, 2, 'The listener was not added with the once flag');
25+
controller.abort();
26+
et.dispatchEvent(new Event('test'));
27+
strictEqual(count, 2, 'Aborting on the controller removes the listener');
28+
et.addEventListener('test', handler, { signal: controller.signal });
29+
et.dispatchEvent(new Event('test'));
30+
strictEqual(count, 2, 'Passing an aborted signal never adds the handler');
31+
}
32+
33+
{
34+
// Passing an AbortSignal to addEventListener works with the once flag
35+
let count = 0;
36+
function handler() {
37+
count++;
38+
}
39+
const et = new EventTarget();
40+
const controller = new AbortController();
41+
et.addEventListener('test', handler, { signal: controller.signal });
42+
et.removeEventListener('test', handler);
43+
et.dispatchEvent(new Event('test'));
44+
strictEqual(count, 0, 'The listener was still removed');
45+
}
46+
47+
{
48+
// Removing a once listener works with a passed signal
49+
let count = 0;
50+
function handler() {
51+
count++;
52+
}
53+
const et = new EventTarget();
54+
const controller = new AbortController();
55+
const options = { signal: controller.signal, once: true };
56+
et.addEventListener('test', handler, options);
57+
controller.abort();
58+
et.dispatchEvent(new Event('test'));
59+
strictEqual(count, 0, 'The listener was still removed');
60+
}
61+
62+
{
63+
let count = 0;
64+
function handler() {
65+
count++;
66+
}
67+
const et = new EventTarget();
68+
const controller = new AbortController();
69+
const options = { signal: controller.signal, once: true };
70+
et.addEventListener('test', handler, options);
71+
et.removeEventListener('test', handler);
72+
et.dispatchEvent(new Event('test'));
73+
strictEqual(count, 0, 'The listener was still removed');
74+
}
75+
76+
{
77+
// Passing an AbortSignal to multiple listeners
78+
let count = 0;
79+
function handler() {
80+
count++;
81+
}
82+
const et = new EventTarget();
83+
const controller = new AbortController();
84+
const options = { signal: controller.signal, once: true };
85+
et.addEventListener('first', handler, options);
86+
et.addEventListener('second', handler, options);
87+
controller.abort();
88+
et.dispatchEvent(new Event('first'));
89+
et.dispatchEvent(new Event('second'));
90+
strictEqual(count, 0, 'The listener was still removed');
91+
}
92+
93+
{
94+
// Passing an AbortSignal to addEventListener works with the capture flag
95+
let count = 0;
96+
function handler() {
97+
count++;
98+
}
99+
const et = new EventTarget();
100+
const controller = new AbortController();
101+
const options = { signal: controller.signal, capture: true };
102+
et.addEventListener('test', handler, options);
103+
controller.abort();
104+
et.dispatchEvent(new Event('test'));
105+
strictEqual(count, 0, 'The listener was still removed');
106+
}
107+
108+
{
109+
// Aborting from a listener does not call future listeners
110+
let count = 0;
111+
function handler() {
112+
count++;
113+
}
114+
const et = new EventTarget();
115+
const controller = new AbortController();
116+
const options = { signal: controller.signal };
117+
et.addEventListener('test', () => {
118+
controller.abort();
119+
}, options);
120+
et.addEventListener('test', handler, options);
121+
et.dispatchEvent(new Event('test'));
122+
strictEqual(count, 0, 'The listener was still removed');
123+
}
124+
125+
{
126+
// Adding then aborting a listener in another listener does not call it
127+
let count = 0;
128+
function handler() {
129+
count++;
130+
}
131+
const et = new EventTarget();
132+
const controller = new AbortController();
133+
et.addEventListener('test', () => {
134+
et.addEventListener('test', handler, { signal: controller.signal });
135+
controller.abort();
136+
}, { signal: controller.signal });
137+
et.dispatchEvent(new Event('test'));
138+
strictEqual(count, 0, 'The listener was still removed');
139+
}
140+
141+
{
142+
// Aborting from a nested listener should remove it
143+
const et = new EventTarget();
144+
const ac = new AbortController();
145+
let count = 0;
146+
et.addEventListener('foo', () => {
147+
et.addEventListener('foo', () => {
148+
count++;
149+
if (count > 5) ac.abort();
150+
et.dispatchEvent(new Event('foo'));
151+
}, { signal: ac.signal });
152+
et.dispatchEvent(new Event('foo'));
153+
}, { once: true });
154+
et.dispatchEvent(new Event('foo'));
155+
}

test/parallel/test-eventtarget.js

+9
Original file line numberDiff line numberDiff line change
@@ -532,3 +532,12 @@ let asyncTest = Promise.resolve();
532532
target.dispatchEvent(new Event('foo'));
533533
deepStrictEqual(output, [1, 2, 3, 4]);
534534
}
535+
{
536+
const et = new EventTarget();
537+
const listener = common.mustNotCall();
538+
et.addEventListener('foo', common.mustCall((e) => {
539+
et.removeEventListener('foo', listener);
540+
}));
541+
et.addEventListener('foo', listener);
542+
et.dispatchEvent(new Event('foo'));
543+
}

0 commit comments

Comments
 (0)