Skip to content

Release the caller abort listener in getTimeoutPromise#1640

Merged
mcintyre94 merged 1 commit into
anza-xyz:mainfrom
kh0ra:fix-timeout-listener-leak
May 18, 2026
Merged

Release the caller abort listener in getTimeoutPromise#1640
mcintyre94 merged 1 commit into
anza-xyz:mainfrom
kh0ra:fix-timeout-listener-leak

Conversation

@kh0ra
Copy link
Copy Markdown
Contributor

@kh0ra kh0ra commented May 13, 2026

Summary

getTimeoutPromise in @solana/transaction-confirmation registers an abort listener on the caller supplied AbortSignal but never removes it. When a caller reuses the same signal across multiple calls, listeners accumulate. This change brings the function in line with the auto-cleanup pattern already used by the three sibling confirmation strategies in the same package.

Before

export async function getTimeoutPromise({ abortSignal: callerAbortSignal, commitment }: Config) {
    return await new Promise((_, reject) => {
        const handleAbort = (e: AbortSignalEventMap['abort']) => {
            clearTimeout(timeoutId);
            const abortError = new DOMException((e.target as AbortSignal).reason, 'AbortError');
            reject(abortError);
        };
        callerAbortSignal.addEventListener('abort', handleAbort);
        // ...
    });
}

The third argument to addEventListener is omitted. There is no matching removeEventListener, so the listener stays attached for the lifetime of the caller signal regardless of how the promise settles.

After

getTimeoutPromise now creates an inner AbortController, registers the caller signal listener with { signal: innerController.signal }, and aborts the inner controller in a finally block. The platform releases the listener automatically when the inner controller aborts. The setTimeout handle is also cleared in finally so that the underlying timer is released on every exit path.

This is the same pattern already used by:

  • confirmation-strategy-recent-signature.ts:87
  • confirmation-strategy-racer.ts:28
  • confirmation-strategy-blockheight.ts:89

Before this change, confirmation-strategy-timeout.ts was the only file in the package that deviated from this convention.

Behavioral impact

None for callers. The function still throws a TimeoutError when the timeout elapses and an AbortError when the caller signal aborts, exactly as documented. The four existing tests pass unchanged.

The leak itself is silent in the common case (a fresh AbortController per confirmation) but surfaces when callers reuse a long-lived signal across many timeout calls. In Node this can produce a MaxListenersExceededWarning once the listener count crosses the implicit cap. In any runtime it grows memory proportionally to the number of calls made.

Tests

Three new tests cover the cleanup contract:

  1. registers the caller abort listener with an auto-cleanup signal verifies synchronously that the listener is registered with the signal option.
  2. aborts the cleanup signal once the timeout elapses verifies that the inner cleanup signal aborts after the timeout fires, releasing the listener.
  3. aborts the cleanup signal when the caller aborts verifies the same on the caller abort path.

The four existing tests in confirmation-strategy-timeout-test.ts continue to pass without modification.

Closes

Closes #1639

getTimeoutPromise registered an abort listener on the caller's AbortSignal but never removed it. This change registers the listener with an auto-cleanup signal option and aborts the inner controller in finally, matching the pattern already used by the other three confirmation strategies in this package.
@changeset-bot
Copy link
Copy Markdown

changeset-bot Bot commented May 13, 2026

🦋 Changeset detected

Latest commit: 55c9a31

The changes in this PR will be included in the next version bump.

This PR includes changesets to release 47 packages
Name Type
@solana/transaction-confirmation Patch
@solana/kit Patch
@solana/accounts Patch
@solana/addresses Patch
@solana/assertions Patch
@solana/codecs-core Patch
@solana/codecs-data-structures Patch
@solana/codecs-numbers Patch
@solana/codecs-strings Patch
@solana/codecs Patch
@solana/compat Patch
@solana/errors Patch
@solana/fast-stable-stringify Patch
@solana/fixed-points Patch
@solana/functional Patch
@solana/instruction-plans Patch
@solana/instructions Patch
@solana/keys Patch
@solana/nominal-types Patch
@solana/offchain-messages Patch
@solana/options Patch
@solana/plugin-core Patch
@solana/plugin-interfaces Patch
@solana/program-client-core Patch
@solana/programs Patch
@solana/promises Patch
@solana/react Patch
@solana/rpc-api Patch
@solana/rpc-graphql Patch
@solana/rpc-parsed-types Patch
@solana/rpc-spec-types Patch
@solana/rpc-spec Patch
@solana/rpc-subscriptions-api Patch
@solana/rpc-subscriptions-channel-websocket Patch
@solana/rpc-subscriptions-spec Patch
@solana/rpc-subscriptions Patch
@solana/rpc-transformers Patch
@solana/rpc-transport-http Patch
@solana/rpc-types Patch
@solana/rpc Patch
@solana/signers Patch
@solana/subscribable Patch
@solana/sysvars Patch
@solana/transaction-messages Patch
@solana/transactions Patch
@solana/wallet-account-signer Patch
@solana/webcrypto-ed25519-polyfill Patch

Not sure what this means? Click here to learn what changesets are.

Click here if you're a maintainer who wants to add another changeset to this PR

@bundlemon
Copy link
Copy Markdown

bundlemon Bot commented May 15, 2026

BundleMon

Files updated (4)
Status Path Size Limits
@solana/kit production bundle
kit/dist/index.production.min.js
52.26KB (+21B +0.04%) -
transaction-confirmation/dist/index.native.mj
s
2.37KB (+19B +0.79%) -
transaction-confirmation/dist/index.node.mjs
2.42KB (+19B +0.77%) -
transaction-confirmation/dist/index.browser.m
js
2.37KB (+18B +0.75%) -
Unchanged files (143)
Status Path Size Limits
errors/dist/index.node.mjs
20.54KB -
errors/dist/index.browser.mjs
20.52KB -
errors/dist/index.native.mjs
20.52KB -
rpc-graphql/dist/index.browser.mjs
18.82KB -
rpc-graphql/dist/index.native.mjs
18.81KB -
rpc-graphql/dist/index.node.mjs
18.81KB -
wallet-account-signer/dist/index.node.mjs
17.38KB -
wallet-account-signer/dist/index.browser.mjs
17.37KB -
wallet-account-signer/dist/index.native.mjs
17.37KB -
transaction-messages/dist/index.browser.mjs
11.32KB -
transaction-messages/dist/index.native.mjs
11.32KB -
transaction-messages/dist/index.node.mjs
11.32KB -
instruction-plans/dist/index.browser.mjs
6.58KB -
instruction-plans/dist/index.native.mjs
6.58KB -
instruction-plans/dist/index.node.mjs
6.58KB -
fixed-points/dist/index.browser.mjs
5.08KB -
fixed-points/dist/index.native.mjs
5.07KB -
fixed-points/dist/index.node.mjs
5.07KB -
codecs-data-structures/dist/index.browser.mjs
5.04KB -
codecs-data-structures/dist/index.native.mjs
5.03KB -
codecs-data-structures/dist/index.node.mjs
5.03KB -
offchain-messages/dist/index.browser.mjs
4.89KB -
offchain-messages/dist/index.native.mjs
4.89KB -
offchain-messages/dist/index.node.mjs
4.89KB -
transactions/dist/index.browser.mjs
4.07KB -
transactions/dist/index.native.mjs
4.07KB -
transactions/dist/index.node.mjs
4.07KB -
kit/dist/index.browser.mjs
3.97KB -
kit/dist/index.native.mjs
3.97KB -
kit/dist/index.node.mjs
3.97KB -
codecs-core/dist/index.browser.mjs
3.62KB -
codecs-core/dist/index.native.mjs
3.62KB -
codecs-core/dist/index.node.mjs
3.62KB -
webcrypto-ed25519-polyfill/dist/index.node.mj
s
3.61KB -
webcrypto-ed25519-polyfill/dist/index.browser
.mjs
3.59KB -
webcrypto-ed25519-polyfill/dist/index.native.
mjs
3.57KB -
rpc-subscriptions/dist/index.browser.mjs
3.37KB -
rpc-subscriptions/dist/index.node.mjs
3.34KB -
rpc-subscriptions/dist/index.native.mjs
3.31KB -
signers/dist/index.browser.mjs
3.26KB -
signers/dist/index.native.mjs
3.26KB -
signers/dist/index.node.mjs
3.26KB -
rpc-transformers/dist/index.browser.mjs
3.16KB -
rpc-transformers/dist/index.native.mjs
3.16KB -
rpc-transformers/dist/index.node.mjs
3.16KB -
react/dist/index.browser.mjs
3.09KB -
react/dist/index.native.mjs
3.09KB -
react/dist/index.node.mjs
3.09KB -
keys/dist/index.node.mjs
3.06KB -
addresses/dist/index.browser.mjs
2.93KB -
addresses/dist/index.native.mjs
2.92KB -
addresses/dist/index.node.mjs
2.92KB -
keys/dist/index.browser.mjs
2.85KB -
keys/dist/index.native.mjs
2.85KB -
subscribable/dist/index.node.mjs
2.68KB -
subscribable/dist/index.native.mjs
2.61KB -
subscribable/dist/index.browser.mjs
2.6KB -
codecs-strings/dist/index.browser.mjs
2.55KB -
codecs-strings/dist/index.node.mjs
2.51KB -
codecs-strings/dist/index.native.mjs
2.47KB -
sysvars/dist/index.browser.mjs
2.37KB -
sysvars/dist/index.native.mjs
2.37KB -
sysvars/dist/index.node.mjs
2.37KB -
rpc-subscriptions-spec/dist/index.node.mjs
2.25KB -
rpc-subscriptions-spec/dist/index.native.mjs
2.2KB -
rpc-subscriptions-spec/dist/index.browser.mjs
2.2KB -
rpc/dist/index.node.mjs
1.95KB -
codecs-numbers/dist/index.browser.mjs
1.95KB -
codecs-numbers/dist/index.native.mjs
1.95KB -
codecs-numbers/dist/index.node.mjs
1.94KB -
rpc-transport-http/dist/index.browser.mjs
1.91KB -
rpc-transport-http/dist/index.native.mjs
1.9KB -
rpc/dist/index.native.mjs
1.81KB -
rpc-types/dist/index.browser.mjs
1.8KB -
rpc/dist/index.browser.mjs
1.8KB -
rpc-types/dist/index.native.mjs
1.8KB -
rpc-types/dist/index.node.mjs
1.8KB -
rpc-transport-http/dist/index.node.mjs
1.72KB -
rpc-subscriptions-channel-websocket/dist/inde
x.node.mjs
1.33KB -
rpc-subscriptions-channel-websocket/dist/inde
x.native.mjs
1.27KB -
rpc-subscriptions-channel-websocket/dist/inde
x.browser.mjs
1.26KB -
program-client-core/dist/index.browser.mjs
1.21KB -
program-client-core/dist/index.native.mjs
1.21KB -
program-client-core/dist/index.node.mjs
1.21KB -
options/dist/index.browser.mjs
1.18KB -
options/dist/index.native.mjs
1.18KB -
options/dist/index.node.mjs
1.17KB -
accounts/dist/index.browser.mjs
1.17KB -
accounts/dist/index.native.mjs
1.17KB -
accounts/dist/index.node.mjs
1.16KB -
rpc-api/dist/index.browser.mjs
976B -
rpc-api/dist/index.native.mjs
975B -
rpc-api/dist/index.node.mjs
973B -
compat/dist/index.browser.mjs
969B -
compat/dist/index.native.mjs
968B -
compat/dist/index.node.mjs
966B -
rpc-spec-types/dist/index.browser.mjs
962B -
rpc-spec-types/dist/index.native.mjs
961B -
rpc-spec-types/dist/index.node.mjs
959B -
rpc-spec/dist/index.browser.mjs
918B -
rpc-spec/dist/index.native.mjs
918B -
rpc-spec/dist/index.node.mjs
917B -
rpc-subscriptions-api/dist/index.native.mjs
870B -
rpc-subscriptions-api/dist/index.node.mjs
869B -
rpc-subscriptions-api/dist/index.browser.mjs
868B -
promises/dist/index.native.mjs
841B -
promises/dist/index.node.mjs
840B -
promises/dist/index.browser.mjs
839B -
plugin-core/dist/index.browser.mjs
820B -
plugin-core/dist/index.native.mjs
819B -
plugin-core/dist/index.node.mjs
817B -
assertions/dist/index.browser.mjs
783B -
instructions/dist/index.browser.mjs
771B -
instructions/dist/index.native.mjs
770B -
instructions/dist/index.node.mjs
768B -
fast-stable-stringify/dist/index.browser.mjs
726B -
fast-stable-stringify/dist/index.native.mjs
725B -
assertions/dist/index.native.mjs
724B -
fast-stable-stringify/dist/index.node.mjs
724B -
assertions/dist/index.node.mjs
723B -
programs/dist/index.browser.mjs
329B -
programs/dist/index.native.mjs
327B -
programs/dist/index.node.mjs
325B -
fs-impl/dist/index.browser.mjs
245B -
event-target-impl/dist/index.node.mjs
230B -
functional/dist/index.browser.mjs
154B -
functional/dist/index.native.mjs
152B -
text-encoding-impl/dist/index.native.mjs
152B -
functional/dist/index.node.mjs
151B -
codecs/dist/index.browser.mjs
145B -
codecs/dist/index.native.mjs
144B -
codecs/dist/index.node.mjs
142B -
event-target-impl/dist/index.browser.mjs
133B -
ws-impl/dist/index.node.mjs
131B -
text-encoding-impl/dist/index.browser.mjs
122B -
fs-impl/dist/index.node.mjs
120B -
text-encoding-impl/dist/index.node.mjs
119B -
ws-impl/dist/index.browser.mjs
113B -
crypto-impl/dist/index.node.mjs
111B -
crypto-impl/dist/index.browser.mjs
109B -
rpc-parsed-types/dist/index.browser.mjs
66B -
rpc-parsed-types/dist/index.native.mjs
65B -
rpc-parsed-types/dist/index.node.mjs
63B -

Total files change +77B +0.01%

Final result: ✅

View report in BundleMon website ➡️


Current branch size history | Target branch size history

@kh0ra
Copy link
Copy Markdown
Contributor Author

kh0ra commented May 16, 2026

@kit-content-writer @steveluscher @beeman @alessandrod @ChALkeR @mcintyre94

Copy link
Copy Markdown
Member

@mcintyre94 mcintyre94 left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Great spot, thankyou!

Edit: Sorry, merge is blocked on unrelated CI issues - will get that sorted! That's unrelated to your changes, this PR is good to go.

@mcintyre94 mcintyre94 added this pull request to the merge queue May 18, 2026
@github-merge-queue github-merge-queue Bot removed this pull request from the merge queue due to failed status checks May 18, 2026
@mcintyre94 mcintyre94 added this pull request to the merge queue May 18, 2026
Merged via the queue into anza-xyz:main with commit 953eed6 May 18, 2026
12 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

getTimeoutPromise never removes its abort listener from the caller signal

2 participants