GeezerVison is showing github.com — lib: add option to force handling stopped events · nodejs/node@97b00c3
File treeExpand file treeCollapse file tree Open diff view settings Expand file treeCollapse file tree Open diff view settings Collapse file Copy file name to clipboardExpand all lines: lib/events.js +8-3Lines changed: 8 additions & 3 deletions | Original file line number | Diff line number | Diff line change |
|---|
@@ -61,6 +61,7 @@ const { | 61 | 61 | let spliceOne; | 62 | 62 | let FixedQueue; | 63 | 63 | let kFirstEventParam; | | 64 | +let kResistStopPropagation; | 64 | 65 |
| 65 | 66 | const { | 66 | 67 | AbortError, | @@ -978,7 +979,10 @@ async function once(emitter, name, options = kEmptyObject) { | 978 | 979 | } | 979 | 980 | resolve(args); | 980 | 981 | }; | 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); | 982 | 986 | if (name !== 'error' && typeof emitter.once === 'function') { | 983 | 987 | // EventTarget does not have `error` event semantics like Node | 984 | 988 | // EventEmitters, we listen to `error` events only on EventEmitters. | @@ -991,7 +995,7 @@ async function once(emitter, name, options = kEmptyObject) { | 991 | 995 | } | 992 | 996 | if (signal != null) { | 993 | 997 | eventTargetAgnosticAddListener( | 994 | | - signal, 'abort', abortListener, { once: true }); | | 998 | + signal, 'abort', abortListener, { __proto__: null, once: true, [kResistStopPropagation]: true }); | 995 | 999 | } | 996 | 1000 | }); | 997 | 1001 | } | @@ -1149,11 +1153,12 @@ function on(emitter, event, options = kEmptyObject) { | 1149 | 1153 | } | 1150 | 1154 | } | 1151 | 1155 | if (signal) { | | 1156 | + kResistStopPropagation ??= require('internal/event_target').kResistStopPropagation; | 1152 | 1157 | eventTargetAgnosticAddListener( | 1153 | 1158 | signal, | 1154 | 1159 | 'abort', | 1155 | 1160 | abortListener, | 1156 | | - { once: true }); | | 1161 | + { __proto__: null, once: true, [kResistStopPropagation]: true }); | 1157 | 1162 | } | 1158 | 1163 |
| 1159 | 1164 | return iterator; | |
Collapse file Copy file name to clipboardExpand all lines: lib/internal/event_target.js +26-8Lines changed: 26 additions & 8 deletions | Original file line number | Diff line number | Diff line change |
|---|
@@ -59,6 +59,7 @@ const kStop = Symbol('kStop'); | 59 | 59 | const kTarget = Symbol('kTarget'); | 60 | 60 | const kHandlers = Symbol('kHandlers'); | 61 | 61 | const kWeakHandler = Symbol('kWeak'); | | 62 | +const kResistStopPropagation = Symbol('kResistStopPropagation'); | 62 | 63 |
| 63 | 64 | const kHybridDispatch = SymbolFor('nodejs.internal.kHybridDispatch'); | 64 | 65 | const kCreateEvent = Symbol('kCreateEvent'); | @@ -421,6 +422,7 @@ const kFlagPassive = 1 << 2; | 421 | 422 | const kFlagNodeStyle = 1 << 3; | 422 | 423 | const kFlagWeak = 1 << 4; | 423 | 424 | const kFlagRemoved = 1 << 5; | | 425 | +const kFlagResistStopPropagation = 1 << 6; | 424 | 426 |
| 425 | 427 | // The listeners for an EventTarget are maintained as a linked list. | 426 | 428 | // Unfortunately, the way EventTarget is defined, listeners are accounted | @@ -431,7 +433,7 @@ const kFlagRemoved = 1 << 5; | 431 | 433 | // slower. | 432 | 434 | class Listener { | 433 | 435 | constructor(previous, listener, once, capture, passive, | 434 | | - isNodeStyleListener, weak) { | | 436 | + isNodeStyleListener, weak, resistStopPropagation) { | 435 | 437 | this.next = undefined; | 436 | 438 | if (previous !== undefined) | 437 | 439 | previous.next = this; | @@ -449,6 +451,8 @@ class Listener { | 449 | 451 | flags |= kFlagNodeStyle; | 450 | 452 | if (weak) | 451 | 453 | flags |= kFlagWeak; | | 454 | + if (resistStopPropagation) | | 455 | + flags |= kFlagResistStopPropagation; | 452 | 456 | this.flags = flags; | 453 | 457 |
| 454 | 458 | this.removed = false; | @@ -486,6 +490,9 @@ class Listener { | 486 | 490 | get weak() { | 487 | 491 | return Boolean(this.flags & kFlagWeak); | 488 | 492 | } | | 493 | + get resistStopPropagation() { | | 494 | + return Boolean(this.flags & kFlagResistStopPropagation); | | 495 | + } | 489 | 496 | get removed() { | 490 | 497 | return Boolean(this.flags & kFlagRemoved); | 491 | 498 | } | @@ -583,6 +590,7 @@ class EventTarget { | 583 | 590 | signal, | 584 | 591 | isNodeStyleListener, | 585 | 592 | weak, | | 593 | + resistStopPropagation, | 586 | 594 | } = validateEventListenerOptions(options); | 587 | 595 |
| 588 | 596 | validateAbortSignal(signal, 'options.signal'); | @@ -609,16 +617,16 @@ class EventTarget { | 609 | 617 | // not prevent the event target from GC. | 610 | 618 | signal.addEventListener('abort', () => { | 611 | 619 | this.removeEventListener(type, listener, options); | 612 | | - }, { once: true, [kWeakHandler]: this }); | | 620 | + }, { __proto__: null, once: true, [kWeakHandler]: this, [kResistStopPropagation]: true }); | 613 | 621 | } | 614 | 622 |
| 615 | 623 | let root = this[kEvents].get(type); | 616 | 624 |
| 617 | 625 | if (root === undefined) { | 618 | | - root = { size: 1, next: undefined }; | | 626 | + root = { size: 1, next: undefined, resistStopPropagation: Boolean(resistStopPropagation) }; | 619 | 627 | // This is the first handler in our linked list. | 620 | 628 | new Listener(root, listener, once, capture, passive, | 621 | | - isNodeStyleListener, weak); | | 629 | + isNodeStyleListener, weak, resistStopPropagation); | 622 | 630 | this[kNewListener]( | 623 | 631 | root.size, | 624 | 632 | type, | @@ -645,8 +653,9 @@ class EventTarget { | 645 | 653 | } | 646 | 654 |
| 647 | 655 | new Listener(previous, listener, once, capture, passive, | 648 | | - isNodeStyleListener, weak); | | 656 | + isNodeStyleListener, weak, resistStopPropagation); | 649 | 657 | root.size++; | | 658 | + root.resistStopPropagation ||= Boolean(resistStopPropagation); | 650 | 659 | this[kNewListener](root.size, type, listener, once, capture, passive, weak); | 651 | 660 | } | 652 | 661 |
| @@ -730,14 +739,21 @@ class EventTarget { | 730 | 739 | let handler = root.next; | 731 | 740 | let next; | 732 | 741 |
| 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()) { | 735 | 749 | // Cache the next item in case this iteration removes the current one | 736 | 750 | next = handler.next; | 737 | 751 |
| 738 | | - if (handler.removed) { | | 752 | + if (handler.removed || (event?.[kStop] === true && !handler.resistStopPropagation)) { | 739 | 753 | // Deal with the case an event is removed while event handlers are | 740 | 754 | // Being processed (removeEventListener called from a listener) | | 755 | + // And the case of event.stopImmediatePropagation() being called | | 756 | + // For events not flagged as resistStopPropagation | 741 | 757 | handler = next; | 742 | 758 | continue; | 743 | 759 | } | @@ -1005,6 +1021,7 @@ function validateEventListenerOptions(options) { | 1005 | 1021 | passive: Boolean(options.passive), | 1006 | 1022 | signal: options.signal, | 1007 | 1023 | weak: options[kWeakHandler], | | 1024 | + resistStopPropagation: options[kResistStopPropagation] ?? false, | 1008 | 1025 | isNodeStyleListener: Boolean(options[kIsNodeStyleListener]), | 1009 | 1026 | }; | 1010 | 1027 | } | @@ -1132,5 +1149,6 @@ module.exports = { | 1132 | 1149 | kRemoveListener, | 1133 | 1150 | kEvents, | 1134 | 1151 | kWeakHandler, | | 1152 | + kResistStopPropagation, | 1135 | 1153 | isEventTarget, | 1136 | 1154 | }; |
Collapse file Copy file name to clipboardExpand all lines: test/parallel/test-events-once.js +13Lines changed: 13 additions & 0 deletions | Original file line number | Diff line number | Diff line change |
|---|
@@ -233,6 +233,18 @@ async function eventTargetAbortSignalBefore() { | 233 | 233 | }); | 234 | 234 | } | 235 | 235 |
| | 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 | + | 236 | 248 | async function eventTargetAbortSignalAfter() { | 237 | 249 | const et = new EventTarget(); | 238 | 250 | const ac = new AbortController(); | @@ -270,6 +282,7 @@ Promise.all([ | 270 | 282 | abortSignalAfterEvent(), | 271 | 283 | abortSignalRemoveListener(), | 272 | 284 | eventTargetAbortSignalBefore(), | | 285 | + eventTargetAbortSignalBeforeEvenWhenSignalPropagationStopped(), | 273 | 286 | eventTargetAbortSignalAfter(), | 274 | 287 | eventTargetAbortSignalAfterEvent(), | 275 | 288 | ]).then(common.mustCall()); |
Collapse file Copy file name to clipboardExpand all lines: test/parallel/test-eventtarget.js +12Lines changed: 12 additions & 0 deletions | Original file line number | Diff line number | Diff line change |
|---|
@@ -726,3 +726,15 @@ let asyncTest = Promise.resolve(); | 726 | 726 | et.removeEventListener(Symbol('symbol'), () => {}); | 727 | 727 | }, TypeError); | 728 | 728 | } | | 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 | +} |
|