Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
7 changes: 7 additions & 0 deletions lib/internal/abort_controller.js
Original file line number Diff line number Diff line change
Expand Up @@ -522,6 +522,13 @@ function abortSignal(signal, reason) {
for (let i = 0; i < dependentSignalsToAbort.length; i++) {
const dependentSignal = dependentSignalsToAbort[i];
runAbort(dependentSignal);
// A transitively-aborted dependent signal can never abort again, so there
// is no longer any reason to keep it alive. Dropping it from
// gcPersistentSignals lets it be collected, which in turn prunes its
// WeakRef from its sources' kDependantSignals sets. Otherwise, an observed
// composite that follows a long-lived source stays retained forever and
// its entry accumulates in that source's kDependantSignals.
gcPersistentSignals.delete(dependentSignal);
}

// Clean up the signal from gcPersistentSignals
Expand Down
31 changes: 31 additions & 0 deletions test/parallel/test-abortsignal-drop-settled-signals.mjs
Original file line number Diff line number Diff line change
Expand Up @@ -142,6 +142,37 @@ describe('when there is a long-lived signal', () => {

run(1);
});

it('drops observed dependent signals once they are transitively aborted', async () => {
const longLived = new AbortController();
const handler = () => {};
const size = () => {
const sym = Object.getOwnPropertySymbols(longLived.signal).find(
(s) => s.toString() === 'Symbol(kDependantSignals)'
);
return sym ? longLived.signal[sym].size : 0;
};

// Each composite observes the long-lived source and a per-request source,
// then is aborted through the per-request source without ever removing its
// listener. The aborted composites can never fire again, so the long-lived
// source's dependant set must not accumulate them. Using a helper keeps the
// last iteration's signals from lingering on the stack for the assertion.
const createObservedAbortedComposite = () => {
const perReq = new AbortController();
const composite = AbortSignal.any([perReq.signal, longLived.signal]);
composite.addEventListener('abort', handler);
perReq.abort();
};
for (let i = 0; i < limit; i++) {
createObservedAbortedComposite();
}

await gcUntil(
'observed dependents are dropped after transitive abort',
() => size() === 0,
);
});
});

it('does not prevent source signal from being GCed if it is short-lived', (t, done) => {
Expand Down
Loading