diff --git a/lib/internal/abort_controller.js b/lib/internal/abort_controller.js index 55be0b6f366..9722484a422 100644 --- a/lib/internal/abort_controller.js +++ b/lib/internal/abort_controller.js @@ -86,8 +86,8 @@ function lazyMessageChannel() { const clearTimeoutRegistry = new SafeFinalizationRegistry(clearTimeout); const dependantSignalsCleanupRegistry = new SafeFinalizationRegistry( - ({ sourceSignalRef, dependantSignalRef, sourceSignalsCleanupToken }) => { - sourceSignalsCleanupRegistry.unregister(sourceSignalsCleanupToken); + ({ sourceSignalRef, dependantSignalRef }) => { + sourceSignalsCleanupRegistry.unregister(dependantSignalRef); const sourceSignal = sourceSignalRef.deref(); if (sourceSignal === undefined) { @@ -180,12 +180,14 @@ function followCompositeSignal(signal) { dependantSignalsCleanupRegistry.register(signal, { sourceSignalRef: sourceSignalWeakRef, dependantSignalRef: resultSignalWeakRef, - sourceSignalsCleanupToken: sourceSignalWeakRef, }); + // Nested composites share their sources' WeakRefs, so use this signal's + // own WeakRef as the unregister token. Otherwise collecting an inner + // composite would also unregister the outer composite from its sources. sourceSignalsCleanupRegistry.register(sourceSignal, { sourceSignalRef: sourceSignalWeakRef, composedSignalRef: resultSignalWeakRef, - }, sourceSignalWeakRef); + }, resultSignalWeakRef); } signal[kFollowing] = true; diff --git a/test/parallel/test-abortsignal-drop-settled-signals.mjs b/test/parallel/test-abortsignal-drop-settled-signals.mjs index d4c81a7165d..03e0146722e 100644 --- a/test/parallel/test-abortsignal-drop-settled-signals.mjs +++ b/test/parallel/test-abortsignal-drop-settled-signals.mjs @@ -246,6 +246,31 @@ it('drops settled dependent signals when signal is composite', (t, done) => { })); }); +it('drops an observed composite of a composite once its sources are collected', async (t) => { + let controllers = [new AbortController(), new AbortController()]; + + // Only the outer composite is observed, so the inner one is unreachable and + // is collected while the sources are still alive. + const outerRef = (() => { + const outer = AbortSignal.any([ + controllers[0].signal, + AbortSignal.any([controllers[1].signal]), + ]); + outer.addEventListener('abort', () => {}); + return new WeakRef(outer); + })(); + + const kDependantSignals = Object.getOwnPropertySymbols(controllers[1].signal).find( + (s) => s.toString() === 'Symbol(kDependantSignals)' + ); + t.assert.strictEqual(controllers[1].signal[kDependantSignals].size, 2); + + await gcUntil('inner composite is collected', () => controllers[1].signal[kDependantSignals].size === 1); + + controllers = null; + await gcUntil('outer composite is collected', () => outerRef.deref() === undefined); +}); + it('drops settled signals even when there are listeners', (t, done) => { runWithOrphanListeners(limit, async (signalRefs) => { await gcUntil('all signals are GCed', () => {