Skip to content

Commit 061ea54

Browse files
committed
lib: add option to force handling stopped events
1 parent e7646a5 commit 061ea54

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
@@ -61,6 +61,7 @@ const {
6161
let spliceOne;
6262
let FixedQueue;
6363
let kFirstEventParam;
64+
let kResistStopPropagation;
6465

6566
const {
6667
AbortError,
@@ -978,7 +979,10 @@ async function once(emitter, name, options = kEmptyObject) {
978979
}
979980
resolve(args);
980981
};
981-
eventTargetAgnosticAddListener(emitter, name, resolver, { once: true });
982+
983+
kResistStopPropagation ??= require('internal/event_target').kResistStopPropagation;
984+
const opts = { __proto__: null, once: true, [kResistStopPropagation]: true };
985+
eventTargetAgnosticAddListener(emitter, name, resolver, opts);
982986
if (name !== 'error' && typeof emitter.once === 'function') {
983987
// EventTarget does not have `error` event semantics like Node
984988
// EventEmitters, we listen to `error` events only on EventEmitters.
@@ -991,7 +995,7 @@ async function once(emitter, name, options = kEmptyObject) {
991995
}
992996
if (signal != null) {
993997
eventTargetAgnosticAddListener(
994-
signal, 'abort', abortListener, { once: true });
998+
signal, 'abort', abortListener, { __proto__: null, once: true, [kResistStopPropagation]: true });
995999
}
9961000
});
9971001
}
@@ -1149,11 +1153,12 @@ function on(emitter, event, options = kEmptyObject) {
11491153
}
11501154
}
11511155
if (signal) {
1156+
kResistStopPropagation ??= require('internal/event_target').kResistStopPropagation;
11521157
eventTargetAgnosticAddListener(
11531158
signal,
11541159
'abort',
11551160
abortListener,
1156-
{ once: true });
1161+
{ __proto__: null, once: true, [kResistStopPropagation]: true });
11571162
}
11581163

11591164
return iterator;

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');
@@ -421,6 +422,7 @@ const kFlagPassive = 1 << 2;
421422
const kFlagNodeStyle = 1 << 3;
422423
const kFlagWeak = 1 << 4;
423424
const kFlagRemoved = 1 << 5;
425+
const kFlagResistStopPropagation = 1 << 6;
424426

425427
// The listeners for an EventTarget are maintained as a linked list.
426428
// Unfortunately, the way EventTarget is defined, listeners are accounted
@@ -431,7 +433,7 @@ const kFlagRemoved = 1 << 5;
431433
// slower.
432434
class Listener {
433435
constructor(previous, listener, once, capture, passive,
434-
isNodeStyleListener, weak) {
436+
isNodeStyleListener, weak, resistStopPropagation) {
435437
this.next = undefined;
436438
if (previous !== undefined)
437439
previous.next = this;
@@ -449,6 +451,8 @@ class Listener {
449451
flags |= kFlagNodeStyle;
450452
if (weak)
451453
flags |= kFlagWeak;
454+
if (resistStopPropagation)
455+
flags |= kFlagResistStopPropagation;
452456
this.flags = flags;
453457

454458
this.removed = false;
@@ -486,6 +490,9 @@ class Listener {
486490
get weak() {
487491
return Boolean(this.flags & kFlagWeak);
488492
}
493+
get resistStopPropagation() {
494+
return Boolean(this.flags & kFlagResistStopPropagation);
495+
}
489496
get removed() {
490497
return Boolean(this.flags & kFlagRemoved);
491498
}
@@ -583,6 +590,7 @@ class EventTarget {
583590
signal,
584591
isNodeStyleListener,
585592
weak,
593+
resistStopPropagation,
586594
} = validateEventListenerOptions(options);
587595

588596
validateAbortSignal(signal, 'options.signal');
@@ -609,16 +617,16 @@ class EventTarget {
609617
// not prevent the event target from GC.
610618
signal.addEventListener('abort', () => {
611619
this.removeEventListener(type, listener, options);
612-
}, { once: true, [kWeakHandler]: this });
620+
}, { __proto__: null, once: true, [kWeakHandler]: this, [kResistStopPropagation]: true });
613621
}
614622

615623
let root = this[kEvents].get(type);
616624

617625
if (root === undefined) {
618-
root = { size: 1, next: undefined };
626+
root = { size: 1, next: undefined, resistStopPropagation: Boolean(resistStopPropagation) };
619627
// This is the first handler in our linked list.
620628
new Listener(root, listener, once, capture, passive,
621-
isNodeStyleListener, weak);
629+
isNodeStyleListener, weak, resistStopPropagation);
622630
this[kNewListener](
623631
root.size,
624632
type,
@@ -645,8 +653,9 @@ class EventTarget {
645653
}
646654

647655
new Listener(previous, listener, once, capture, passive,
648-
isNodeStyleListener, weak);
656+
isNodeStyleListener, weak, resistStopPropagation);
649657
root.size++;
658+
root.resistStopPropagation ||= Boolean(resistStopPropagation);
650659
this[kNewListener](root.size, type, listener, once, capture, passive, weak);
651660
}
652661

@@ -730,14 +739,21 @@ class EventTarget {
730739
let handler = root.next;
731740
let next;
732741

733-
while (handler !== undefined &&
734-
(handler.passive || event?.[kStop] !== true)) {
742+
const iterationCondition = () => {
743+
if (handler === undefined) {
744+
return false;
745+
}
746+
return root.resistStopPropagation || handler.passive || event?.[kStop] !== true;
747+
};
748+
while (iterationCondition()) {
735749
// Cache the next item in case this iteration removes the current one
736750
next = handler.next;
737751

738-
if (handler.removed) {
752+
if (handler.removed || (event?.[kStop] === true && !handler.resistStopPropagation)) {
739753
// Deal with the case an event is removed while event handlers are
740754
// Being processed (removeEventListener called from a listener)
755+
// And the case of event.stopImmediatePropagation() being called
756+
// For events not flagged as resistStopPropagation
741757
handler = next;
742758
continue;
743759
}
@@ -1005,6 +1021,7 @@ function validateEventListenerOptions(options) {
10051021
passive: Boolean(options.passive),
10061022
signal: options.signal,
10071023
weak: options[kWeakHandler],
1024+
resistStopPropagation: options[kResistStopPropagation] ?? false,
10081025
isNodeStyleListener: Boolean(options[kIsNodeStyleListener]),
10091026
};
10101027
}
@@ -1132,5 +1149,6 @@ module.exports = {
11321149
kRemoveListener,
11331150
kEvents,
11341151
kWeakHandler,
1152+
kResistStopPropagation,
11351153
isEventTarget,
11361154
};

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
@@ -726,3 +726,15 @@ let asyncTest = Promise.resolve();
726726
et.removeEventListener(Symbol('symbol'), () => {});
727727
}, TypeError);
728728
}
729+
730+
{
731+
// Test that event listeners are removed by signal even when
732+
// signal's abort event propagation stopped
733+
const controller = new AbortController();
734+
const { signal } = controller;
735+
signal.addEventListener('abort', (e) => e.stopImmediatePropagation(), { once: true });
736+
const et = new EventTarget();
737+
et.addEventListener('foo', common.mustNotCall(), { signal });
738+
controller.abort();
739+
et.dispatchEvent(new Event('foo'));
740+
}

0 commit comments

Comments
 (0)