From ad71b8f12dbe3ae206134a92303528bc3329e8fe Mon Sep 17 00:00:00 2001 From: Jiexi Luan Date: Tue, 14 Oct 2025 09:23:37 -0700 Subject: [PATCH 01/10] clear block cache after blockResetTimeout on update --- src/PollingBlockTracker.ts | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/src/PollingBlockTracker.ts b/src/PollingBlockTracker.ts index 79514f2..af260d4 100644 --- a/src/PollingBlockTracker.ts +++ b/src/PollingBlockTracker.ts @@ -326,6 +326,12 @@ export class PollingBlockTracker // fetch + set latest block const latestBlock = await this._fetchLatestBlock(); this._newPotentialLatest(latestBlock); + + if (!this._isRunning) { + // Ensure the one-time update is eventually reset once it's stale + this._setupBlockResetTimeout(); + } + // _newPotentialLatest() ensures that this._currentBlock is not null // eslint-disable-next-line @typescript-eslint/no-non-null-assertion return this._currentBlock!; From b9917d2d5cd1cb10069bbbb7067e188def4e226d Mon Sep 17 00:00:00 2001 From: Jiexi Luan Date: Tue, 14 Oct 2025 10:07:25 -0700 Subject: [PATCH 02/10] fix existing tests --- src/PollingBlockTracker.test.ts | 22 ++++++++++++++++------ 1 file changed, 16 insertions(+), 6 deletions(-) diff --git a/src/PollingBlockTracker.test.ts b/src/PollingBlockTracker.test.ts index 385e5f6..aa9051c 100644 --- a/src/PollingBlockTracker.test.ts +++ b/src/PollingBlockTracker.test.ts @@ -1017,9 +1017,11 @@ describe('PollingBlockTracker', () => { }); it('should fetch a new block even if a block is already cached and more than the polling interval time has passed since the last call', async () => { - recordCallsToSetTimeout({ - numAutomaticCalls: 1, - }); + const setTimeoutRecorder = recordCallsToSetTimeout(); + const blockTrackerOptions = { + pollingInterval: 100, + blockResetDuration: 200, + }; await withPollingBlockTracker( { @@ -1035,9 +1037,13 @@ describe('PollingBlockTracker', () => { }, ], }, + blockTracker: blockTrackerOptions, }, async ({ blockTracker }) => { await blockTracker.getLatestBlock(); + await setTimeoutRecorder.nextMatchingDuration( + blockTrackerOptions.pollingInterval, + ); const block = await blockTracker.getLatestBlock({ useCache: false, }); @@ -1306,7 +1312,7 @@ describe('PollingBlockTracker', () => { ); }); - it('should never start a timer to clear the current block number later', async () => { + it('should start a timer to clear the current block number later', async () => { const setTimeoutRecorder = recordCallsToSetTimeout(); const blockResetDuration = 1000; @@ -1330,8 +1336,12 @@ describe('PollingBlockTracker', () => { await new Promise((resolve) => originalSetTimeout(resolve, blockResetDuration), ); - - expect(setTimeoutRecorder.calls).toHaveLength(0); + const blockResetTimeouts = setTimeoutRecorder.calls.filter( + (call) => { + return call.duration === blockResetDuration; + }, + ); + expect(blockResetTimeouts).toHaveLength(1); expect(blockTracker.getCurrentBlock()).toBe('0x0'); }, ); From 7e861384ff5f3bef3fd3fdbf49c61eff09e93214 Mon Sep 17 00:00:00 2001 From: Jiexi Luan Date: Tue, 14 Oct 2025 10:10:28 -0700 Subject: [PATCH 03/10] cleanup useCache: false scenario/test names --- src/PollingBlockTracker.test.ts | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/src/PollingBlockTracker.test.ts b/src/PollingBlockTracker.test.ts index aa9051c..a7c9164 100644 --- a/src/PollingBlockTracker.test.ts +++ b/src/PollingBlockTracker.test.ts @@ -985,9 +985,9 @@ describe('PollingBlockTracker', () => { }); }); - describe('with useCache: false', () => { + describe('with useCache: false and a block number is already cached', () => { describe('when the block tracker is not running', () => { - it('should not fetch a new block even if a block is already cached and less than the polling interval time has passed since the last call', async () => { + it('should not fetch a new block even if less than the polling interval time has passed since the last call', async () => { recordCallsToSetTimeout(); await withPollingBlockTracker( @@ -1016,7 +1016,7 @@ describe('PollingBlockTracker', () => { ); }); - it('should fetch a new block even if a block is already cached and more than the polling interval time has passed since the last call', async () => { + it('should fetch a new block even if more than the polling interval time has passed since the last call', async () => { const setTimeoutRecorder = recordCallsToSetTimeout(); const blockTrackerOptions = { pollingInterval: 100, @@ -1055,7 +1055,7 @@ describe('PollingBlockTracker', () => { }); describe('when the block tracker is already started', () => { - it('should wait for the next block event even if a block is already cached', async () => { + it('should wait for the next block event', async () => { const setTimeoutRecorder = recordCallsToSetTimeout(); await withPollingBlockTracker( From d2a1d7611a777629b180414fa37ceb2bc0daf774 Mon Sep 17 00:00:00 2001 From: Jiexi Luan Date: Tue, 14 Oct 2025 10:14:03 -0700 Subject: [PATCH 04/10] add getLatestBlock when not polling clear block test --- src/PollingBlockTracker.test.ts | 35 +++++++++++++++++++++++++++++++++ 1 file changed, 35 insertions(+) diff --git a/src/PollingBlockTracker.test.ts b/src/PollingBlockTracker.test.ts index a7c9164..842f02e 100644 --- a/src/PollingBlockTracker.test.ts +++ b/src/PollingBlockTracker.test.ts @@ -188,6 +188,41 @@ describe('PollingBlockTracker', () => { expect(block).toBe('0x0'); }); }); + + it('should start a timer to clear the current block number later', async () => { + const setTimeoutRecorder = recordCallsToSetTimeout(); + const blockResetDuration = 1000; + + await withPollingBlockTracker( + { + blockTracker: { + blockResetDuration, + }, + provider: { + stubs: [ + { + methodName: 'eth_blockNumber', + result: '0x0', + }, + ], + }, + }, + async ({ blockTracker }) => { + await blockTracker.getLatestBlock(); + + await new Promise((resolve) => + originalSetTimeout(resolve, blockResetDuration), + ); + const blockResetTimeouts = setTimeoutRecorder.calls.filter( + (call) => { + return call.duration === blockResetDuration; + }, + ); + expect(blockResetTimeouts).toHaveLength(1); + expect(blockTracker.getCurrentBlock()).toBe('0x0'); + }, + ); + }) }); describe('if an error occurs while fetching the latest block number', () => { From 9a00a7aa2f6c6c59932a88b078080d7f891496b4 Mon Sep 17 00:00:00 2001 From: Jiexi Luan Date: Tue, 14 Oct 2025 10:22:03 -0700 Subject: [PATCH 05/10] replace misplaced `should clear the current block number some time after being called` with `should not start a timer to clear the current block number later` --- src/PollingBlockTracker.test.ts | 17 +++++++---------- 1 file changed, 7 insertions(+), 10 deletions(-) diff --git a/src/PollingBlockTracker.test.ts b/src/PollingBlockTracker.test.ts index 842f02e..dca2b65 100644 --- a/src/PollingBlockTracker.test.ts +++ b/src/PollingBlockTracker.test.ts @@ -222,7 +222,7 @@ describe('PollingBlockTracker', () => { expect(blockTracker.getCurrentBlock()).toBe('0x0'); }, ); - }) + }); }); describe('if an error occurs while fetching the latest block number', () => { @@ -507,7 +507,7 @@ describe('PollingBlockTracker', () => { ); }); - it('should clear the current block number some time after being called', async () => { + it('should not start a timer to clear the current block number later', async () => { const setTimeoutRecorder = recordCallsToSetTimeout(); const blockTrackerOptions = { pollingInterval: 100, @@ -531,16 +531,13 @@ describe('PollingBlockTracker', () => { await blockTracker.getLatestBlock(); const currentBlockNumber = blockTracker.getCurrentBlock(); expect(currentBlockNumber).toBe('0x0'); - await blockTracker.destroy(); - // 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. - await setTimeoutRecorder.nextMatchingDuration( - blockTrackerOptions.blockResetDuration, + const blockResetTimeouts = setTimeoutRecorder.calls.filter( + (call) => { + return call.duration === blockTrackerOptions.blockResetDuration; + }, ); - expect(blockTracker.getCurrentBlock()).toBeNull(); + expect(blockResetTimeouts).toHaveLength(0); }, ); }); From a75787033575532cfc3a85cc88f5f85eaa82d7d7 Mon Sep 17 00:00:00 2001 From: Jiexi Luan Date: Tue, 14 Oct 2025 10:22:36 -0700 Subject: [PATCH 06/10] add `should not start a timer to clear the current block number later if the block tracker is running` to `checkForLatestBlock` --- src/PollingBlockTracker.test.ts | 44 ++++++++++++++++++++++++++------- 1 file changed, 35 insertions(+), 9 deletions(-) diff --git a/src/PollingBlockTracker.test.ts b/src/PollingBlockTracker.test.ts index dca2b65..3ad67da 100644 --- a/src/PollingBlockTracker.test.ts +++ b/src/PollingBlockTracker.test.ts @@ -1344,7 +1344,7 @@ describe('PollingBlockTracker', () => { ); }); - it('should start a timer to clear the current block number later', async () => { + it('should start a timer to clear the current block number later if the block tracker is not running', async () => { const setTimeoutRecorder = recordCallsToSetTimeout(); const blockResetDuration = 1000; @@ -1365,20 +1365,46 @@ describe('PollingBlockTracker', () => { async ({ blockTracker }) => { await blockTracker.checkForLatestBlock(); - await new Promise((resolve) => - originalSetTimeout(resolve, blockResetDuration), - ); - const blockResetTimeouts = setTimeoutRecorder.calls.filter( - (call) => { - return call.duration === blockResetDuration; - }, - ); + const blockResetTimeouts = setTimeoutRecorder.calls.filter((call) => { + return call.duration === blockResetDuration; + }); expect(blockResetTimeouts).toHaveLength(1); expect(blockTracker.getCurrentBlock()).toBe('0x0'); }, ); }); + it('should not start a timer to clear the current block number later if the block tracker is running', async () => { + const setTimeoutRecorder = recordCallsToSetTimeout(); + const blockResetDuration = 1000; + + await withPollingBlockTracker( + { + blockTracker: { + blockResetDuration, + }, + provider: { + stubs: [ + { + methodName: 'eth_blockNumber', + result: '0x0', + }, + ], + }, + }, + async ({ blockTracker }) => { + blockTracker.on('latest', EMPTY_FUNCTION); + await blockTracker.checkForLatestBlock(); + + const blockResetTimeouts = setTimeoutRecorder.calls.filter((call) => { + return call.duration === blockResetDuration; + }); + expect(blockResetTimeouts).toHaveLength(0); + expect(blockTracker.getCurrentBlock()).toBe('0x0'); + }, + ); + }); + describe.each([ ['not initialized with `usePastBlocks`', {}], ['initialized with `usePastBlocks: false`', { usePastBlocks: false }], From 384c252070fb327bba22bc9f73aa01f1136e901e Mon Sep 17 00:00:00 2001 From: Jiexi Luan Date: Tue, 14 Oct 2025 10:48:46 -0700 Subject: [PATCH 07/10] changelo --- CHANGELOG.md | 3 +++ 1 file changed, 3 insertions(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index 47e7c93..568a3de 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -7,6 +7,9 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 ## [Unreleased] +### Fixed +- `PollingBlockTracker.checkForLatestBlock()` and `getLatestBlock()` now clear the cached block after `blockReset` duration when called and the `PollingBlockTracker` is not polling. ([#348](https://github.com/MetaMask/eth-block-tracker/pull/348)) + ## [12.2.0] ### Changed From fb0f051dcc53f6ff3272398417f43911eab762f4 Mon Sep 17 00:00:00 2001 From: jiexi Date: Tue, 14 Oct 2025 10:49:25 -0700 Subject: [PATCH 08/10] Apply suggestion from @Gudahtt Co-authored-by: Mark Stacey --- src/PollingBlockTracker.test.ts | 3 --- 1 file changed, 3 deletions(-) diff --git a/src/PollingBlockTracker.test.ts b/src/PollingBlockTracker.test.ts index 3ad67da..0bfda3f 100644 --- a/src/PollingBlockTracker.test.ts +++ b/src/PollingBlockTracker.test.ts @@ -210,9 +210,6 @@ describe('PollingBlockTracker', () => { async ({ blockTracker }) => { await blockTracker.getLatestBlock(); - await new Promise((resolve) => - originalSetTimeout(resolve, blockResetDuration), - ); const blockResetTimeouts = setTimeoutRecorder.calls.filter( (call) => { return call.duration === blockResetDuration; From 3ad871eb0ec6e1a43666d690f4a5b652c1151b17 Mon Sep 17 00:00:00 2001 From: Jiexi Luan Date: Tue, 14 Oct 2025 10:55:20 -0700 Subject: [PATCH 09/10] assert outcome of blockResetDuration --- src/PollingBlockTracker.test.ts | 20 +++++++------------- 1 file changed, 7 insertions(+), 13 deletions(-) diff --git a/src/PollingBlockTracker.test.ts b/src/PollingBlockTracker.test.ts index 0bfda3f..a6410d0 100644 --- a/src/PollingBlockTracker.test.ts +++ b/src/PollingBlockTracker.test.ts @@ -208,15 +208,12 @@ describe('PollingBlockTracker', () => { }, }, async ({ blockTracker }) => { - await blockTracker.getLatestBlock(); - - const blockResetTimeouts = setTimeoutRecorder.calls.filter( - (call) => { - return call.duration === blockResetDuration; - }, + const block = await blockTracker.getLatestBlock(); + expect(block).toBe('0x0'); + await setTimeoutRecorder.nextMatchingDuration( + blockResetDuration, ); - expect(blockResetTimeouts).toHaveLength(1); - expect(blockTracker.getCurrentBlock()).toBe('0x0'); + expect(blockTracker.getCurrentBlock()).toBeNull(); }, ); }); @@ -1361,12 +1358,9 @@ describe('PollingBlockTracker', () => { }, async ({ blockTracker }) => { await blockTracker.checkForLatestBlock(); - - const blockResetTimeouts = setTimeoutRecorder.calls.filter((call) => { - return call.duration === blockResetDuration; - }); - expect(blockResetTimeouts).toHaveLength(1); expect(blockTracker.getCurrentBlock()).toBe('0x0'); + await setTimeoutRecorder.nextMatchingDuration(blockResetDuration); + expect(blockTracker.getCurrentBlock()).toBeNull(); }, ); }); From 172f2496a7742d314addd9ca68d0fa60fa70b798 Mon Sep 17 00:00:00 2001 From: Jiexi Luan Date: Tue, 14 Oct 2025 10:57:13 -0700 Subject: [PATCH 10/10] lint --- CHANGELOG.md | 1 + 1 file changed, 1 insertion(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index 568a3de..5f8c3ca 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -8,6 +8,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 ## [Unreleased] ### Fixed + - `PollingBlockTracker.checkForLatestBlock()` and `getLatestBlock()` now clear the cached block after `blockReset` duration when called and the `PollingBlockTracker` is not polling. ([#348](https://github.com/MetaMask/eth-block-tracker/pull/348)) ## [12.2.0]