diff --git a/.changeset/early-eyes-mix.md b/.changeset/early-eyes-mix.md new file mode 100644 index 000000000000..4f2678bccb0b --- /dev/null +++ b/.changeset/early-eyes-mix.md @@ -0,0 +1,5 @@ +--- +'@solana/rpc': patch +--- + +Fixed a bug where coalesced RPC calls could end up aborted even though there were still interested consumers. This would happen if the consumer count fell to zero, then rose above zero again, in the same runloop. diff --git a/package.json b/package.json index adf7b376f133..6f0de9cb4437 100644 --- a/package.json +++ b/package.json @@ -58,7 +58,10 @@ "packageManager": "pnpm@9.1.0", "pnpm": { "overrides": { - "@solana/web3.js": "workspace:*", + "@solana/buffer-layout-utils>@solana/web3.js": "workspace:*", + "@solana/spl-token>@solana/web3.js": "workspace:*", + "@solana/spl-token-group>@solana/web3.js": "workspace:*", + "@solana/spl-token-metadata>@solana/web3.js": "workspace:*", "@wallet-standard/base": "pre", "conventional-changelog-conventionalcommits": ">= 8.0.0", "jsdom": "^22", diff --git a/packages/rpc/src/__tests__/rpc-request-coalescer-test.ts b/packages/rpc/src/__tests__/rpc-request-coalescer-test.ts index 567188deeab2..a8bca1eb7200 100644 --- a/packages/rpc/src/__tests__/rpc-request-coalescer-test.ts +++ b/packages/rpc/src/__tests__/rpc-request-coalescer-test.ts @@ -79,6 +79,22 @@ describe('RPC request coalescer', () => { const expectationB = expect(responsePromiseB).rejects.toBe(mockErrorB); await Promise.all([expectationA, expectationB]); }); + it('does not abort the transport when the number of consumers increases, falls to zero, then increases again in the same runloop', async () => { + expect.assertions(2); + const abortControllerA = new AbortController(); + const abortControllerB = new AbortController(); + coalescedTransport({ payload: null, signal: abortControllerA.signal }).catch(() => {}); + coalescedTransport({ payload: null, signal: abortControllerB.signal }).catch(() => {}); + // Both abort, bringing the consumer count to zero. + abortControllerA.abort('o no A'); + abortControllerB.abort('o no B'); + // New request comes in at the last moment before the end of the runloop. + coalescedTransport({ payload: null }); + await jest.runOnlyPendingTimersAsync(); + expect(mockTransport).toHaveBeenCalledTimes(1); + const transportAbortSignal = mockTransport.mock.lastCall![0].signal!; + expect(transportAbortSignal.aborted).toBe(false); + }); describe('multiple coalesced requests each with an abort signal', () => { let abortControllerA: AbortController; let abortControllerB: AbortController; @@ -120,9 +136,13 @@ describe('RPC request coalescer', () => { abortControllerA.abort('o no'); await expect(responsePromiseA).rejects.toBe('o no'); }); - it('aborts the transport when all of the requests abort', () => { + it('aborts the transport at the end of the runloop when all of the requests abort', async () => { + expect.assertions(1); + responsePromiseA.catch(() => {}); + responsePromiseB.catch(() => {}); abortControllerA.abort('o no A'); abortControllerB.abort('o no B'); + await jest.runOnlyPendingTimersAsync(); // eslint-disable-next-line @typescript-eslint/no-non-null-assertion const transportAbortSignal = mockTransport.mock.lastCall![0].signal!; expect(transportAbortSignal.aborted).toBe(true); diff --git a/packages/rpc/src/rpc-request-coalescer.ts b/packages/rpc/src/rpc-request-coalescer.ts index 78edc62524eb..83ca40ba1c57 100644 --- a/packages/rpc/src/rpc-request-coalescer.ts +++ b/packages/rpc/src/rpc-request-coalescer.ts @@ -69,10 +69,12 @@ export function getRpcTransportWithRequestCoalescing { signal.removeEventListener('abort', handleAbort); coalescedRequest.numConsumers -= 1; - if (coalescedRequest.numConsumers === 0) { - const abortController = coalescedRequest.abortController; - abortController.abort(EXPLICIT_ABORT_TOKEN); - } + Promise.resolve().then(() => { + if (coalescedRequest.numConsumers === 0) { + const abortController = coalescedRequest.abortController; + abortController.abort(EXPLICIT_ABORT_TOKEN); + } + }); reject((e.target as AbortSignal).reason); }; signal.addEventListener('abort', handleAbort); diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 902bbb4f5138..40dd280bdc28 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -5,7 +5,10 @@ settings: excludeLinksFromLockfile: false overrides: - '@solana/web3.js': workspace:* + '@solana/buffer-layout-utils>@solana/web3.js': workspace:* + '@solana/spl-token>@solana/web3.js': workspace:* + '@solana/spl-token-group>@solana/web3.js': workspace:* + '@solana/spl-token-metadata>@solana/web3.js': workspace:* '@wallet-standard/base': pre conventional-changelog-conventionalcommits: '>= 8.0.0' jsdom: ^22 @@ -290,7 +293,7 @@ importers: version: 5.4.5 devDependencies: '@solana/web3.js': - specifier: workspace:* + specifier: workspace:../library-legacy version: link:../library-legacy packages/crypto-impl: {}