lib: fix AbortSignal.any() with timeout signals

PR-URL: https://github.com/nodejs/node/pull/57867
Reviewed-By: Edy Silva <edigleyssonsilva@gmail.com>
Reviewed-By: James M Snell <jasnell@gmail.com>
Reviewed-By: Chemi Atlow <chemi@atlow.co.il>
This commit is contained in:
Gürgün Dayıoğlu 2025-04-17 16:28:36 +02:00 committed by GitHub
parent d8e9e05a27
commit 9d6626a37e
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
2 changed files with 67 additions and 0 deletions

View File

@ -259,14 +259,30 @@ class AbortSignal extends EventTarget {
if (!signalsArray.length) {
return resultSignal;
}
const resultSignalWeakRef = new SafeWeakRef(resultSignal);
resultSignal[kSourceSignals] = new SafeSet();
// Track if we have any timeout signals
let hasTimeoutSignals = false;
for (let i = 0; i < signalsArray.length; i++) {
const signal = signalsArray[i];
// Check if this is a timeout signal
if (signal[kTimeout]) {
hasTimeoutSignals = true;
// Add the timeout signal to gcPersistentSignals to keep it alive
// This is what the kNewListener method would do when adding abort listeners
gcPersistentSignals.add(signal);
}
if (signal.aborted) {
abortSignal(resultSignal, signal.reason);
return resultSignal;
}
signal[kDependantSignals] ??= new SafeSet();
if (!signal[kComposite]) {
const signalWeakRef = new SafeWeakRef(signal);
@ -301,6 +317,12 @@ class AbortSignal extends EventTarget {
}
}
}
// If we have any timeout signals, add the composite signal to gcPersistentSignals
if (hasTimeoutSignals && resultSignal[kSourceSignals].size > 0) {
gcPersistentSignals.add(resultSignal);
}
return resultSignal;
}
@ -416,8 +438,10 @@ function abortSignal(signal, reason) {
// otherwise to a new "AbortError" DOMException.
signal[kAborted] = true;
signal[kReason] = reason;
// 3. Let dependentSignalsToAbort be a new list.
const dependentSignalsToAbort = ObjectSetPrototypeOf([], null);
// 4. For each dependentSignal of signal's dependent signals:
signal[kDependantSignals]?.forEach((s) => {
const dependentSignal = s.deref();
@ -433,12 +457,27 @@ function abortSignal(signal, reason) {
// 5. Run the abort steps for signal
runAbort(signal);
// 6. For each dependentSignal of dependentSignalsToAbort,
// run the abort steps for dependentSignal.
for (let i = 0; i < dependentSignalsToAbort.length; i++) {
const dependentSignal = dependentSignalsToAbort[i];
runAbort(dependentSignal);
}
// Clean up the signal from gcPersistentSignals
gcPersistentSignals.delete(signal);
// If this is a composite signal, also remove all of its source signals from gcPersistentSignals
// when they get dereferenced from the signal's kSourceSignals set
if (signal[kComposite] && signal[kSourceSignals]) {
signal[kSourceSignals].forEach((sourceWeakRef) => {
const sourceSignal = sourceWeakRef.deref();
if (sourceSignal) {
gcPersistentSignals.delete(sourceSignal);
}
});
}
}
// To run the abort steps for an AbortSignal signal

View File

@ -0,0 +1,28 @@
'use strict';
require('../common');
const assert = require('assert');
const { once } = require('node:events');
const { describe, it } = require('node:test');
describe('AbortSignal.any() with timeout signals', () => {
it('should abort when the first timeout signal fires', async () => {
const signal = AbortSignal.any([AbortSignal.timeout(9000), AbortSignal.timeout(110000)]);
const abortPromise = Promise.race([
once(signal, 'abort').then(() => {
throw signal.reason;
}),
new Promise((resolve) => setTimeout(resolve, 10000)),
]);
// The promise should be aborted by the 9000ms timeout
await assert.rejects(
() => abortPromise,
{
name: 'TimeoutError',
message: 'The operation was aborted due to timeout'
}
);
});
});