You signed in with another tab or window. Reload to refresh your session.You signed out in another tab or window. Reload to refresh your session.You switched accounts on another tab or window. Reload to refresh your session.Dismiss alert
{{ message }}
Repository navigation
AbortSignal.any() stops following a timeout signal once a listener is added to the timeout and removed, so the composite never aborts after a GC #66610
// node --expose-gc e1.mjsconstsleep=ms=>newPromise(resolve=>setTimeout(resolve,ms));for(consttouchof[false,true]){constcomposite=(()=>{consttimeout=AbortSignal.timeout(100);constmade=AbortSignal.any([timeout]);if(touch){constlistener=()=>{};timeout.addEventListener("abort",listener);timeout.removeEventListener("abort",listener);}returnmade;})();composite.addEventListener("abort",()=>{});awaitsleep(10);globalThis.gc();awaitsleep(300);console.log(`listener added and removed on the timeout: ${touch}; composite aborted: ${composite.aborted}`);}
How often does it reproduce? Is there a required condition?
Always: 3 of 3 runs on each version. The timeout must have had a listener that was then removed, and a garbage collection must run before it fires.
What is the expected behavior? Why is that the expected behavior?
listener added and removed on the timeout: true; composite aborted: true. A composite aborts when one of its sources does, whatever listeners came and went on the source.
What do you see instead?
composite aborted: false: the timeout is collected before it fires, so the composite never aborts.
Additional information
#57867 keeps a timeout that AbortSignal.any() follows in gcPersistentSignals. Removing the timeout's last abort listener seems to delete it from that set while a composite still follows it. We hit this in code that hands a timeout to AbortSignal.any() and also waits on the timeout itself, for example with timers/promises' setTimeout(ms, value, { signal }), which adds a listener and removes it.
Activity