From 3da873247e7605751b65cdbd6062d155d4396045 Mon Sep 17 00:00:00 2001 From: Jiexi Luan Date: Tue, 7 Oct 2025 10:48:21 -0700 Subject: [PATCH 01/23] add options to getLatestBlock --- src/PollingBlockTracker.ts | 31 ++++++++++++++++++------------- 1 file changed, 18 insertions(+), 13 deletions(-) diff --git a/src/PollingBlockTracker.ts b/src/PollingBlockTracker.ts index a5115c2..3e26597 100644 --- a/src/PollingBlockTracker.ts +++ b/src/PollingBlockTracker.ts @@ -35,8 +35,7 @@ type InternalListener = (value: string) => void; export class PollingBlockTracker extends SafeEventEmitter - implements BlockTracker -{ + implements BlockTracker { private _isRunning: boolean; private readonly _blockResetDuration: number; @@ -111,26 +110,27 @@ export class PollingBlockTracker return this._currentBlock; } - async getLatestBlock(): Promise { + async getLatestBlock({ useCache = true, waitForPending = true }: { useCache?: boolean, waitForPending?: boolean } = {}): Promise { // return if available - if (this._currentBlock) { + if (this._currentBlock && useCache) { return this._currentBlock; } - if (this.#pendingLatestBlock) { + if (this.#pendingLatestBlock && waitForPending) { return await this.#pendingLatestBlock.promise; } const { promise, resolve, reject } = createDeferredPromise({ suppressUnhandledRejection: true, }); - this.#pendingLatestBlock = { reject, promise }; + const pendingLatestBlockPromise = { reject, promise }; + this.#pendingLatestBlock = pendingLatestBlockPromise; + const wasRunning = this._isRunning; try { // If tracker isn't running, just fetch directly - if (!this._isRunning) { - const latestBlock = await this._fetchLatestBlock(); - this._newPotentialLatest(latestBlock); + if (!wasRunning) { + const latestBlock = await this._updateLatestBlock(); resolve(latestBlock); return latestBlock; } @@ -150,7 +150,11 @@ export class PollingBlockTracker reject(error); throw error; } finally { - this.#pendingLatestBlock = undefined; + setTimeout(() => { + if (this.#pendingLatestBlock === pendingLatestBlockPromise) { + this.#pendingLatestBlock = undefined; + } + }, this._pollingInterval); } } @@ -288,8 +292,7 @@ export class PollingBlockTracker } async checkForLatestBlock() { - await this._updateLatestBlock(); - return await this.getLatestBlock(); + return this.getLatestBlock({ useCache: false, waitForPending: false }); } private _start() { @@ -302,10 +305,12 @@ export class PollingBlockTracker this._clearPollingTimeout(); } - private async _updateLatestBlock(): Promise { + private async _updateLatestBlock(): Promise { // fetch + set latest block const latestBlock = await this._fetchLatestBlock(); this._newPotentialLatest(latestBlock); + // _newPotentialLatest ensures that this._currentBlock is not null + return this._currentBlock!; } private async _fetchLatestBlock(): Promise { From f3ef6e64dd5202189fc20d0e708e748b132b2db1 Mon Sep 17 00:00:00 2001 From: Jiexi Luan Date: Tue, 7 Oct 2025 10:51:02 -0700 Subject: [PATCH 02/23] lint --- src/PollingBlockTracker.ts | 11 ++++++++--- 1 file changed, 8 insertions(+), 3 deletions(-) diff --git a/src/PollingBlockTracker.ts b/src/PollingBlockTracker.ts index 3e26597..4ba1274 100644 --- a/src/PollingBlockTracker.ts +++ b/src/PollingBlockTracker.ts @@ -35,7 +35,8 @@ type InternalListener = (value: string) => void; export class PollingBlockTracker extends SafeEventEmitter - implements BlockTracker { + implements BlockTracker +{ private _isRunning: boolean; private readonly _blockResetDuration: number; @@ -110,7 +111,10 @@ export class PollingBlockTracker return this._currentBlock; } - async getLatestBlock({ useCache = true, waitForPending = true }: { useCache?: boolean, waitForPending?: boolean } = {}): Promise { + async getLatestBlock({ + useCache = true, + waitForPending = true, + }: { useCache?: boolean; waitForPending?: boolean } = {}): Promise { // return if available if (this._currentBlock && useCache) { return this._currentBlock; @@ -309,7 +313,8 @@ export class PollingBlockTracker // fetch + set latest block const latestBlock = await this._fetchLatestBlock(); this._newPotentialLatest(latestBlock); - // _newPotentialLatest ensures that this._currentBlock is not null + // _newPotentialLatest() ensures that this._currentBlock is not null + // eslint-disable-next-line @typescript-eslint/no-non-null-assertion return this._currentBlock!; } From 22006e41ea088281fb4a25aab1c9988d3b941f24 Mon Sep 17 00:00:00 2001 From: Jiexi Luan Date: Tue, 7 Oct 2025 10:51:39 -0700 Subject: [PATCH 03/23] cleanup wasRunning --- src/PollingBlockTracker.ts | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/src/PollingBlockTracker.ts b/src/PollingBlockTracker.ts index 4ba1274..9344361 100644 --- a/src/PollingBlockTracker.ts +++ b/src/PollingBlockTracker.ts @@ -130,10 +130,9 @@ export class PollingBlockTracker const pendingLatestBlockPromise = { reject, promise }; this.#pendingLatestBlock = pendingLatestBlockPromise; - const wasRunning = this._isRunning; try { // If tracker isn't running, just fetch directly - if (!wasRunning) { + if (!this._isRunning) { const latestBlock = await this._updateLatestBlock(); resolve(latestBlock); return latestBlock; From 194e85535fba39cf3698565bb25082dd26d86e90 Mon Sep 17 00:00:00 2001 From: Jiexi Luan Date: Tue, 7 Oct 2025 12:02:27 -0700 Subject: [PATCH 04/23] only delay unsetting pendingLatestBlock if the block tracker is not running --- src/PollingBlockTracker.ts | 7 +++++-- 1 file changed, 5 insertions(+), 2 deletions(-) diff --git a/src/PollingBlockTracker.ts b/src/PollingBlockTracker.ts index 9344361..1e523eb 100644 --- a/src/PollingBlockTracker.ts +++ b/src/PollingBlockTracker.ts @@ -132,7 +132,7 @@ export class PollingBlockTracker try { // If tracker isn't running, just fetch directly - if (!this._isRunning) { + if (this._isRunning) { const latestBlock = await this._updateLatestBlock(); resolve(latestBlock); return latestBlock; @@ -153,11 +153,14 @@ export class PollingBlockTracker reject(error); throw error; } finally { + // We want to rate limit calls to this method if we made a direct fetch + // for the block number because the BlockTracker was not running. We + // achieve this by delaying the unsetting of the #pendingLatestBlock promise. setTimeout(() => { if (this.#pendingLatestBlock === pendingLatestBlockPromise) { this.#pendingLatestBlock = undefined; } - }, this._pollingInterval); + }, this._isRunning ? 0 : this._pollingInterval); } } From 6453182f677dcddfafb3a1e06e7576b9e6ffaca6 Mon Sep 17 00:00:00 2001 From: Jiexi Luan Date: Tue, 7 Oct 2025 12:14:30 -0700 Subject: [PATCH 05/23] lint --- src/PollingBlockTracker.ts | 13 ++++++++----- 1 file changed, 8 insertions(+), 5 deletions(-) diff --git a/src/PollingBlockTracker.ts b/src/PollingBlockTracker.ts index 1e523eb..f45e2a8 100644 --- a/src/PollingBlockTracker.ts +++ b/src/PollingBlockTracker.ts @@ -156,11 +156,14 @@ export class PollingBlockTracker // We want to rate limit calls to this method if we made a direct fetch // for the block number because the BlockTracker was not running. We // achieve this by delaying the unsetting of the #pendingLatestBlock promise. - setTimeout(() => { - if (this.#pendingLatestBlock === pendingLatestBlockPromise) { - this.#pendingLatestBlock = undefined; - } - }, this._isRunning ? 0 : this._pollingInterval); + setTimeout( + () => { + if (this.#pendingLatestBlock === pendingLatestBlockPromise) { + this.#pendingLatestBlock = undefined; + } + }, + this._isRunning ? 0 : this._pollingInterval, + ); } } From 51b24f8ee0285789eb0e409c1ad5243efd8f1e1d Mon Sep 17 00:00:00 2001 From: Jiexi Luan Date: Tue, 7 Oct 2025 12:14:58 -0700 Subject: [PATCH 06/23] remove extra space --- src/PollingBlockTracker.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/PollingBlockTracker.ts b/src/PollingBlockTracker.ts index f45e2a8..5711edb 100644 --- a/src/PollingBlockTracker.ts +++ b/src/PollingBlockTracker.ts @@ -153,7 +153,7 @@ export class PollingBlockTracker reject(error); throw error; } finally { - // We want to rate limit calls to this method if we made a direct fetch + // We want to rate limit calls to this method if we made a direct fetch // for the block number because the BlockTracker was not running. We // achieve this by delaying the unsetting of the #pendingLatestBlock promise. setTimeout( From c1842b9494bd35c34949fa221a2bad4063db84ec Mon Sep 17 00:00:00 2001 From: Jiexi Luan Date: Tue, 7 Oct 2025 13:41:54 -0700 Subject: [PATCH 07/23] fix polling isRunning check in getLatestBlock --- src/PollingBlockTracker.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/PollingBlockTracker.ts b/src/PollingBlockTracker.ts index 5711edb..e6e386d 100644 --- a/src/PollingBlockTracker.ts +++ b/src/PollingBlockTracker.ts @@ -132,7 +132,7 @@ export class PollingBlockTracker try { // If tracker isn't running, just fetch directly - if (this._isRunning) { + if (!this._isRunning) { const latestBlock = await this._updateLatestBlock(); resolve(latestBlock); return latestBlock; From f59c0951b16d7d145a2d1e65163f9d2b959e88b7 Mon Sep 17 00:00:00 2001 From: Jiexi Luan Date: Tue, 7 Oct 2025 14:19:12 -0700 Subject: [PATCH 08/23] remove waitForPending --- src/PollingBlockTracker.ts | 10 +++------- 1 file changed, 3 insertions(+), 7 deletions(-) diff --git a/src/PollingBlockTracker.ts b/src/PollingBlockTracker.ts index e6e386d..810605a 100644 --- a/src/PollingBlockTracker.ts +++ b/src/PollingBlockTracker.ts @@ -113,22 +113,20 @@ export class PollingBlockTracker async getLatestBlock({ useCache = true, - waitForPending = true, }: { useCache?: boolean; waitForPending?: boolean } = {}): Promise { // return if available if (this._currentBlock && useCache) { return this._currentBlock; } - if (this.#pendingLatestBlock && waitForPending) { + if (this.#pendingLatestBlock) { return await this.#pendingLatestBlock.promise; } const { promise, resolve, reject } = createDeferredPromise({ suppressUnhandledRejection: true, }); - const pendingLatestBlockPromise = { reject, promise }; - this.#pendingLatestBlock = pendingLatestBlockPromise; + this.#pendingLatestBlock = { reject, promise }; try { // If tracker isn't running, just fetch directly @@ -158,9 +156,7 @@ export class PollingBlockTracker // achieve this by delaying the unsetting of the #pendingLatestBlock promise. setTimeout( () => { - if (this.#pendingLatestBlock === pendingLatestBlockPromise) { - this.#pendingLatestBlock = undefined; - } + this.#pendingLatestBlock = undefined; }, this._isRunning ? 0 : this._pollingInterval, ); From 20b6c1004fa67cc897476c2bff06786444050165 Mon Sep 17 00:00:00 2001 From: Jiexi Luan Date: Tue, 7 Oct 2025 14:19:18 -0700 Subject: [PATCH 09/23] restore checkForLatestBlock --- src/PollingBlockTracker.ts | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/src/PollingBlockTracker.ts b/src/PollingBlockTracker.ts index 810605a..60ebc6c 100644 --- a/src/PollingBlockTracker.ts +++ b/src/PollingBlockTracker.ts @@ -297,7 +297,8 @@ export class PollingBlockTracker } async checkForLatestBlock() { - return this.getLatestBlock({ useCache: false, waitForPending: false }); + await this._updateLatestBlock(); + return await this.getLatestBlock(); } private _start() { From 83e8a156e22a520098bcc329bf50c2a26eb684e7 Mon Sep 17 00:00:00 2001 From: Jiexi Luan Date: Tue, 7 Oct 2025 14:41:15 -0700 Subject: [PATCH 10/23] reorganize getLatestBlock fetch and poll try catch blocks --- src/PollingBlockTracker.ts | 58 ++++++++++++++++++++------------------ 1 file changed, 31 insertions(+), 27 deletions(-) diff --git a/src/PollingBlockTracker.ts b/src/PollingBlockTracker.ts index 60ebc6c..4c47439 100644 --- a/src/PollingBlockTracker.ts +++ b/src/PollingBlockTracker.ts @@ -128,38 +128,42 @@ export class PollingBlockTracker }); this.#pendingLatestBlock = { reject, promise }; - try { + if (this._isRunning) { + try { + // If tracker is running, wait for next block with timeout + const onLatestBlock = (value: string) => { + this.#removeInternalListener(onLatestBlock); + this.removeListener('latest', onLatestBlock); + resolve(value); + }; + + this.#addInternalListener(onLatestBlock); + this.once('latest', onLatestBlock); + + return await promise; + } catch (error) { + reject(error); + throw error; + } finally { + this.#pendingLatestBlock = undefined; + } + } else { // If tracker isn't running, just fetch directly - if (!this._isRunning) { + try { const latestBlock = await this._updateLatestBlock(); resolve(latestBlock); return latestBlock; - } - - // If tracker is running, wait for next block with timeout - const onLatestBlock = (value: string) => { - this.#removeInternalListener(onLatestBlock); - this.removeListener('latest', onLatestBlock); - resolve(value); - }; - - this.#addInternalListener(onLatestBlock); - this.once('latest', onLatestBlock); - - return await promise; - } catch (error) { - reject(error); - throw error; - } finally { - // We want to rate limit calls to this method if we made a direct fetch - // for the block number because the BlockTracker was not running. We - // achieve this by delaying the unsetting of the #pendingLatestBlock promise. - setTimeout( - () => { + } catch (error) { + reject(error); + throw error; + } finally { + // We want to rate limit calls to this method if we made a direct fetch + // for the block number because the BlockTracker was not running. We + // achieve this by delaying the unsetting of the #pendingLatestBlock promise. + setTimeout(() => { this.#pendingLatestBlock = undefined; - }, - this._isRunning ? 0 : this._pollingInterval, - ); + }, this._pollingInterval); + } } } From 9a516e8ccf1a7deca78600b22975adab82977749 Mon Sep 17 00:00:00 2001 From: Jiexi Luan Date: Tue, 7 Oct 2025 14:45:22 -0700 Subject: [PATCH 11/23] mark checkForLatestBlock deprecated --- src/PollingBlockTracker.ts | 3 +++ 1 file changed, 3 insertions(+) diff --git a/src/PollingBlockTracker.ts b/src/PollingBlockTracker.ts index 4c47439..903b786 100644 --- a/src/PollingBlockTracker.ts +++ b/src/PollingBlockTracker.ts @@ -300,6 +300,9 @@ export class PollingBlockTracker this._currentBlock = null; } + /** + * @deprecated getLatestBlock() should be used instead. + */ async checkForLatestBlock() { await this._updateLatestBlock(); return await this.getLatestBlock(); From ce63eb1537c6dde6d42123ed10ae6c193d7f35d9 Mon Sep 17 00:00:00 2001 From: jiexi Date: Tue, 7 Oct 2025 14:45:45 -0700 Subject: [PATCH 12/23] Update src/PollingBlockTracker.ts Co-authored-by: Mark Stacey --- src/PollingBlockTracker.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/PollingBlockTracker.ts b/src/PollingBlockTracker.ts index 903b786..89c4b2f 100644 --- a/src/PollingBlockTracker.ts +++ b/src/PollingBlockTracker.ts @@ -113,7 +113,7 @@ export class PollingBlockTracker async getLatestBlock({ useCache = true, - }: { useCache?: boolean; waitForPending?: boolean } = {}): Promise { + }: { useCache?: boolean } = {}): Promise { // return if available if (this._currentBlock && useCache) { return this._currentBlock; From e23bc9c410bee551398dfa19c6f99d7e338c1432 Mon Sep 17 00:00:00 2001 From: Jiexi Luan Date: Tue, 7 Oct 2025 14:49:36 -0700 Subject: [PATCH 13/23] jsdoc --- src/PollingBlockTracker.ts | 8 ++++++-- 1 file changed, 6 insertions(+), 2 deletions(-) diff --git a/src/PollingBlockTracker.ts b/src/PollingBlockTracker.ts index 903b786..a48c651 100644 --- a/src/PollingBlockTracker.ts +++ b/src/PollingBlockTracker.ts @@ -301,8 +301,12 @@ export class PollingBlockTracker } /** - * @deprecated getLatestBlock() should be used instead. - */ + * Checks for the latest block, updates the internal state, and returns the + * value immediately rather than waiting for the next polling interval. + * + * @deprecated Use {@link getLatestBlock} instead. + * @returns {Promise} A promise that resolves to the latest block number. + */ async checkForLatestBlock() { await this._updateLatestBlock(); return await this.getLatestBlock(); From 3d63b9d3f6963e95a17c55a84f06906c76cab758 Mon Sep 17 00:00:00 2001 From: Jiexi Luan Date: Tue, 7 Oct 2025 14:57:02 -0700 Subject: [PATCH 14/23] lint --- src/PollingBlockTracker.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/PollingBlockTracker.ts b/src/PollingBlockTracker.ts index 9428563..79514f2 100644 --- a/src/PollingBlockTracker.ts +++ b/src/PollingBlockTracker.ts @@ -305,7 +305,7 @@ export class PollingBlockTracker * value immediately rather than waiting for the next polling interval. * * @deprecated Use {@link getLatestBlock} instead. - * @returns {Promise} A promise that resolves to the latest block number. + * @returns A promise that resolves to the latest block number. */ async checkForLatestBlock() { await this._updateLatestBlock(); From 8e41b218e92355a8637ab938c7ec81c5e6add810 Mon Sep 17 00:00:00 2001 From: Jiexi Luan Date: Tue, 7 Oct 2025 17:29:12 -0700 Subject: [PATCH 15/23] Add specs --- src/PollingBlockTracker.test.ts | 163 ++++++++++++++++++++++++++++++++ 1 file changed, 163 insertions(+) diff --git a/src/PollingBlockTracker.test.ts b/src/PollingBlockTracker.test.ts index b940d66..dd1498a 100644 --- a/src/PollingBlockTracker.test.ts +++ b/src/PollingBlockTracker.test.ts @@ -984,6 +984,169 @@ describe('PollingBlockTracker', () => { ); }); }); + + describe('with useCache: false', () => { + 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 () => { + recordCallsToSetTimeout(); + + await withPollingBlockTracker( + { + provider: { + stubs: [ + { + methodName: 'eth_blockNumber', + result: '0x1', + }, + { + methodName: 'eth_blockNumber', + result: '0x2', + }, + ], + }, + }, + async ({ blockTracker }) => { + await blockTracker.getLatestBlock(); + const block = await blockTracker.getLatestBlock({ useCache: false }); + expect(block).toBe('0x1'); + expect(blockTracker.isRunning()).toBe(false); + } + ); + }); + + 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, + }); + + await withPollingBlockTracker( + { + provider: { + stubs: [ + { + methodName: 'eth_blockNumber', + result: '0x1', + }, + { + methodName: 'eth_blockNumber', + result: '0x2', + }, + ], + }, + }, + async ({ blockTracker }) => { + await blockTracker.getLatestBlock(); + const block = await blockTracker.getLatestBlock({ useCache: false }); + expect(block).toBe('0x2'); + expect(blockTracker.isRunning()).toBe(false); + } + ); + }); + }); + + describe('when the block tracker is already started', () => { + it('should wait for the next block event even if a block is already cached', async () => { + const timeoutCallbacks: (() => Promise)[] = []; + recordCallsToSetTimeout({ + numAutomaticCalls: 3, + interceptCallback: (callback) => { + return async () => { + timeoutCallbacks.push(callback); + }; + }, + }); + + await withPollingBlockTracker( + { + provider: { + stubs: [ + { + methodName: 'eth_blockNumber', + result: '0x1', + }, + { + methodName: 'eth_blockNumber', + result: '0x2', + }, + { + methodName: 'eth_blockNumber', + result: '0x3', + }, + ], + } + }, + + + async ({ blockTracker }) => { + blockTracker.on('latest', EMPTY_FUNCTION); + + await new Promise((resolve) => { + blockTracker.on('_waitingForNextIteration', resolve); + }); + const blockPromise1 = blockTracker.getLatestBlock({ useCache: false }); + await timeoutCallbacks[0](); + const block1 = await blockPromise1; + expect(block1).toBe('0x2'); + + const blockPromise2 = blockTracker.getLatestBlock({ useCache: false }); + await timeoutCallbacks[1](); + const block2 = await blockPromise2; + expect(block2).toBe('0x3'); + }, + ); + }); + + it('should handle concurrent calls', async () => { + const timeoutCallbacks: (() => Promise)[] = []; + recordCallsToSetTimeout({ + numAutomaticCalls: 3, + interceptCallback: (callback) => { + return async () => { + timeoutCallbacks.push(callback); + }; + }, + }); + + await withPollingBlockTracker( + { + provider: { + stubs: [ + { + methodName: 'eth_blockNumber', + result: '0x1', + }, + { + methodName: 'eth_blockNumber', + result: '0x2', + }, + { + methodName: 'eth_blockNumber', + result: '0x3', + }, + ], + }, + }, + async ({ blockTracker }) => { + blockTracker.on('latest', EMPTY_FUNCTION); + + await new Promise((resolve) => { + blockTracker.on('_waitingForNextIteration', resolve); + }); + + const blockPromise1 = blockTracker.getLatestBlock({ useCache: false }); + const blockPromise2 = blockTracker.getLatestBlock({ useCache: false }); + + await timeoutCallbacks[0](); + + const block1 = await blockPromise1; + const block2 = await blockPromise2; + expect(block1).toBe('0x2'); + expect(block2).toBe('0x2'); + }, + ); + }); + }); + }); }); describe('checkForLatestBlock', () => { From a71ca3d95614b427bf01a5c855c53d602ee11d34 Mon Sep 17 00:00:00 2001 From: Jiexi Luan Date: Tue, 7 Oct 2025 17:31:48 -0700 Subject: [PATCH 16/23] add specs --- src/PollingBlockTracker.test.ts | 31 +++++++++++++++++++++---------- 1 file changed, 21 insertions(+), 10 deletions(-) diff --git a/src/PollingBlockTracker.test.ts b/src/PollingBlockTracker.test.ts index dd1498a..adb4795 100644 --- a/src/PollingBlockTracker.test.ts +++ b/src/PollingBlockTracker.test.ts @@ -1007,10 +1007,12 @@ describe('PollingBlockTracker', () => { }, async ({ blockTracker }) => { await blockTracker.getLatestBlock(); - const block = await blockTracker.getLatestBlock({ useCache: false }); + const block = await blockTracker.getLatestBlock({ + useCache: false, + }); expect(block).toBe('0x1'); expect(blockTracker.isRunning()).toBe(false); - } + }, ); }); @@ -1036,10 +1038,12 @@ describe('PollingBlockTracker', () => { }, async ({ blockTracker }) => { await blockTracker.getLatestBlock(); - const block = await blockTracker.getLatestBlock({ useCache: false }); + const block = await blockTracker.getLatestBlock({ + useCache: false, + }); expect(block).toBe('0x2'); expect(blockTracker.isRunning()).toBe(false); - } + }, ); }); }); @@ -1073,22 +1077,25 @@ describe('PollingBlockTracker', () => { result: '0x3', }, ], - } + }, }, - async ({ blockTracker }) => { blockTracker.on('latest', EMPTY_FUNCTION); await new Promise((resolve) => { blockTracker.on('_waitingForNextIteration', resolve); }); - const blockPromise1 = blockTracker.getLatestBlock({ useCache: false }); + const blockPromise1 = blockTracker.getLatestBlock({ + useCache: false, + }); await timeoutCallbacks[0](); const block1 = await blockPromise1; expect(block1).toBe('0x2'); - const blockPromise2 = blockTracker.getLatestBlock({ useCache: false }); + const blockPromise2 = blockTracker.getLatestBlock({ + useCache: false, + }); await timeoutCallbacks[1](); const block2 = await blockPromise2; expect(block2).toBe('0x3'); @@ -1133,8 +1140,12 @@ describe('PollingBlockTracker', () => { blockTracker.on('_waitingForNextIteration', resolve); }); - const blockPromise1 = blockTracker.getLatestBlock({ useCache: false }); - const blockPromise2 = blockTracker.getLatestBlock({ useCache: false }); + const blockPromise1 = blockTracker.getLatestBlock({ + useCache: false, + }); + const blockPromise2 = blockTracker.getLatestBlock({ + useCache: false, + }); await timeoutCallbacks[0](); From 32a138e79732ec05c0e7002d294952472d2c49cb Mon Sep 17 00:00:00 2001 From: Jiexi Luan Date: Wed, 8 Oct 2025 08:51:42 -0700 Subject: [PATCH 17/23] cleanup numAutomaticCalls --- src/PollingBlockTracker.test.ts | 8 ++------ 1 file changed, 2 insertions(+), 6 deletions(-) diff --git a/src/PollingBlockTracker.test.ts b/src/PollingBlockTracker.test.ts index adb4795..0f47db4 100644 --- a/src/PollingBlockTracker.test.ts +++ b/src/PollingBlockTracker.test.ts @@ -1052,7 +1052,7 @@ describe('PollingBlockTracker', () => { it('should wait for the next block event even if a block is already cached', async () => { const timeoutCallbacks: (() => Promise)[] = []; recordCallsToSetTimeout({ - numAutomaticCalls: 3, + numAutomaticCalls: 2, interceptCallback: (callback) => { return async () => { timeoutCallbacks.push(callback); @@ -1106,7 +1106,7 @@ describe('PollingBlockTracker', () => { it('should handle concurrent calls', async () => { const timeoutCallbacks: (() => Promise)[] = []; recordCallsToSetTimeout({ - numAutomaticCalls: 3, + numAutomaticCalls: 1, interceptCallback: (callback) => { return async () => { timeoutCallbacks.push(callback); @@ -1126,10 +1126,6 @@ describe('PollingBlockTracker', () => { methodName: 'eth_blockNumber', result: '0x2', }, - { - methodName: 'eth_blockNumber', - result: '0x3', - }, ], }, }, From 2407ab3d19558011a08d2783fc984070ec39b665 Mon Sep 17 00:00:00 2001 From: Jiexi Luan Date: Wed, 8 Oct 2025 09:11:16 -0700 Subject: [PATCH 18/23] changelog --- CHANGELOG.md | 8 ++++++++ 1 file changed, 8 insertions(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index dab4a5e..31d5ef3 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -7,6 +7,14 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 ## [Unreleased] +### Changed + +- `PollingBlockTracker.getLatestBlock()` now accepts an optional param option `useCache` which ignores the cached block number value in state and instead updates and returns a new block number retrieved within the last `pollingInterval` period when false. Defaults: true ([#340](https://github.com/MetaMask/eth-block-tracker/pull/340)) + +### Fixed + +- Fix a bug in `PollingBlockTracker.getLatestBlock()` that made it possible for a stale block to be returned when the `PollingBlockTracker` isn't running. + ## [12.1.0] ### Changed From e49d5bceda81bece0cafd31bd1336aaf865fcbfb Mon Sep 17 00:00:00 2001 From: jiexi Date: Wed, 8 Oct 2025 10:15:09 -0700 Subject: [PATCH 19/23] Apply suggestion from @Gudahtt Co-authored-by: Mark Stacey --- CHANGELOG.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 31d5ef3..89b697a 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -13,7 +13,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 ### Fixed -- Fix a bug in `PollingBlockTracker.getLatestBlock()` that made it possible for a stale block to be returned when the `PollingBlockTracker` isn't running. +- Fix a bug in `PollingBlockTracker.getLatestBlock()` that made it possible for a stale block to be returned when the `PollingBlockTracker` isn't running. ([#340](https://github.com/MetaMask/eth-block-tracker/pull/340)) ## [12.1.0] From 70c87f52d8ce693aee71605cf00e7c95c4e3e42f Mon Sep 17 00:00:00 2001 From: Jiexi Luan Date: Wed, 8 Oct 2025 10:17:27 -0700 Subject: [PATCH 20/23] update changelog --- CHANGELOG.md | 4 ---- 1 file changed, 4 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 31d5ef3..e92b9bc 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -11,10 +11,6 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 - `PollingBlockTracker.getLatestBlock()` now accepts an optional param option `useCache` which ignores the cached block number value in state and instead updates and returns a new block number retrieved within the last `pollingInterval` period when false. Defaults: true ([#340](https://github.com/MetaMask/eth-block-tracker/pull/340)) -### Fixed - -- Fix a bug in `PollingBlockTracker.getLatestBlock()` that made it possible for a stale block to be returned when the `PollingBlockTracker` isn't running. - ## [12.1.0] ### Changed From 17ae0e03a87c2572c1552ca320948e26eb19ef86 Mon Sep 17 00:00:00 2001 From: Jiexi Luan Date: Wed, 8 Oct 2025 10:48:49 -0700 Subject: [PATCH 21/23] cleanup `when the block tracker is already started` spec --- src/PollingBlockTracker.test.ts | 29 ++++++----------------------- 1 file changed, 6 insertions(+), 23 deletions(-) diff --git a/src/PollingBlockTracker.test.ts b/src/PollingBlockTracker.test.ts index 0f47db4..aed58aa 100644 --- a/src/PollingBlockTracker.test.ts +++ b/src/PollingBlockTracker.test.ts @@ -1050,15 +1050,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 () => { - const timeoutCallbacks: (() => Promise)[] = []; - recordCallsToSetTimeout({ - numAutomaticCalls: 2, - interceptCallback: (callback) => { - return async () => { - timeoutCallbacks.push(callback); - }; - }, - }); + const setTimeoutRecorder = recordCallsToSetTimeout(); await withPollingBlockTracker( { @@ -1082,21 +1074,21 @@ describe('PollingBlockTracker', () => { async ({ blockTracker }) => { blockTracker.on('latest', EMPTY_FUNCTION); - await new Promise((resolve) => { blockTracker.on('_waitingForNextIteration', resolve); }); + const blockPromise1 = blockTracker.getLatestBlock({ useCache: false, }); - await timeoutCallbacks[0](); + await setTimeoutRecorder.next(); const block1 = await blockPromise1; expect(block1).toBe('0x2'); const blockPromise2 = blockTracker.getLatestBlock({ useCache: false, }); - await timeoutCallbacks[1](); + await setTimeoutRecorder.next(); const block2 = await blockPromise2; expect(block2).toBe('0x3'); }, @@ -1104,15 +1096,7 @@ describe('PollingBlockTracker', () => { }); it('should handle concurrent calls', async () => { - const timeoutCallbacks: (() => Promise)[] = []; - recordCallsToSetTimeout({ - numAutomaticCalls: 1, - interceptCallback: (callback) => { - return async () => { - timeoutCallbacks.push(callback); - }; - }, - }); + const setTimeoutRecorder = recordCallsToSetTimeout(); await withPollingBlockTracker( { @@ -1131,7 +1115,6 @@ describe('PollingBlockTracker', () => { }, async ({ blockTracker }) => { blockTracker.on('latest', EMPTY_FUNCTION); - await new Promise((resolve) => { blockTracker.on('_waitingForNextIteration', resolve); }); @@ -1143,7 +1126,7 @@ describe('PollingBlockTracker', () => { useCache: false, }); - await timeoutCallbacks[0](); + await setTimeoutRecorder.next(); const block1 = await blockPromise1; const block2 = await blockPromise2; From f2338524f28ccdd9cf24f6992a82fdd82b955baa Mon Sep 17 00:00:00 2001 From: Jiexi Luan Date: Wed, 8 Oct 2025 11:04:12 -0700 Subject: [PATCH 22/23] add explicit _waitingForNextIteration event checks --- src/PollingBlockTracker.test.ts | 16 ++++++++++++++-- 1 file changed, 14 insertions(+), 2 deletions(-) diff --git a/src/PollingBlockTracker.test.ts b/src/PollingBlockTracker.test.ts index aed58aa..385e5f6 100644 --- a/src/PollingBlockTracker.test.ts +++ b/src/PollingBlockTracker.test.ts @@ -1075,20 +1075,28 @@ describe('PollingBlockTracker', () => { async ({ blockTracker }) => { blockTracker.on('latest', EMPTY_FUNCTION); await new Promise((resolve) => { - blockTracker.on('_waitingForNextIteration', resolve); + blockTracker.once('_waitingForNextIteration', resolve); }); const blockPromise1 = blockTracker.getLatestBlock({ useCache: false, }); + const pollingLoopPromise1 = new Promise((resolve) => { + blockTracker.once('_waitingForNextIteration', resolve); + }); await setTimeoutRecorder.next(); + await pollingLoopPromise1; const block1 = await blockPromise1; expect(block1).toBe('0x2'); + const pollingLoopPromise2 = new Promise((resolve) => { + blockTracker.once('_waitingForNextIteration', resolve); + }); const blockPromise2 = blockTracker.getLatestBlock({ useCache: false, }); await setTimeoutRecorder.next(); + await pollingLoopPromise2; const block2 = await blockPromise2; expect(block2).toBe('0x3'); }, @@ -1116,7 +1124,7 @@ describe('PollingBlockTracker', () => { async ({ blockTracker }) => { blockTracker.on('latest', EMPTY_FUNCTION); await new Promise((resolve) => { - blockTracker.on('_waitingForNextIteration', resolve); + blockTracker.once('_waitingForNextIteration', resolve); }); const blockPromise1 = blockTracker.getLatestBlock({ @@ -1126,7 +1134,11 @@ describe('PollingBlockTracker', () => { useCache: false, }); + const pollingLoopPromise = new Promise((resolve) => { + blockTracker.once('_waitingForNextIteration', resolve); + }); await setTimeoutRecorder.next(); + await pollingLoopPromise; const block1 = await blockPromise1; const block2 = await blockPromise2; From aa0f91076b8d13b3204cf50d93aeefbae02dccf5 Mon Sep 17 00:00:00 2001 From: Jiexi Luan Date: Wed, 8 Oct 2025 11:17:12 -0700 Subject: [PATCH 23/23] update changelog using Elliot's comment --- CHANGELOG.md | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index e92b9bc..f06d25f 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -9,7 +9,8 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 ### Changed -- `PollingBlockTracker.getLatestBlock()` now accepts an optional param option `useCache` which ignores the cached block number value in state and instead updates and returns a new block number retrieved within the last `pollingInterval` period when false. Defaults: true ([#340](https://github.com/MetaMask/eth-block-tracker/pull/340)) +- `PollingBlockTracker.getLatestBlock()` now accepts an optional parameter `useCache` ([#340](https://github.com/MetaMask/eth-block-tracker/pull/340)) + - This option defaults to `true`, but when `false`, it ignores the cached block number and instead updates and returns a new block number, ensuring that the frequency of requests is limited to the `pollingInterval` period ## [12.1.0]