Skip to content

Commit db355d1

Browse files
atlowChemiruyadorno
authored andcommitted
lib: add option to force handling stopped events
PR-URL: #48301 Backport-PR-URL: #49587 Reviewed-By: Benjamin Gruenbaum <benjamingr@gmail.com> Reviewed-By: Moshe Atlow <moshe@atlow.co.il>
1 parent 56249b0 commit db355d1

File tree

4 files changed

+59
-11
lines changed

4 files changed

+59
-11
lines changed

lib/events.js

+8-3
Original file line numberDiff line numberDiff line change
@@ -59,6 +59,7 @@ const {
5959
} = require('internal/util/inspect');
6060

6161
let spliceOne;
62+
let kResistStopPropagation;
6263

6364
const {
6465
AbortError,
@@ -981,7 +982,10 @@ async function once(emitter, name, options = kEmptyObject) {
981982
}
982983
resolve(args);
983984
};
984-
eventTargetAgnosticAddListener(emitter, name, resolver, { once: true });
985+
986+
kResistStopPropagation ??= require('internal/event_target').kResistStopPropagation;
987+
const opts = { __proto__: null, once: true, [kResistStopPropagation]: true };
988+
eventTargetAgnosticAddListener(emitter, name, resolver, opts);
985989
if (name !== 'error' && typeof emitter.once === 'function') {
986990
// EventTarget does not have `error` event semantics like Node
987991
// EventEmitters, we listen to `error` events only on EventEmitters.
@@ -994,7 +998,7 @@ async function once(emitter, name, options = kEmptyObject) {
994998
}
995999
if (signal != null) {
9961000
eventTargetAgnosticAddListener(
997-
signal, 'abort', abortListener, { once: true });
1001+
signal, 'abort', abortListener, { __proto__: null, once: true, [kResistStopPropagation]: true });
9981002
}
9991003
});
10001004
}
@@ -1119,11 +1123,12 @@ function on(emitter, event, options = kEmptyObject) {
11191123
}
11201124

11211125
if (signal) {
1126+
kResistStopPropagation ??= require('internal/event_target').kResistStopPropagation;
11221127
eventTargetAgnosticAddListener(
11231128
signal,
11241129
'abort',
11251130
abortListener,
1126-
{ once: true });
1131+
{ __proto__: null, once: true, [kResistStopPropagation]: true });
11271132
}
11281133

11291134
function abortListener() {

lib/internal/event_target.js

+26-8
Original file line numberDiff line numberDiff line change
@@ -59,6 +59,7 @@ const kStop = Symbol('kStop');
5959
const kTarget = Symbol('kTarget');
6060
const kHandlers = Symbol('kHandlers');
6161
const kWeakHandler = Symbol('kWeak');
62+
const kResistStopPropagation = Symbol('kResistStopPropagation');
6263

6364
const kHybridDispatch = SymbolFor('nodejs.internal.kHybridDispatch');
6465
const kCreateEvent = Symbol('kCreateEvent');
@@ -403,6 +404,7 @@ const kFlagPassive = 1 << 2;
403404
const kFlagNodeStyle = 1 << 3;
404405
const kFlagWeak = 1 << 4;
405406
const kFlagRemoved = 1 << 5;
407+
const kFlagResistStopPropagation = 1 << 6;
406408

407409
// The listeners for an EventTarget are maintained as a linked list.
408410
// Unfortunately, the way EventTarget is defined, listeners are accounted
@@ -413,7 +415,7 @@ const kFlagRemoved = 1 << 5;
413415
// slower.
414416
class Listener {
415417
constructor(previous, listener, once, capture, passive,
416-
isNodeStyleListener, weak) {
418+
isNodeStyleListener, weak, resistStopPropagation) {
417419
this.next = undefined;
418420
if (previous !== undefined)
419421
previous.next = this;
@@ -431,6 +433,8 @@ class Listener {
431433
flags |= kFlagNodeStyle;
432434
if (weak)
433435
flags |= kFlagWeak;
436+
if (resistStopPropagation)
437+
flags |= kFlagResistStopPropagation;
434438
this.flags = flags;
435439

436440
this.removed = false;
@@ -468,6 +472,9 @@ class Listener {
468472
get weak() {
469473
return Boolean(this.flags & kFlagWeak);
470474
}
475+
get resistStopPropagation() {
476+
return Boolean(this.flags & kFlagResistStopPropagation);
477+
}
471478
get removed() {
472479
return Boolean(this.flags & kFlagRemoved);
473480
}
@@ -564,6 +571,7 @@ class EventTarget {
564571
signal,
565572
isNodeStyleListener,
566573
weak,
574+
resistStopPropagation,
567575
} = validateEventListenerOptions(options);
568576

569577
if (!validateEventListener(listener)) {
@@ -588,16 +596,16 @@ class EventTarget {
588596
// not prevent the event target from GC.
589597
signal.addEventListener('abort', () => {
590598
this.removeEventListener(type, listener, options);
591-
}, { once: true, [kWeakHandler]: this });
599+
}, { __proto__: null, once: true, [kWeakHandler]: this, [kResistStopPropagation]: true });
592600
}
593601

594602
let root = this[kEvents].get(type);
595603

596604
if (root === undefined) {
597-
root = { size: 1, next: undefined };
605+
root = { size: 1, next: undefined, resistStopPropagation: Boolean(resistStopPropagation) };
598606
// This is the first handler in our linked list.
599607
new Listener(root, listener, once, capture, passive,
600-
isNodeStyleListener, weak);
608+
isNodeStyleListener, weak, resistStopPropagation);
601609
this[kNewListener](
602610
root.size,
603611
type,
@@ -624,8 +632,9 @@ class EventTarget {
624632
}
625633

626634
new Listener(previous, listener, once, capture, passive,
627-
isNodeStyleListener, weak);
635+
isNodeStyleListener, weak, resistStopPropagation);
628636
root.size++;
637+
root.resistStopPropagation ||= Boolean(resistStopPropagation);
629638
this[kNewListener](root.size, type, listener, once, capture, passive, weak);
630639
}
631640

@@ -709,14 +718,21 @@ class EventTarget {
709718
let handler = root.next;
710719
let next;
711720

712-
while (handler !== undefined &&
713-
(handler.passive || event?.[kStop] !== true)) {
721+
const iterationCondition = () => {
722+
if (handler === undefined) {
723+
return false;
724+
}
725+
return root.resistStopPropagation || handler.passive || event?.[kStop] !== true;
726+
};
727+
while (iterationCondition()) {
714728
// Cache the next item in case this iteration removes the current one
715729
next = handler.next;
716730

717-
if (handler.removed) {
731+
if (handler.removed || (event?.[kStop] === true && !handler.resistStopPropagation)) {
718732
// Deal with the case an event is removed while event handlers are
719733
// Being processed (removeEventListener called from a listener)
734+
// And the case of event.stopImmediatePropagation() being called
735+
// For events not flagged as resistStopPropagation
720736
handler = next;
721737
continue;
722738
}
@@ -984,6 +1000,7 @@ function validateEventListenerOptions(options) {
9841000
passive: Boolean(options.passive),
9851001
signal: options.signal,
9861002
weak: options[kWeakHandler],
1003+
resistStopPropagation: options[kResistStopPropagation] ?? false,
9871004
isNodeStyleListener: Boolean(options[kIsNodeStyleListener]),
9881005
};
9891006
}
@@ -1099,5 +1116,6 @@ module.exports = {
10991116
kRemoveListener,
11001117
kEvents,
11011118
kWeakHandler,
1119+
kResistStopPropagation,
11021120
isEventTarget,
11031121
};

test/parallel/test-events-once.js

+13
Original file line numberDiff line numberDiff line change
@@ -233,6 +233,18 @@ async function eventTargetAbortSignalBefore() {
233233
});
234234
}
235235

236+
async function eventTargetAbortSignalBeforeEvenWhenSignalPropagationStopped() {
237+
const et = new EventTarget();
238+
const ac = new AbortController();
239+
const { signal } = ac;
240+
signal.addEventListener('abort', (e) => e.stopImmediatePropagation(), { once: true });
241+
242+
process.nextTick(() => ac.abort());
243+
return rejects(once(et, 'foo', { signal }), {
244+
name: 'AbortError',
245+
});
246+
}
247+
236248
async function eventTargetAbortSignalAfter() {
237249
const et = new EventTarget();
238250
const ac = new AbortController();
@@ -270,6 +282,7 @@ Promise.all([
270282
abortSignalAfterEvent(),
271283
abortSignalRemoveListener(),
272284
eventTargetAbortSignalBefore(),
285+
eventTargetAbortSignalBeforeEvenWhenSignalPropagationStopped(),
273286
eventTargetAbortSignalAfter(),
274287
eventTargetAbortSignalAfterEvent(),
275288
]).then(common.mustCall());

test/parallel/test-eventtarget.js

+12
Original file line numberDiff line numberDiff line change
@@ -717,3 +717,15 @@ let asyncTest = Promise.resolve();
717717
et.removeEventListener(Symbol('symbol'), () => {});
718718
}, TypeError);
719719
}
720+
721+
{
722+
// Test that event listeners are removed by signal even when
723+
// signal's abort event propagation stopped
724+
const controller = new AbortController();
725+
const { signal } = controller;
726+
signal.addEventListener('abort', (e) => e.stopImmediatePropagation(), { once: true });
727+
const et = new EventTarget();
728+
et.addEventListener('foo', common.mustNotCall(), { signal });
729+
controller.abort();
730+
et.dispatchEvent(new Event('foo'));
731+
}

0 commit comments

Comments
 (0)