Skip to content
This repository was archived by the owner on Oct 16, 2025. It is now read-only.
4 changes: 4 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,10 @@ 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
Expand Down
101 changes: 80 additions & 21 deletions src/PollingBlockTracker.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -174,7 +174,7 @@

await withPollingBlockTracker(async ({ blockTracker }) => {
expect(blockTracker.isRunning()).toBe(false);
blockTracker.getLatestBlock();

Check warning on line 177 in src/PollingBlockTracker.test.ts

View workflow job for this annotation

GitHub Actions / Build, lint, and test / Build, Lint, and Test (18.x)

Promises must be awaited, end with a call to .catch, end with a call to .then with a rejection handler or be explicitly marked as ignored with the `void` operator

Check warning on line 177 in src/PollingBlockTracker.test.ts

View workflow job for this annotation

GitHub Actions / Build, lint, and test / Build, Lint, and Test (22.x)

Promises must be awaited, end with a call to .catch, end with a call to .then with a rejection handler or be explicitly marked as ignored with the `void` operator

Check warning on line 177 in src/PollingBlockTracker.test.ts

View workflow job for this annotation

GitHub Actions / Build, lint, and test / Build, Lint, and Test (20.x)

Promises must be awaited, end with a call to .catch, end with a call to .then with a rejection handler or be explicitly marked as ignored with the `void` operator
expect(blockTracker.isRunning()).toBe(false);
});
});
Expand All @@ -188,6 +188,35 @@
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 }) => {
const block = await blockTracker.getLatestBlock();
expect(block).toBe('0x0');
await setTimeoutRecorder.nextMatchingDuration(
blockResetDuration,
);
expect(blockTracker.getCurrentBlock()).toBeNull();
},
);
});
});

describe('if an error occurs while fetching the latest block number', () => {
Expand Down Expand Up @@ -472,7 +501,7 @@
);
});

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,
Expand All @@ -496,16 +525,13 @@
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);
},
);
});
Expand Down Expand Up @@ -985,9 +1011,9 @@
});
});

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(
Expand Down Expand Up @@ -1016,10 +1042,12 @@
);
});

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,
});
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,
blockResetDuration: 200,
};

await withPollingBlockTracker(
{
Expand All @@ -1035,9 +1063,13 @@
},
],
},
blockTracker: blockTrackerOptions,
},
async ({ blockTracker }) => {
await blockTracker.getLatestBlock();
await setTimeoutRecorder.nextMatchingDuration(
blockTrackerOptions.pollingInterval,
);
const block = await blockTracker.getLatestBlock({
useCache: false,
});
Expand All @@ -1049,7 +1081,7 @@
});

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(
Expand Down Expand Up @@ -1306,7 +1338,7 @@
);
});

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 if the block tracker is not running', async () => {
const setTimeoutRecorder = recordCallsToSetTimeout();
const blockResetDuration = 1000;

Expand All @@ -1326,12 +1358,39 @@
},
async ({ blockTracker }) => {
await blockTracker.checkForLatestBlock();
expect(blockTracker.getCurrentBlock()).toBe('0x0');
await setTimeoutRecorder.nextMatchingDuration(blockResetDuration);
expect(blockTracker.getCurrentBlock()).toBeNull();
},
);
});

await new Promise((resolve) =>
originalSetTimeout(resolve, blockResetDuration),
);
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;

expect(setTimeoutRecorder.calls).toHaveLength(0);
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');
},
);
Expand Down
6 changes: 6 additions & 0 deletions src/PollingBlockTracker.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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!;
Expand Down
Loading