Repository navigation
remove the abort listener from their signal when a fetch settles - #418
Open
OsamaAnsar wants to merge 1 commit into
Open
OsamaAnsar wants to merge 1 commit into
OsamaAnsar wants to merge 1 commit into
Conversation
fetch() added an abort listener to options.signal that was only removed if the internal AbortController aborted, never when the fetch finished. With a long-lived signal (an app-wide shutdown signal, say), every completed fetch left a listener behind, and through the shared closure those listeners kept the fetched values and promises alive after they were evicted from the cache. The listener is now removed when the fetch resolves or rejects. Also, a signal that was already aborted was ignored, because the listener never fired: fetchMethod ran with a signal that was not aborted, and the fetch resolved normally. Now the fetch rejects with the signal's reason, and fetchMethod is passed a signal that is already aborted.
This branch has not been deployed
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
#backgroundFetchadds an abort listener tooptions.signal, but the only thing that removed it was the internalAbortControlleraborting. When a fetch finished normally the listener stayed. With a long-lived signal (an app-wide shutdown signal, say), every completed fetch left a listener behind, and through the shared closure each listener kept that fetch's value and promise alive after the entry was evicted.Second problem, same code: a
signalthat was already aborted was ignored, because the listener never fires for it.fetchMethodran with a signal that was not aborted, and thefetch()resolved normally.Repro on Node v24.19.0 (
node --expose-gc), numbers from that run:The fix keeps the handler in a named function, adds it with a plain
addEventListener, and removes it withremoveEventListenerin both the resolve and reject callbacks. Ifsignal.abortedis already true, the internal controller is aborted straight away, and the abort handling inpcallalso runs right after it is registered (the event would never fire for it), so the fetch rejects with the signal's reason, as it does when the signal aborts right afterfetch()is called. One thing that changes: with an already-aborted signal,fetchMethodis still called, but now it gets a signal that is already aborted.I also compared four abort scenarios before and after (abort mid-fetch, abort mid-fetch with
allowStaleOnFetchAbortand a stale value, abort after the fetch completed, and the signal seen byfetchMethodafter an abort) and the results are identical.Tests are in the new
test/fetch-signal-listeners.ts(new file so the tracing snapshots do not change). They check the listener count while fetches are pending and after resolve, reject, a syncfetchMethodand a throwing one; that aborting mid-fetch still aborts the fetch (also withallowStaleOnFetchAbort); the pre-aborted signal (withstatus, and withignoreFetchAbort); and aWeakRef+gc()check that evicted values are not kept alive by a shared signal (skipped without--expose-gc, which.taprcalready sets). With only the source change reverted, 12 assertions in those tests fail (the listener count, pre-aborted andWeakRefones); the two abort-mid-fetch tests pass both ways, as they are there to guard the existing behavior. With the fix, all 34 pass.This touches the same area as #415 (
staleFetchTimeout). The two do not conflict in intent, but this patch does not apply as-is on top of #415's branch because of the context lines at the top of the fetch callback, so whichever lands second needs a small rebase.Checked on Node v24.19.0 on Windows: full
tap -t0run (coverage enforcement left on) passes with 0 failures,oxlint src testreports 0 warnings and 0 errors, andprettier --checkis clean. I did not run Linux or macOS, other Node versions, or a browser. (On this machine I rantapwith a small preload that falls back to junctions forsymlink, and installed@tapjs/clockwith--no-save, see #413. Neither is part of this PR.)