From eca0fdb3b87710a736a45d1dd6830c53b0a5f23f Mon Sep 17 00:00:00 2001 From: Lazizbek Ergashev Date: Sat, 29 Aug 2026 13:47:31 +0500 Subject: [PATCH] events: fix addAbortListener for aborted signals Signed-off-by: Lazizbek Ergashev --- doc/api/events.md | 7 +++++++ lib/internal/event_target.js | 2 ++ lib/internal/events/abort_listener.js | 16 +++++++++++++++- .../test-events-add-abort-listener.mjs | 19 +++++++++++++++++++ 4 files changed, 43 insertions(+), 1 deletion(-) diff --git a/doc/api/events.md b/doc/api/events.md index 0c61b1780349..508153cf00e4 100644 --- a/doc/api/events.md +++ b/doc/api/events.md @@ -1883,6 +1883,10 @@ added: - v20.5.0 - v18.18.0 changes: + - version: REPLACEME + pr-url: https://github.com/nodejs/node/pull/65640 + description: When the signal is already aborted, the listener now receives an + `abort` event and disposing cancels the pending call. - version: - v24.0.0 - v22.16.0 @@ -1908,6 +1912,9 @@ not prevent the listener from running. Returns a disposable so that it may be unsubscribed from more easily. +If `signal` is already aborted, the listener is called with an `abort` event in a +microtask. Disposing before that microtask runs cancels the call. + ```cjs const { addAbortListener } = require('node:events'); diff --git a/lib/internal/event_target.js b/lib/internal/event_target.js index 23a2ef702739..69cdc4aac5b9 100644 --- a/lib/internal/event_target.js +++ b/lib/internal/event_target.js @@ -1243,6 +1243,8 @@ module.exports = { kTrustEvent, kRemoveListener, kEvents, + kIsBeingDispatched, + kTarget, kWeakHandler, kResistStopPropagation, isEventTarget, diff --git a/lib/internal/events/abort_listener.js b/lib/internal/events/abort_listener.js index 3f91cec5faca..38ad7351cf92 100644 --- a/lib/internal/events/abort_listener.js +++ b/lib/internal/events/abort_listener.js @@ -17,6 +17,7 @@ const { let queueMicrotask; let kResistStopPropagation; let abortListenerOptions; +let eventTarget; /** * @param {AbortSignal} signal @@ -33,7 +34,20 @@ function addAbortListener(signal, listener) { let removeEventListener; if (signal.aborted) { queueMicrotask ??= require('internal/process/task_queues').queueMicrotask; - queueMicrotask(() => listener()); + eventTarget ??= require('internal/event_target'); + let disposed = false; + queueMicrotask(() => { + if (disposed) return; + const { Event, kIsBeingDispatched, kTarget, kTrustEvent } = eventTarget; + const event = new Event('abort', { [kTrustEvent]: true }); + event[kTarget] = signal; + event[kIsBeingDispatched] = true; + listener(event); + event[kIsBeingDispatched] = false; + }); + removeEventListener = () => { + disposed = true; + }; } else { kResistStopPropagation ??= require('internal/event_target').kResistStopPropagation; abortListenerOptions ??= ObjectFreeze({ __proto__: null, once: true, [kResistStopPropagation]: true }); diff --git a/test/parallel/test-events-add-abort-listener.mjs b/test/parallel/test-events-add-abort-listener.mjs index 69b6a054e395..9f7c04859ef5 100644 --- a/test/parallel/test-events-add-abort-listener.mjs +++ b/test/parallel/test-events-add-abort-listener.mjs @@ -33,6 +33,25 @@ describe('events.addAbortListener', () => { assert.strictEqual(typeof disposable[Symbol.dispose], 'function'); }); + it('should pass an abort event to the listener for aborted runners', async () => { + const signal = AbortSignal.abort(); + const { promise, resolve } = Promise.withResolvers(); + events.addAbortListener(signal, (e) => resolve([e, e?.currentTarget])); + + const [event, currentTarget] = await promise; + assert.strictEqual(event.type, 'abort'); + assert.strictEqual(event.target, signal); + assert.strictEqual(event.isTrusted, true); + assert.strictEqual(currentTarget, signal); + }); + + it('should not execute the listener for aborted runners when disposed', async () => { + const disposable = events.addAbortListener(AbortSignal.abort(), common.mustNotCall()); + disposable[Symbol.dispose](); + + await new Promise(setImmediate); + }); + it('should execute the listener even when event propagation stopped', () => { const controller = new AbortController(); const { signal } = controller;