Repository: denoland/deno
Pull request: fix(ext/node): fix addAbortListener

This PR fixes events.addAbortListener API, and enables `parallel/test-events-add-abort-listener.mjs`

towards #29595 

Changed files:
filename: ext/node/polyfills/internal/events/abort_listener.mjs
patch: @@ -1,7 +1,7 @@
 // Copyright 2018-2025 the Deno authors. MIT license.
 // Copyright Joyent, Inc. and Node.js contributors. All rights reserved. MIT license.
 
-import { primordials } from "ext:deno_node/internal/test/binding.ts";
+import { primordials } from "ext:core/mod.js";
 const { queueMicrotask } = primordials;
 import { SymbolDispose } from "ext:deno_web/00_infra.js";
 import * as abortSignal from "ext:deno_web/03_abort_signal.js";
@@ -23,14 +23,15 @@ function addAbortListener(signal, listener) {
 
   let removeEventListener;
   if (signal.aborted) {
-    queueMicrotask(() => listener());
+    queueMicrotask(() => listener({ target: signal }));
   } else {
-    signal[abortSignal.add](() => {
+    const handler = () => {
       removeEventListener?.();
-      listener();
-    });
+      listener({ target: signal });
+    };
+    signal[abortSignal.add](handler);
     removeEventListener = () => {
-      signal[abortSignal.remove](listener);
+      signal[abortSignal.remove](handler);
     };
   }
   return {

filename: tests/node_compat/config.jsonc
patch: @@ -532,6 +532,7 @@
       "test-event-emitter-special-event-names.js",
       "test-event-emitter-subclass.js",
       "test-event-emitter-symbols.js",
+      "test-events-add-abort-listener.mjs",
       "test-events-list.js",
       "test-events-on-async-iterator.js",
       "test-events-once.js",

filename: tests/node_compat/test/parallel/test-events-add-abort-listener.mjs
patch: @@ -0,0 +1,62 @@
+// deno-fmt-ignore-file
+// deno-lint-ignore-file
+
+// Copyright Joyent and Node contributors. All rights reserved. MIT license.
+// Taken from Node 23.9.0
+// This file is automatically generated by `tests/node_compat/runner/setup.ts`. Do not modify this file manually.
+
+import * as common from '../common/index.mjs';
+import * as events from 'node:events';
+import * as assert from 'node:assert';
+import { describe, it } from 'node:test';
+
+describe('events.addAbortListener', () => {
+  it('should throw if signal not provided', () => {
+    assert.throws(() => events.addAbortListener(), { code: 'ERR_INVALID_ARG_TYPE' });
+  });
+
+  it('should throw if provided signal is invalid', () => {
+    assert.throws(() => events.addAbortListener(undefined), { code: 'ERR_INVALID_ARG_TYPE' });
+    assert.throws(() => events.addAbortListener(null), { code: 'ERR_INVALID_ARG_TYPE' });
+    assert.throws(() => events.addAbortListener({}), { code: 'ERR_INVALID_ARG_TYPE' });
+  });
+
+  it('should throw if listener is not a function', () => {
+    const { signal } = new AbortController();
+    assert.throws(() => events.addAbortListener(signal), { code: 'ERR_INVALID_ARG_TYPE' });
+    assert.throws(() => events.addAbortListener(signal, {}), { code: 'ERR_INVALID_ARG_TYPE' });
+    assert.throws(() => events.addAbortListener(signal, undefined), { code: 'ERR_INVALID_ARG_TYPE' });
+  });
+
+  it('should return a Disposable', () => {
+    const { signal } = new AbortController();
+    const disposable = events.addAbortListener(signal, common.mustNotCall());
+
+    assert.strictEqual(typeof disposable[Symbol.dispose], 'function');
+  });
+
+  it('should execute the listener immediately for aborted runners', () => {
+    const disposable = events.addAbortListener(AbortSignal.abort(), common.mustCall());
+    assert.strictEqual(typeof disposable[Symbol.dispose], 'function');
+  });
+
+  it('should execute the listener even when event propagation stopped', () => {
+    const controller = new AbortController();
+    const { signal } = controller;
+
+    signal.addEventListener('abort', (e) => e.stopImmediatePropagation());
+    events.addAbortListener(
+      signal,
+      common.mustCall((e) => assert.strictEqual(e.target, signal)),
+    );
+
+    controller.abort();
+  });
+
+  it('should remove event listeners when disposed', () => {
+    const controller = new AbortController();
+    const disposable = events.addAbortListener(controller.signal, common.mustNotCall());
+    disposable[Symbol.dispose]();
+    controller.abort();
+  });
+});