diff --git a/lib/events.js b/lib/events.js index ed707b5300be61..89d32d681908c1 100644 --- a/lib/events.js +++ b/lib/events.js @@ -61,6 +61,7 @@ const { let spliceOne; let FixedQueue; let kFirstEventParam; +let kResistStopPropagation; const { AbortError, @@ -978,7 +979,10 @@ async function once(emitter, name, options = kEmptyObject) { } resolve(args); }; - eventTargetAgnosticAddListener(emitter, name, resolver, { once: true }); + + kResistStopPropagation ??= require('internal/event_target').kResistStopPropagation; + const opts = { __proto__: null, once: true, [kResistStopPropagation]: true }; + eventTargetAgnosticAddListener(emitter, name, resolver, opts); if (name !== 'error' && typeof emitter.once === 'function') { // EventTarget does not have `error` event semantics like Node // EventEmitters, we listen to `error` events only on EventEmitters. @@ -991,7 +995,7 @@ async function once(emitter, name, options = kEmptyObject) { } if (signal != null) { eventTargetAgnosticAddListener( - signal, 'abort', abortListener, { once: true }); + signal, 'abort', abortListener, { __proto__: null, once: true, [kResistStopPropagation]: true }); } }); } @@ -1149,11 +1153,12 @@ function on(emitter, event, options = kEmptyObject) { } } if (signal) { + kResistStopPropagation ??= require('internal/event_target').kResistStopPropagation; eventTargetAgnosticAddListener( signal, 'abort', abortListener, - { once: true }); + { __proto__: null, once: true, [kResistStopPropagation]: true }); } return iterator; diff --git a/lib/internal/event_target.js b/lib/internal/event_target.js index 61aa31afa4449e..2da787d6388603 100644 --- a/lib/internal/event_target.js +++ b/lib/internal/event_target.js @@ -59,6 +59,7 @@ const kStop = Symbol('kStop'); const kTarget = Symbol('kTarget'); const kHandlers = Symbol('kHandlers'); const kWeakHandler = Symbol('kWeak'); +const kResistStopPropagation = Symbol('kResistStopPropagation'); const kHybridDispatch = SymbolFor('nodejs.internal.kHybridDispatch'); const kCreateEvent = Symbol('kCreateEvent'); @@ -421,6 +422,7 @@ const kFlagPassive = 1 << 2; const kFlagNodeStyle = 1 << 3; const kFlagWeak = 1 << 4; const kFlagRemoved = 1 << 5; +const kFlagResistStopPropagation = 1 << 6; // The listeners for an EventTarget are maintained as a linked list. // Unfortunately, the way EventTarget is defined, listeners are accounted @@ -431,7 +433,7 @@ const kFlagRemoved = 1 << 5; // slower. class Listener { constructor(previous, listener, once, capture, passive, - isNodeStyleListener, weak) { + isNodeStyleListener, weak, resistStopPropagation) { this.next = undefined; if (previous !== undefined) previous.next = this; @@ -449,6 +451,8 @@ class Listener { flags |= kFlagNodeStyle; if (weak) flags |= kFlagWeak; + if (resistStopPropagation) + flags |= kFlagResistStopPropagation; this.flags = flags; this.removed = false; @@ -486,6 +490,9 @@ class Listener { get weak() { return Boolean(this.flags & kFlagWeak); } + get resistStopPropagation() { + return Boolean(this.flags & kFlagResistStopPropagation); + } get removed() { return Boolean(this.flags & kFlagRemoved); } @@ -583,6 +590,7 @@ class EventTarget { signal, isNodeStyleListener, weak, + resistStopPropagation, } = validateEventListenerOptions(options); validateAbortSignal(signal, 'options.signal'); @@ -609,16 +617,16 @@ class EventTarget { // not prevent the event target from GC. signal.addEventListener('abort', () => { this.removeEventListener(type, listener, options); - }, { once: true, [kWeakHandler]: this }); + }, { __proto__: null, once: true, [kWeakHandler]: this, [kResistStopPropagation]: true }); } let root = this[kEvents].get(type); if (root === undefined) { - root = { size: 1, next: undefined }; + root = { size: 1, next: undefined, resistStopPropagation: Boolean(resistStopPropagation) }; // This is the first handler in our linked list. new Listener(root, listener, once, capture, passive, - isNodeStyleListener, weak); + isNodeStyleListener, weak, resistStopPropagation); this[kNewListener]( root.size, type, @@ -645,8 +653,9 @@ class EventTarget { } new Listener(previous, listener, once, capture, passive, - isNodeStyleListener, weak); + isNodeStyleListener, weak, resistStopPropagation); root.size++; + root.resistStopPropagation ||= Boolean(resistStopPropagation); this[kNewListener](root.size, type, listener, once, capture, passive, weak); } @@ -730,14 +739,21 @@ class EventTarget { let handler = root.next; let next; - while (handler !== undefined && - (handler.passive || event?.[kStop] !== true)) { + const iterationCondition = () => { + if (handler === undefined) { + return false; + } + return root.resistStopPropagation || handler.passive || event?.[kStop] !== true; + }; + while (iterationCondition()) { // Cache the next item in case this iteration removes the current one next = handler.next; - if (handler.removed) { + if (handler.removed || (event?.[kStop] === true && !handler.resistStopPropagation)) { // Deal with the case an event is removed while event handlers are // Being processed (removeEventListener called from a listener) + // And the case of event.stopImmediatePropagation() being called + // For events not flagged as resistStopPropagation handler = next; continue; } @@ -1005,6 +1021,7 @@ function validateEventListenerOptions(options) { passive: Boolean(options.passive), signal: options.signal, weak: options[kWeakHandler], + resistStopPropagation: options[kResistStopPropagation] ?? false, isNodeStyleListener: Boolean(options[kIsNodeStyleListener]), }; } @@ -1132,5 +1149,6 @@ module.exports = { kRemoveListener, kEvents, kWeakHandler, + kResistStopPropagation, isEventTarget, }; diff --git a/test/parallel/test-events-once.js b/test/parallel/test-events-once.js index 5ae5461d92676c..be05028faaf0c2 100644 --- a/test/parallel/test-events-once.js +++ b/test/parallel/test-events-once.js @@ -233,6 +233,18 @@ async function eventTargetAbortSignalBefore() { }); } +async function eventTargetAbortSignalBeforeEvenWhenSignalPropagationStopped() { + const et = new EventTarget(); + const ac = new AbortController(); + const { signal } = ac; + signal.addEventListener('abort', (e) => e.stopImmediatePropagation(), { once: true }); + + process.nextTick(() => ac.abort()); + return rejects(once(et, 'foo', { signal }), { + name: 'AbortError', + }); +} + async function eventTargetAbortSignalAfter() { const et = new EventTarget(); const ac = new AbortController(); @@ -270,6 +282,7 @@ Promise.all([ abortSignalAfterEvent(), abortSignalRemoveListener(), eventTargetAbortSignalBefore(), + eventTargetAbortSignalBeforeEvenWhenSignalPropagationStopped(), eventTargetAbortSignalAfter(), eventTargetAbortSignalAfterEvent(), ]).then(common.mustCall()); diff --git a/test/parallel/test-eventtarget.js b/test/parallel/test-eventtarget.js index 44638ca7393c7b..016d357c9d7c91 100644 --- a/test/parallel/test-eventtarget.js +++ b/test/parallel/test-eventtarget.js @@ -726,3 +726,15 @@ let asyncTest = Promise.resolve(); et.removeEventListener(Symbol('symbol'), () => {}); }, TypeError); } + +{ + // Test that event listeners are removed by signal even when + // signal's abort event propagation stopped + const controller = new AbortController(); + const { signal } = controller; + signal.addEventListener('abort', (e) => e.stopImmediatePropagation(), { once: true }); + const et = new EventTarget(); + et.addEventListener('foo', common.mustNotCall(), { signal }); + controller.abort(); + et.dispatchEvent(new Event('foo')); +}