From 20555c7de06b4112ac03f918b2640447e12e470e Mon Sep 17 00:00:00 2001 From: Salah-Eddine Saakoun Date: Wed, 28 May 2025 18:11:01 +0200 Subject: [PATCH 1/2] tests: confirm rejection of pending getLatestBlock requests when block tracker is destroyed --- src/PollingBlockTracker.test.ts | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/src/PollingBlockTracker.test.ts b/src/PollingBlockTracker.test.ts index 436dbca..dc740c1 100644 --- a/src/PollingBlockTracker.test.ts +++ b/src/PollingBlockTracker.test.ts @@ -822,7 +822,7 @@ describe('PollingBlockTracker', () => { }); }); - it('should reject pending latest block request if block tracker is stopped before fetch completes on second getLatestBlock call', async () => { + it('should reject pending latest block request if block tracker is destroyed before fetch completes on second getLatestBlock call', async () => { const setTimeoutRecorder = recordCallsToSetTimeout(); const blockTrackerOptions = { pollingInterval: 100, @@ -873,8 +873,8 @@ describe('PollingBlockTracker', () => { // Step 3: Immediately after, call getLatestBlock const secondBlockPromise = blockTracker.getLatestBlock(); - // Step 4: Immediately after, stop the block tracker - blockTracker.removeAllListeners(); + // Step 4: Immediately after, destroy the block tracker + await blockTracker.destroy(); // Verify block tracker state expect(blockTracker.isRunning()).toBe(false); From 109f0d4af992d3587892fbedad6f166a0ee5cfcf Mon Sep 17 00:00:00 2001 From: Salah-Eddine Saakoun Date: Thu, 29 May 2025 00:36:05 +0200 Subject: [PATCH 2/2] fix: unit tests --- src/PollingBlockTracker.test.ts | 81 +++++++++++++++++++++++++++++++++ 1 file changed, 81 insertions(+) diff --git a/src/PollingBlockTracker.test.ts b/src/PollingBlockTracker.test.ts index dc740c1..b940d66 100644 --- a/src/PollingBlockTracker.test.ts +++ b/src/PollingBlockTracker.test.ts @@ -822,6 +822,87 @@ describe('PollingBlockTracker', () => { }); }); + it('should reject pending latest block request if block tracker is stopped before fetch completes on second getLatestBlock call', async () => { + const setTimeoutRecorder = recordCallsToSetTimeout(); + const blockTrackerOptions = { + pollingInterval: 100, + blockResetDuration: 200, + }; + + await withPollingBlockTracker( + { + provider: { + stubs: [ + { + methodName: 'eth_blockNumber', + result: '0x0', + }, + { + methodName: 'eth_blockNumber', + result: '0x0', + }, + ], + }, + blockTracker: blockTrackerOptions, + }, + async ({ blockTracker }) => { + // Step 1: Start the block tracker + blockTracker.on('latest', EMPTY_FUNCTION); + + // Step 2: Wait for the first block update to resolve + await new Promise((resolve) => { + blockTracker.on('sync', resolve); + }); + expect(blockTracker.getCurrentBlock()).toBe('0x0'); + expect(blockTracker.isRunning()).toBe(true); + + // Clear the current block to force a new request for the next getLatestBlock + // When the block tracker stops, there may be two `setTimeout`s in + // play: one to go to the next iteration of the block tracker + // loop, another to expire the current block number cache. We don't + // know which one has been added first, so we have to find it. + blockTracker.removeAllListeners(); + await setTimeoutRecorder.nextMatchingDuration( + blockTrackerOptions.blockResetDuration, + ); + expect(blockTracker.getCurrentBlock()).toBeNull(); + + // Restart the tracker for the second call + blockTracker.on('latest', EMPTY_FUNCTION); + + // Step 3: Immediately after, call getLatestBlock + const secondBlockPromise = blockTracker.getLatestBlock(); + + // Step 4: Immediately after, stop the block tracker + blockTracker.removeAllListeners(); + + // Verify block tracker state + expect(blockTracker.isRunning()).toBe(false); + expect(blockTracker.getCurrentBlock()).toBeNull(); + + // The call to getLatestBlock would then never resolve (should be rejected) + await expect(secondBlockPromise).rejects.toThrow( + 'Block tracker destroyed', + ); + + // Verify that the block reset timeout is set up + expect( + setTimeoutRecorder.calls.some((call) => { + return call.duration === blockTrackerOptions.blockResetDuration; + }), + ).toBe(true); + + // Wait for the block reset timeout to complete + await setTimeoutRecorder.nextMatchingDuration( + blockTrackerOptions.blockResetDuration, + ); + + // Verify that the current block is still null after the timeout + expect(blockTracker.getCurrentBlock()).toBeNull(); + }, + ); + }); + it('should reject pending latest block request if block tracker is destroyed before fetch completes on second getLatestBlock call', async () => { const setTimeoutRecorder = recordCallsToSetTimeout(); const blockTrackerOptions = {