From 18b4fcd3854af141c75fb277d35a743a31a77f2a Mon Sep 17 00:00:00 2001 From: Alex Date: Wed, 6 Dec 2023 12:20:28 -0600 Subject: [PATCH 1/9] PollingController: add option to poll by blockTracker --- .../src/PollingController.ts | 72 +++++++++++++++++-- 1 file changed, 67 insertions(+), 5 deletions(-) diff --git a/packages/polling-controller/src/PollingController.ts b/packages/polling-controller/src/PollingController.ts index 0327a7806e..5233c333a5 100644 --- a/packages/polling-controller/src/PollingController.ts +++ b/packages/polling-controller/src/PollingController.ts @@ -1,6 +1,8 @@ -import { BaseController, BaseControllerV1 } from '@metamask/base-controller'; -import type { NetworkClientId } from '@metamask/network-controller'; -import type { Json } from '@metamask/utils'; +import { BaseController, BaseControllerV2 } from '@metamask/base-controller'; +import type { + NetworkClientId, + NetworkClient, +} from '@metamask/network-controller'; import stringify from 'fast-json-stable-stringify'; import { v4 as random } from 'uuid'; @@ -38,6 +40,11 @@ function PollingControllerMixin(Base: TBase) { readonly #intervalIds: Record = {}; + readonly #activeListeners: Record< + PollingTokenSetId, + (options: Json) => Promise + > = {}; + #callbacks: Map< NetworkClientId, Set<(networkClientId: NetworkClientId) => void> @@ -45,6 +52,10 @@ function PollingControllerMixin(Base: TBase) { #intervalLength = 1000; + #getNetworkClientById: + | ((networkClientId: NetworkClientId) => NetworkClient) + | undefined; + getIntervalLength() { return this.#intervalLength; } @@ -58,6 +69,17 @@ function PollingControllerMixin(Base: TBase) { this.#intervalLength = length; } + setPollOnNewBlocks( + getNetworkClientById: (networkClientId: NetworkClientId) => NetworkClient, + ) { + if (!getNetworkClientById) { + throw new Error('getNetworkClientById callback required'); + } + + this.#intervalLength = 0; + this.#getNetworkClientById = getNetworkClientById; + } + /** * Starts polling for a networkClientId * @@ -113,6 +135,21 @@ function PollingControllerMixin(Base: TBase) { if (tokenSet.size === 0) { clearTimeout(this.#intervalIds[key]); delete this.#intervalIds[key]; + + // if applicable stop listening for new blocks + if (this.#getNetworkClientById !== undefined) { + const [networkClientId] = key.split(':'); + const { blockTracker } = + this.#getNetworkClientById(networkClientId); + if (blockTracker) { + blockTracker.removeListener( + 'latest', + this.#activeListeners[key], + ); + } + delete this.#activeListeners[key]; + } + this.#pollingTokenSets.delete(key); this.#callbacks.get(key)?.forEach((callback) => { callback(key); @@ -139,6 +176,31 @@ function PollingControllerMixin(Base: TBase) { #poll(networkClientId: NetworkClientId, options: Json) { const key = getKey(networkClientId, options); + + // if #getNetworkClientById is defined, we want to poll on new blocks + if (this.#getNetworkClientById !== undefined) { + // if we're already listening for new blocks for this key, don't add another listener + if (this.#activeListeners[key]) { + return; + } + const blockTracker = + this.#getNetworkClientById(networkClientId)?.blockTracker; + + if (blockTracker) { + const updateOnNewBlock = this._executePoll.bind( + networkClientId, + options, + ); + blockTracker.addListener('latest', updateOnNewBlock); + this.#activeListeners[key] = updateOnNewBlock; + return; + } + + throw new Error(` + Unable to retreive blockTracker for networkClientId ${networkClientId} `); + } + + // if we're not polling on new blocks, use setTimeout const interval = this.#intervalIds[key]; if (interval) { clearTimeout(interval); @@ -190,5 +252,5 @@ function PollingControllerMixin(Base: TBase) { class Empty {} export const PollingControllerOnly = PollingControllerMixin(Empty); -export const PollingController = PollingControllerMixin(BaseController); -export const PollingControllerV1 = PollingControllerMixin(BaseControllerV1); +export const PollingController = PollingControllerMixin(BaseControllerV2); +export const PollingControllerV1 = PollingControllerMixin(BaseController); From c95768f82cdce5e91a58ad26aa92d7826f433f82 Mon Sep 17 00:00:00 2001 From: Alex Date: Wed, 6 Dec 2023 12:25:00 -0600 Subject: [PATCH 2/9] fix --- packages/polling-controller/src/PollingController.ts | 8 +++++--- 1 file changed, 5 insertions(+), 3 deletions(-) diff --git a/packages/polling-controller/src/PollingController.ts b/packages/polling-controller/src/PollingController.ts index 5233c333a5..5468d6f11f 100644 --- a/packages/polling-controller/src/PollingController.ts +++ b/packages/polling-controller/src/PollingController.ts @@ -1,8 +1,9 @@ -import { BaseController, BaseControllerV2 } from '@metamask/base-controller'; +import { BaseController, BaseControllerV1 } from '@metamask/base-controller'; import type { NetworkClientId, NetworkClient, } from '@metamask/network-controller'; +import type { Json } from '@metamask/utils'; import stringify from 'fast-json-stable-stringify'; import { v4 as random } from 'uuid'; @@ -188,6 +189,7 @@ function PollingControllerMixin(Base: TBase) { if (blockTracker) { const updateOnNewBlock = this._executePoll.bind( + this, networkClientId, options, ); @@ -252,5 +254,5 @@ function PollingControllerMixin(Base: TBase) { class Empty {} export const PollingControllerOnly = PollingControllerMixin(Empty); -export const PollingController = PollingControllerMixin(BaseControllerV2); -export const PollingControllerV1 = PollingControllerMixin(BaseController); +export const PollingController = PollingControllerMixin(BaseController); +export const PollingControllerV1 = PollingControllerMixin(BaseControllerV1); From 1be1f9ab8c0dfd9cdec41dc8372660b2bd31f731 Mon Sep 17 00:00:00 2001 From: Alex Date: Wed, 6 Dec 2023 12:53:36 -0600 Subject: [PATCH 3/9] adding tests --- .../src/PollingController.test.ts | 323 +++++++++++++++--- 1 file changed, 280 insertions(+), 43 deletions(-) diff --git a/packages/polling-controller/src/PollingController.test.ts b/packages/polling-controller/src/PollingController.test.ts index b6a9344de5..11fc29e11a 100644 --- a/packages/polling-controller/src/PollingController.test.ts +++ b/packages/polling-controller/src/PollingController.test.ts @@ -1,4 +1,5 @@ import { ControllerMessenger } from '@metamask/base-controller'; +import EventEmitter from 'events'; import { useFakeTimers } from 'sinon'; import { advanceTime } from '../../../tests/helpers'; @@ -13,6 +14,10 @@ const createExecutePollMock = () => { return executePollMock; }; +class MyGasFeeController extends PollingController { + _executePoll = createExecutePollMock(); +} + describe('PollingController', () => { let clock: sinon.SinonFakeTimers; beforeEach(() => { @@ -23,9 +28,6 @@ describe('PollingController', () => { }); describe('start', () => { it('should start polling if not polling', async () => { - class MyGasFeeController extends PollingController { - _executePoll = createExecutePollMock(); - } const mockMessenger = new ControllerMessenger(); const controller = new MyGasFeeController({ @@ -44,9 +46,6 @@ describe('PollingController', () => { }); describe('stop', () => { it('should stop polling when called with a valid polling that was the only active pollingToken for a given networkClient', async () => { - class MyGasFeeController extends PollingController { - _executePoll = createExecutePollMock(); - } const mockMessenger = new ControllerMessenger(); const controller = new MyGasFeeController({ @@ -65,9 +64,6 @@ describe('PollingController', () => { controller.stopAllPolling(); }); it('should not stop polling if called with one of multiple active polling tokens for a given networkClient', async () => { - class MyGasFeeController extends PollingController { - _executePoll = createExecutePollMock(); - } const mockMessenger = new ControllerMessenger(); const controller = new MyGasFeeController({ @@ -88,9 +84,6 @@ describe('PollingController', () => { controller.stopAllPolling(); }); it('should error if no pollingToken is passed', () => { - class MyGasFeeController extends PollingController { - _executePoll = createExecutePollMock(); - } const mockMessenger = new ControllerMessenger(); const controller = new MyGasFeeController({ @@ -106,9 +99,6 @@ describe('PollingController', () => { controller.stopAllPolling(); }); it('should error if no matching pollingToken is found', () => { - class MyGasFeeController extends PollingController { - _executePoll = createExecutePollMock(); - } const mockMessenger = new ControllerMessenger(); const controller = new MyGasFeeController({ @@ -126,9 +116,6 @@ describe('PollingController', () => { }); describe('startPollingByNetworkClientId', () => { it('should call _executePoll immediately and on interval if polling', async () => { - class MyGasFeeController extends PollingController { - _executePoll = createExecutePollMock(); - } const mockMessenger = new ControllerMessenger(); const controller = new MyGasFeeController({ @@ -144,9 +131,6 @@ describe('PollingController', () => { expect(controller._executePoll).toHaveBeenCalledTimes(3); }); it('should call _executePoll immediately once and continue calling _executePoll on interval when start is called again with the same networkClientId', async () => { - class MyGasFeeController extends PollingController { - _executePoll = createExecutePollMock(); - } const mockMessenger = new ControllerMessenger(); const controller = new MyGasFeeController({ @@ -169,9 +153,7 @@ describe('PollingController', () => { }); it('should publish "pollingComplete" when stop is called', async () => { const pollingComplete: any = jest.fn(); - class MyGasFeeController extends PollingController { - _executePoll = createExecutePollMock(); - } + const name = 'PollingController'; const mockMessenger = new ControllerMessenger(); @@ -188,9 +170,6 @@ describe('PollingController', () => { expect(pollingComplete).toHaveBeenCalledTimes(1); }); it('should poll at the interval length when set via setIntervalLength', async () => { - class MyGasFeeController extends PollingController { - _executePoll = createExecutePollMock(); - } const mockMessenger = new ControllerMessenger(); const controller = new MyGasFeeController({ @@ -211,9 +190,6 @@ describe('PollingController', () => { expect(controller._executePoll).toHaveBeenCalledTimes(2); }); it('should start and stop polling sessions for different networkClientIds with the same options', async () => { - class MyGasFeeController extends PollingController { - _executePoll = createExecutePollMock(); - } const mockMessenger = new ControllerMessenger(); const controller = new MyGasFeeController({ @@ -263,9 +239,6 @@ describe('PollingController', () => { }); describe('multiple networkClientIds', () => { it('should poll for each networkClientId', async () => { - class MyGasFeeController extends PollingController { - _executePoll = createExecutePollMock(); - } const mockMessenger = new ControllerMessenger(); const controller = new MyGasFeeController({ @@ -277,38 +250,35 @@ describe('PollingController', () => { controller.startPollingByNetworkClientId('mainnet'); await advanceTime({ clock, duration: 0 }); - controller.startPollingByNetworkClientId('rinkeby'); + controller.startPollingByNetworkClientId('goerli'); await advanceTime({ clock, duration: 0 }); expect(controller._executePoll.mock.calls).toMatchObject([ ['mainnet', {}], - ['rinkeby', {}], + ['goerli', {}], ]); await advanceTime({ clock, duration: TICK_TIME }); expect(controller._executePoll.mock.calls).toMatchObject([ ['mainnet', {}], - ['rinkeby', {}], + ['goerli', {}], ['mainnet', {}], - ['rinkeby', {}], + ['goerli', {}], ]); await advanceTime({ clock, duration: TICK_TIME }); expect(controller._executePoll.mock.calls).toMatchObject([ ['mainnet', {}], - ['rinkeby', {}], + ['goerli', {}], ['mainnet', {}], - ['rinkeby', {}], + ['goerli', {}], ['mainnet', {}], - ['rinkeby', {}], + ['goerli', {}], ]); controller.stopAllPolling(); }); it('should poll multiple networkClientIds when setting interval length', async () => { - class MyGasFeeController extends PollingController { - _executePoll = createExecutePollMock(); - } const mockMessenger = new ControllerMessenger(); const controller = new MyGasFeeController({ @@ -383,4 +353,271 @@ describe('PollingController', () => { expect(c.stopPollingByPollingToken).toBeDefined(); }); }); + describe('startPollingByNetworkClientId after setPollOnNewBlocks', () => { + class TestBlockTracker extends EventEmitter { + private latestBlockNumber: number; + + public interval: number; + + constructor({ interval } = { interval: 1000 }) { + super(); + this.latestBlockNumber = 0; + this.interval = interval; + this.start(interval); + } + + private start(interval: number) { + setInterval(() => { + this.latestBlockNumber += 1; + this.emit('latest', this.latestBlockNumber); + }, interval); + } + } + + let getNetworkClientById: jest.Mock; + let mainnetBlockTracker: TestBlockTracker; + let goerliBlockTracker: TestBlockTracker; + let sepoliaBlockTracker: TestBlockTracker; + beforeEach(() => { + mainnetBlockTracker = new TestBlockTracker({ interval: 5 }); + goerliBlockTracker = new TestBlockTracker({ interval: 10 }); + sepoliaBlockTracker = new TestBlockTracker({ interval: 15 }); + + getNetworkClientById = jest.fn().mockImplementation((networkClientId) => { + switch (networkClientId) { + case 'mainnet': + return { + blockTracker: mainnetBlockTracker, + }; + case 'goerli': + return { + blockTracker: goerliBlockTracker, + }; + case 'sepolia': + return { + blockTracker: sepoliaBlockTracker, + }; + default: + throw new Error(`Unknown networkClientId: ${networkClientId}`); + } + }); + }); + + it('should start polling for the specified networkClientId', async () => { + const mockMessenger = new ControllerMessenger(); + + const controller = new MyGasFeeController({ + messenger: mockMessenger, + metadata: {}, + name: 'PollingController', + state: { foo: 'bar' }, + }); + + // const getNetworkClientById = jest.fn().mockReturnValue({ + // blockTracker: new TestBlockTracker({ interval: 5 }), + // }); + + controller.setPollOnNewBlocks(getNetworkClientById); + + controller.startPollingByNetworkClientId('mainnet'); + + expect(getNetworkClientById).toHaveBeenCalledWith('mainnet'); + + await advanceTime({ clock, duration: 5 }); + + expect(controller._executePoll).toHaveBeenCalledTimes(1); + + await advanceTime({ clock, duration: 1 }); + + expect(controller._executePoll).toHaveBeenCalledTimes(1); + + await advanceTime({ clock, duration: 4 }); + + expect(controller._executePoll.mock.calls).toMatchObject([ + expect.arrayContaining(['mainnet', {}]), + expect.arrayContaining(['mainnet', {}]), + ]); + + // Stop all polling + controller.stopAllPolling(); + }); + + it('should poll on new block intervals for each networkClientId', async () => { + const mockMessenger = new ControllerMessenger(); + + const controller = new MyGasFeeController({ + messenger: mockMessenger, + metadata: {}, + name: 'PollingController', + state: { foo: 'bar' }, + }); + + controller.setPollOnNewBlocks(getNetworkClientById); + + controller.startPollingByNetworkClientId('mainnet'); + controller.startPollingByNetworkClientId('goerli'); + await advanceTime({ clock, duration: 5 }); + + expect(controller._executePoll).toHaveBeenCalledTimes(1); + expect(controller._executePoll).toHaveBeenCalledWith('mainnet', {}, 1); + + // Start polling for goerli, 10ms interval + await advanceTime({ clock, duration: 5 }); + + expect(controller._executePoll.mock.calls).toMatchObject([ + ['mainnet', {}, 1], + ['mainnet', {}, 2], + ['goerli', {}, 1], + ]); + + await advanceTime({ clock, duration: 5 }); + + expect(controller._executePoll.mock.calls).toMatchObject([ + ['mainnet', {}, 1], + ['mainnet', {}, 2], + ['goerli', {}, 1], + ['mainnet', {}, 3], + ]); + + // 15ms have passed + // Start polling for sepolia, 15ms interval + controller.startPollingByNetworkClientId('sepolia'); + + await advanceTime({ clock, duration: 15 }); + + // at 30ms, 6 blocks have passed for mainnet (every 5ms), 3 for goerli (every 10ms), and 2 for sepolia (every 15ms) + // Didn't start listening to sepolia until 15ms had passed, so we only call executePoll on the 2nd block + expect(controller._executePoll.mock.calls).toMatchObject([ + ['mainnet', {}, 1], + ['mainnet', {}, 2], + ['goerli', {}, 1], + ['mainnet', {}, 3], + ['mainnet', {}, 4], + ['goerli', {}, 2], + ['mainnet', {}, 5], + ['mainnet', {}, 6], + ['goerli', {}, 3], + ['sepolia', {}, 2], + ]); + + // Stop all polling + controller.stopAllPolling(); + }); + + it('should should stop polling when all polling tokens for a networkClientId are deleted', async () => { + const mockMessenger = new ControllerMessenger(); + + const controller = new MyGasFeeController({ + messenger: mockMessenger, + metadata: {}, + name: 'PollingController', + state: { foo: 'bar' }, + }); + + controller.setPollOnNewBlocks(getNetworkClientById); + + const pollingToken1 = controller.startPollingByNetworkClientId('mainnet'); + + await advanceTime({ clock, duration: 5 }); + + expect(controller._executePoll).toHaveBeenCalledTimes(1); + expect(controller._executePoll).toHaveBeenCalledWith('mainnet', {}, 1); + + const pollingToken2 = controller.startPollingByNetworkClientId('mainnet'); + await advanceTime({ clock, duration: 5 }); + + expect(controller._executePoll.mock.calls).toMatchObject([ + ['mainnet', {}, 1], + ['mainnet', {}, 2], + ]); + + controller.stopPollingByPollingToken(pollingToken1); + + await advanceTime({ clock, duration: 5 }); + + expect(controller._executePoll.mock.calls).toMatchObject([ + ['mainnet', {}, 1], + ['mainnet', {}, 2], + ['mainnet', {}, 3], + ]); + + controller.stopPollingByPollingToken(pollingToken2); + + await advanceTime({ clock, duration: 15 }); + + // no further polling should occur + expect(controller._executePoll.mock.calls).toMatchObject([ + ['mainnet', {}, 1], + ['mainnet', {}, 2], + ['mainnet', {}, 3], + ]); + }); + + it('should should stop polling when all polling tokens for a networkClientId are deleted, even if other networkClientIds are still polling', async () => { + const mockMessenger = new ControllerMessenger(); + + const controller = new MyGasFeeController({ + messenger: mockMessenger, + metadata: {}, + name: 'PollingController', + state: { foo: 'bar' }, + }); + + controller.setPollOnNewBlocks(getNetworkClientById); + + const pollingToken1 = controller.startPollingByNetworkClientId('mainnet'); + + await advanceTime({ clock, duration: 5 }); + + expect(controller._executePoll).toHaveBeenCalledWith('mainnet', {}, 1); + + const pollingToken2 = controller.startPollingByNetworkClientId('mainnet'); + + await advanceTime({ clock, duration: 5 }); + + expect(controller._executePoll.mock.calls).toMatchObject([ + ['mainnet', {}, 1], + ['mainnet', {}, 2], + ]); + + controller.startPollingByNetworkClientId('goerli'); + await advanceTime({ clock, duration: 5 }); + + // 3 blocks have passed for mainnet, 1 for goerli but we only started listening to goerli after 5ms + // so the next block will come at 20ms and be the 2nd block for goerli + expect(controller._executePoll.mock.calls).toMatchObject([ + ['mainnet', {}, 1], + ['mainnet', {}, 2], + ['mainnet', {}, 3], + ]); + + controller.stopPollingByPollingToken(pollingToken1); + + await advanceTime({ clock, duration: 5 }); + + // 20ms have passed, 4 blocks for mainnet, 2 for goerli + expect(controller._executePoll.mock.calls).toMatchObject([ + ['mainnet', {}, 1], + ['mainnet', {}, 2], + ['mainnet', {}, 3], + ['mainnet', {}, 4], + ['goerli', {}, 2], + ]); + + controller.stopPollingByPollingToken(pollingToken2); + + await advanceTime({ clock, duration: 20 }); + + // no further polling for mainnet should occur + expect(controller._executePoll.mock.calls).toMatchObject([ + ['mainnet', {}, 1], + ['mainnet', {}, 2], + ['mainnet', {}, 3], + ['mainnet', {}, 4], + ['goerli', {}, 2], + ['goerli', {}, 3], + ['goerli', {}, 4], + ]); + }); + }); }); From 5ac05deed9bd73eb4c543ecebaafa5606f873cda Mon Sep 17 00:00:00 2001 From: Alex Date: Wed, 6 Dec 2023 16:58:37 -0600 Subject: [PATCH 4/9] another test + cleanup --- .../src/PollingController.test.ts | 154 ++---------------- 1 file changed, 13 insertions(+), 141 deletions(-) diff --git a/packages/polling-controller/src/PollingController.test.ts b/packages/polling-controller/src/PollingController.test.ts index 11fc29e11a..4f71f8ddf5 100644 --- a/packages/polling-controller/src/PollingController.test.ts +++ b/packages/polling-controller/src/PollingController.test.ts @@ -20,7 +20,16 @@ class MyGasFeeController extends PollingController { describe('PollingController', () => { let clock: sinon.SinonFakeTimers; + let mockMessenger: any; + let controller: any; beforeEach(() => { + mockMessenger = new ControllerMessenger(); + controller = new MyGasFeeController({ + messenger: mockMessenger, + metadata: {}, + name: 'PollingController', + state: { foo: 'bar' }, + }); clock = useFakeTimers(); }); afterEach(() => { @@ -28,14 +37,6 @@ describe('PollingController', () => { }); describe('start', () => { it('should start polling if not polling', async () => { - const mockMessenger = new ControllerMessenger(); - - const controller = new MyGasFeeController({ - messenger: mockMessenger, - metadata: {}, - name: 'PollingController', - state: { foo: 'bar' }, - }); controller.startPollingByNetworkClientId('mainnet'); await advanceTime({ clock, duration: 0 }); expect(controller._executePoll).toHaveBeenCalledTimes(1); @@ -46,14 +47,6 @@ describe('PollingController', () => { }); describe('stop', () => { it('should stop polling when called with a valid polling that was the only active pollingToken for a given networkClient', async () => { - const mockMessenger = new ControllerMessenger(); - - const controller = new MyGasFeeController({ - messenger: mockMessenger, - metadata: {}, - name: 'PollingController', - state: { foo: 'bar' }, - }); const pollingToken = controller.startPollingByNetworkClientId('mainnet'); await advanceTime({ clock, duration: 0 }); expect(controller._executePoll).toHaveBeenCalledTimes(1); @@ -64,14 +57,6 @@ describe('PollingController', () => { controller.stopAllPolling(); }); it('should not stop polling if called with one of multiple active polling tokens for a given networkClient', async () => { - const mockMessenger = new ControllerMessenger(); - - const controller = new MyGasFeeController({ - messenger: mockMessenger, - metadata: {}, - name: 'PollingController', - state: { foo: 'bar' }, - }); const pollingToken1 = controller.startPollingByNetworkClientId('mainnet'); await advanceTime({ clock, duration: 0 }); @@ -84,14 +69,6 @@ describe('PollingController', () => { controller.stopAllPolling(); }); it('should error if no pollingToken is passed', () => { - const mockMessenger = new ControllerMessenger(); - - const controller = new MyGasFeeController({ - messenger: mockMessenger, - metadata: {}, - name: 'PollingController', - state: { foo: 'bar' }, - }); controller.startPollingByNetworkClientId('mainnet'); expect(() => { controller.stopPollingByPollingToken(undefined as unknown as any); @@ -99,14 +76,6 @@ describe('PollingController', () => { controller.stopAllPolling(); }); it('should error if no matching pollingToken is found', () => { - const mockMessenger = new ControllerMessenger(); - - const controller = new MyGasFeeController({ - messenger: mockMessenger, - metadata: {}, - name: 'PollingController', - state: { foo: 'bar' }, - }); controller.startPollingByNetworkClientId('mainnet'); expect(() => { controller.stopPollingByPollingToken('potato'); @@ -116,14 +85,6 @@ describe('PollingController', () => { }); describe('startPollingByNetworkClientId', () => { it('should call _executePoll immediately and on interval if polling', async () => { - const mockMessenger = new ControllerMessenger(); - - const controller = new MyGasFeeController({ - messenger: mockMessenger, - metadata: {}, - name: 'PollingController', - state: { foo: 'bar' }, - }); controller.startPollingByNetworkClientId('mainnet'); await advanceTime({ clock, duration: 0 }); expect(controller._executePoll).toHaveBeenCalledTimes(1); @@ -131,14 +92,6 @@ describe('PollingController', () => { expect(controller._executePoll).toHaveBeenCalledTimes(3); }); it('should call _executePoll immediately once and continue calling _executePoll on interval when start is called again with the same networkClientId', async () => { - const mockMessenger = new ControllerMessenger(); - - const controller = new MyGasFeeController({ - messenger: mockMessenger, - metadata: {}, - name: 'PollingController', - state: { foo: 'bar' }, - }); controller.startPollingByNetworkClientId('mainnet'); await advanceTime({ clock, duration: 0 }); @@ -153,31 +106,12 @@ describe('PollingController', () => { }); it('should publish "pollingComplete" when stop is called', async () => { const pollingComplete: any = jest.fn(); - - const name = 'PollingController'; - - const mockMessenger = new ControllerMessenger(); - - const controller = new MyGasFeeController({ - messenger: mockMessenger, - metadata: {}, - name, - state: { foo: 'bar' }, - }); controller.onPollingCompleteByNetworkClientId('mainnet', pollingComplete); const pollingToken = controller.startPollingByNetworkClientId('mainnet'); controller.stopPollingByPollingToken(pollingToken); expect(pollingComplete).toHaveBeenCalledTimes(1); }); it('should poll at the interval length when set via setIntervalLength', async () => { - const mockMessenger = new ControllerMessenger(); - - const controller = new MyGasFeeController({ - messenger: mockMessenger, - metadata: {}, - name: 'PollingController', - state: { foo: 'bar' }, - }); controller.setIntervalLength(TICK_TIME); controller.startPollingByNetworkClientId('mainnet'); await advanceTime({ clock, duration: 0 }); @@ -190,14 +124,6 @@ describe('PollingController', () => { expect(controller._executePoll).toHaveBeenCalledTimes(2); }); it('should start and stop polling sessions for different networkClientIds with the same options', async () => { - const mockMessenger = new ControllerMessenger(); - - const controller = new MyGasFeeController({ - messenger: mockMessenger, - metadata: {}, - name: 'PollingController', - state: { foo: 'bar' }, - }); const pollToken1 = controller.startPollingByNetworkClientId('mainnet', { address: '0x1', }); @@ -239,14 +165,6 @@ describe('PollingController', () => { }); describe('multiple networkClientIds', () => { it('should poll for each networkClientId', async () => { - const mockMessenger = new ControllerMessenger(); - - const controller = new MyGasFeeController({ - messenger: mockMessenger, - metadata: {}, - name: 'PollingController', - state: { foo: 'bar' }, - }); controller.startPollingByNetworkClientId('mainnet'); await advanceTime({ clock, duration: 0 }); @@ -279,14 +197,6 @@ describe('PollingController', () => { }); it('should poll multiple networkClientIds when setting interval length', async () => { - const mockMessenger = new ControllerMessenger(); - - const controller = new MyGasFeeController({ - messenger: mockMessenger, - metadata: {}, - name: 'PollingController', - state: { foo: 'bar' }, - }); controller.setIntervalLength(TICK_TIME * 2); controller.startPollingByNetworkClientId('mainnet'); await advanceTime({ clock, duration: 0 }); @@ -404,19 +314,6 @@ describe('PollingController', () => { }); it('should start polling for the specified networkClientId', async () => { - const mockMessenger = new ControllerMessenger(); - - const controller = new MyGasFeeController({ - messenger: mockMessenger, - metadata: {}, - name: 'PollingController', - state: { foo: 'bar' }, - }); - - // const getNetworkClientById = jest.fn().mockReturnValue({ - // blockTracker: new TestBlockTracker({ interval: 5 }), - // }); - controller.setPollOnNewBlocks(getNetworkClientById); controller.startPollingByNetworkClientId('mainnet'); @@ -438,20 +335,10 @@ describe('PollingController', () => { expect.arrayContaining(['mainnet', {}]), ]); - // Stop all polling controller.stopAllPolling(); }); it('should poll on new block intervals for each networkClientId', async () => { - const mockMessenger = new ControllerMessenger(); - - const controller = new MyGasFeeController({ - messenger: mockMessenger, - metadata: {}, - name: 'PollingController', - state: { foo: 'bar' }, - }); - controller.setPollOnNewBlocks(getNetworkClientById); controller.startPollingByNetworkClientId('mainnet'); @@ -500,20 +387,10 @@ describe('PollingController', () => { ['sepolia', {}, 2], ]); - // Stop all polling controller.stopAllPolling(); }); it('should should stop polling when all polling tokens for a networkClientId are deleted', async () => { - const mockMessenger = new ControllerMessenger(); - - const controller = new MyGasFeeController({ - messenger: mockMessenger, - metadata: {}, - name: 'PollingController', - state: { foo: 'bar' }, - }); - controller.setPollOnNewBlocks(getNetworkClientById); const pollingToken1 = controller.startPollingByNetworkClientId('mainnet'); @@ -551,18 +428,11 @@ describe('PollingController', () => { ['mainnet', {}, 2], ['mainnet', {}, 3], ]); + + controller.stopAllPolling(); }); it('should should stop polling when all polling tokens for a networkClientId are deleted, even if other networkClientIds are still polling', async () => { - const mockMessenger = new ControllerMessenger(); - - const controller = new MyGasFeeController({ - messenger: mockMessenger, - metadata: {}, - name: 'PollingController', - state: { foo: 'bar' }, - }); - controller.setPollOnNewBlocks(getNetworkClientById); const pollingToken1 = controller.startPollingByNetworkClientId('mainnet'); @@ -618,6 +488,8 @@ describe('PollingController', () => { ['goerli', {}, 3], ['goerli', {}, 4], ]); + + controller.stopAllPolling(); }); }); }); From 60f9c759390997ac4b77d1277dd513d4f4420357 Mon Sep 17 00:00:00 2001 From: Alex Date: Wed, 6 Dec 2023 17:17:07 -0600 Subject: [PATCH 5/9] add protection against multiple sources of polling interval --- .../src/PollingController.test.ts | 409 ++++++++++-------- .../src/PollingController.ts | 13 +- 2 files changed, 228 insertions(+), 194 deletions(-) diff --git a/packages/polling-controller/src/PollingController.test.ts b/packages/polling-controller/src/PollingController.test.ts index 4f71f8ddf5..b7571c8f2c 100644 --- a/packages/polling-controller/src/PollingController.test.ts +++ b/packages/polling-controller/src/PollingController.test.ts @@ -83,6 +83,18 @@ describe('PollingController', () => { controller.stopAllPolling(); }); }); + + describe('setIntervalLength', () => { + it('should set getNetworkClientById if previously set to undefined when setting interval length', async () => { + controller.setPollWithBlockTracker(() => { + throw new Error('should not be called'); + }); + expect(controller.getPollingWithBlockTracker()).toBe(true); + controller.setIntervalLength(1000); + expect(controller.getPollingWithBlockTracker()).toBe(false); + }); + }); + describe('startPollingByNetworkClientId', () => { it('should call _executePoll immediately and on interval if polling', async () => { controller.startPollingByNetworkClientId('mainnet'); @@ -248,248 +260,261 @@ describe('PollingController', () => { ['sepolia', {}], ]); }); - }); - describe('PollingControllerOnly', () => { - it('can be extended from and constructed', async () => { - class MyClass extends PollingControllerOnly { - _executePoll = createExecutePollMock(); - } - const c = new MyClass(); - expect(c._executePoll).toBeDefined(); - expect(c.getIntervalLength).toBeDefined(); - expect(c.setIntervalLength).toBeDefined(); - expect(c.stopAllPolling).toBeDefined(); - expect(c.startPollingByNetworkClientId).toBeDefined(); - expect(c.stopPollingByPollingToken).toBeDefined(); - }); - }); - describe('startPollingByNetworkClientId after setPollOnNewBlocks', () => { - class TestBlockTracker extends EventEmitter { - private latestBlockNumber: number; - public interval: number; - - constructor({ interval } = { interval: 1000 }) { - super(); - this.latestBlockNumber = 0; - this.interval = interval; - this.start(interval); - } - - private start(interval: number) { - setInterval(() => { - this.latestBlockNumber += 1; - this.emit('latest', this.latestBlockNumber); - }, interval); - } - } - - let getNetworkClientById: jest.Mock; - let mainnetBlockTracker: TestBlockTracker; - let goerliBlockTracker: TestBlockTracker; - let sepoliaBlockTracker: TestBlockTracker; - beforeEach(() => { - mainnetBlockTracker = new TestBlockTracker({ interval: 5 }); - goerliBlockTracker = new TestBlockTracker({ interval: 10 }); - sepoliaBlockTracker = new TestBlockTracker({ interval: 15 }); - - getNetworkClientById = jest.fn().mockImplementation((networkClientId) => { - switch (networkClientId) { - case 'mainnet': - return { - blockTracker: mainnetBlockTracker, - }; - case 'goerli': - return { - blockTracker: goerliBlockTracker, - }; - case 'sepolia': - return { - blockTracker: sepoliaBlockTracker, - }; - default: - throw new Error(`Unknown networkClientId: ${networkClientId}`); + describe('PollingControllerOnly', () => { + it('can be extended from and constructed', async () => { + class MyClass extends PollingControllerOnly { + _executePoll = createExecutePollMock(); } + const c = new MyClass(); + expect(c._executePoll).toBeDefined(); + expect(c.getIntervalLength).toBeDefined(); + expect(c.setIntervalLength).toBeDefined(); + expect(c.stopAllPolling).toBeDefined(); + expect(c.startPollingByNetworkClientId).toBeDefined(); + expect(c.stopPollingByPollingToken).toBeDefined(); }); }); + describe('startPollingByNetworkClientId after setPollWithBlockTracker', () => { + class TestBlockTracker extends EventEmitter { + private latestBlockNumber: number; - it('should start polling for the specified networkClientId', async () => { - controller.setPollOnNewBlocks(getNetworkClientById); - - controller.startPollingByNetworkClientId('mainnet'); - - expect(getNetworkClientById).toHaveBeenCalledWith('mainnet'); - - await advanceTime({ clock, duration: 5 }); + public interval: number; - expect(controller._executePoll).toHaveBeenCalledTimes(1); - - await advanceTime({ clock, duration: 1 }); + constructor({ interval } = { interval: 1000 }) { + super(); + this.latestBlockNumber = 0; + this.interval = interval; + this.start(interval); + } - expect(controller._executePoll).toHaveBeenCalledTimes(1); + private start(interval: number) { + setInterval(() => { + this.latestBlockNumber += 1; + this.emit('latest', this.latestBlockNumber); + }, interval); + } + } - await advanceTime({ clock, duration: 4 }); + let getNetworkClientById: jest.Mock; + let mainnetBlockTracker: TestBlockTracker; + let goerliBlockTracker: TestBlockTracker; + let sepoliaBlockTracker: TestBlockTracker; + beforeEach(() => { + mainnetBlockTracker = new TestBlockTracker({ interval: 5 }); + goerliBlockTracker = new TestBlockTracker({ interval: 10 }); + sepoliaBlockTracker = new TestBlockTracker({ interval: 15 }); + + getNetworkClientById = jest + .fn() + .mockImplementation((networkClientId) => { + switch (networkClientId) { + case 'mainnet': + return { + blockTracker: mainnetBlockTracker, + }; + case 'goerli': + return { + blockTracker: goerliBlockTracker, + }; + case 'sepolia': + return { + blockTracker: sepoliaBlockTracker, + }; + default: + throw new Error(`Unknown networkClientId: ${networkClientId}`); + } + }); + }); - expect(controller._executePoll.mock.calls).toMatchObject([ - expect.arrayContaining(['mainnet', {}]), - expect.arrayContaining(['mainnet', {}]), - ]); + it('should set the interval length to 0', () => { + controller.setPollWithBlockTracker(getNetworkClientById); - controller.stopAllPolling(); - }); + expect(controller.getIntervalLength()).toBe(0); + }); - it('should poll on new block intervals for each networkClientId', async () => { - controller.setPollOnNewBlocks(getNetworkClientById); + it('should start polling for the specified networkClientId', async () => { + controller.setPollWithBlockTracker(getNetworkClientById); - controller.startPollingByNetworkClientId('mainnet'); - controller.startPollingByNetworkClientId('goerli'); - await advanceTime({ clock, duration: 5 }); + controller.startPollingByNetworkClientId('mainnet'); - expect(controller._executePoll).toHaveBeenCalledTimes(1); - expect(controller._executePoll).toHaveBeenCalledWith('mainnet', {}, 1); + expect(getNetworkClientById).toHaveBeenCalledWith('mainnet'); - // Start polling for goerli, 10ms interval - await advanceTime({ clock, duration: 5 }); + await advanceTime({ clock, duration: 5 }); - expect(controller._executePoll.mock.calls).toMatchObject([ - ['mainnet', {}, 1], - ['mainnet', {}, 2], - ['goerli', {}, 1], - ]); + expect(controller._executePoll).toHaveBeenCalledTimes(1); - await advanceTime({ clock, duration: 5 }); + await advanceTime({ clock, duration: 1 }); - expect(controller._executePoll.mock.calls).toMatchObject([ - ['mainnet', {}, 1], - ['mainnet', {}, 2], - ['goerli', {}, 1], - ['mainnet', {}, 3], - ]); + expect(controller._executePoll).toHaveBeenCalledTimes(1); - // 15ms have passed - // Start polling for sepolia, 15ms interval - controller.startPollingByNetworkClientId('sepolia'); + await advanceTime({ clock, duration: 4 }); - await advanceTime({ clock, duration: 15 }); + expect(controller._executePoll.mock.calls).toMatchObject([ + expect.arrayContaining(['mainnet', {}]), + expect.arrayContaining(['mainnet', {}]), + ]); - // at 30ms, 6 blocks have passed for mainnet (every 5ms), 3 for goerli (every 10ms), and 2 for sepolia (every 15ms) - // Didn't start listening to sepolia until 15ms had passed, so we only call executePoll on the 2nd block - expect(controller._executePoll.mock.calls).toMatchObject([ - ['mainnet', {}, 1], - ['mainnet', {}, 2], - ['goerli', {}, 1], - ['mainnet', {}, 3], - ['mainnet', {}, 4], - ['goerli', {}, 2], - ['mainnet', {}, 5], - ['mainnet', {}, 6], - ['goerli', {}, 3], - ['sepolia', {}, 2], - ]); + controller.stopAllPolling(); + }); - controller.stopAllPolling(); - }); + it('should poll on new block intervals for each networkClientId', async () => { + controller.setPollWithBlockTracker(getNetworkClientById); + + controller.startPollingByNetworkClientId('mainnet'); + controller.startPollingByNetworkClientId('goerli'); + await advanceTime({ clock, duration: 5 }); + + expect(controller._executePoll).toHaveBeenCalledTimes(1); + expect(controller._executePoll).toHaveBeenCalledWith('mainnet', {}, 1); + + // Start polling for goerli, 10ms interval + await advanceTime({ clock, duration: 5 }); + + expect(controller._executePoll.mock.calls).toMatchObject([ + ['mainnet', {}, 1], + ['mainnet', {}, 2], + ['goerli', {}, 1], + ]); + + await advanceTime({ clock, duration: 5 }); + + expect(controller._executePoll.mock.calls).toMatchObject([ + ['mainnet', {}, 1], + ['mainnet', {}, 2], + ['goerli', {}, 1], + ['mainnet', {}, 3], + ]); + + // 15ms have passed + // Start polling for sepolia, 15ms interval + controller.startPollingByNetworkClientId('sepolia'); + + await advanceTime({ clock, duration: 15 }); + + // at 30ms, 6 blocks have passed for mainnet (every 5ms), 3 for goerli (every 10ms), and 2 for sepolia (every 15ms) + // Didn't start listening to sepolia until 15ms had passed, so we only call executePoll on the 2nd block + expect(controller._executePoll.mock.calls).toMatchObject([ + ['mainnet', {}, 1], + ['mainnet', {}, 2], + ['goerli', {}, 1], + ['mainnet', {}, 3], + ['mainnet', {}, 4], + ['goerli', {}, 2], + ['mainnet', {}, 5], + ['mainnet', {}, 6], + ['goerli', {}, 3], + ['sepolia', {}, 2], + ]); + + controller.stopAllPolling(); + }); - it('should should stop polling when all polling tokens for a networkClientId are deleted', async () => { - controller.setPollOnNewBlocks(getNetworkClientById); + it('should should stop polling when all polling tokens for a networkClientId are deleted', async () => { + controller.setPollWithBlockTracker(getNetworkClientById); - const pollingToken1 = controller.startPollingByNetworkClientId('mainnet'); + const pollingToken1 = + controller.startPollingByNetworkClientId('mainnet'); - await advanceTime({ clock, duration: 5 }); + await advanceTime({ clock, duration: 5 }); - expect(controller._executePoll).toHaveBeenCalledTimes(1); - expect(controller._executePoll).toHaveBeenCalledWith('mainnet', {}, 1); + expect(controller._executePoll).toHaveBeenCalledTimes(1); + expect(controller._executePoll).toHaveBeenCalledWith('mainnet', {}, 1); - const pollingToken2 = controller.startPollingByNetworkClientId('mainnet'); - await advanceTime({ clock, duration: 5 }); + const pollingToken2 = + controller.startPollingByNetworkClientId('mainnet'); + await advanceTime({ clock, duration: 5 }); - expect(controller._executePoll.mock.calls).toMatchObject([ - ['mainnet', {}, 1], - ['mainnet', {}, 2], - ]); + expect(controller._executePoll.mock.calls).toMatchObject([ + ['mainnet', {}, 1], + ['mainnet', {}, 2], + ]); - controller.stopPollingByPollingToken(pollingToken1); + controller.stopPollingByPollingToken(pollingToken1); - await advanceTime({ clock, duration: 5 }); + await advanceTime({ clock, duration: 5 }); - expect(controller._executePoll.mock.calls).toMatchObject([ - ['mainnet', {}, 1], - ['mainnet', {}, 2], - ['mainnet', {}, 3], - ]); + expect(controller._executePoll.mock.calls).toMatchObject([ + ['mainnet', {}, 1], + ['mainnet', {}, 2], + ['mainnet', {}, 3], + ]); - controller.stopPollingByPollingToken(pollingToken2); + controller.stopPollingByPollingToken(pollingToken2); - await advanceTime({ clock, duration: 15 }); + await advanceTime({ clock, duration: 15 }); - // no further polling should occur - expect(controller._executePoll.mock.calls).toMatchObject([ - ['mainnet', {}, 1], - ['mainnet', {}, 2], - ['mainnet', {}, 3], - ]); + // no further polling should occur + expect(controller._executePoll.mock.calls).toMatchObject([ + ['mainnet', {}, 1], + ['mainnet', {}, 2], + ['mainnet', {}, 3], + ]); - controller.stopAllPolling(); - }); + controller.stopAllPolling(); + }); - it('should should stop polling when all polling tokens for a networkClientId are deleted, even if other networkClientIds are still polling', async () => { - controller.setPollOnNewBlocks(getNetworkClientById); + it('should should stop polling when all polling tokens for a networkClientId are deleted, even if other networkClientIds are still polling', async () => { + controller.setPollWithBlockTracker(getNetworkClientById); - const pollingToken1 = controller.startPollingByNetworkClientId('mainnet'); + const pollingToken1 = + controller.startPollingByNetworkClientId('mainnet'); - await advanceTime({ clock, duration: 5 }); + await advanceTime({ clock, duration: 5 }); - expect(controller._executePoll).toHaveBeenCalledWith('mainnet', {}, 1); + expect(controller._executePoll).toHaveBeenCalledWith('mainnet', {}, 1); - const pollingToken2 = controller.startPollingByNetworkClientId('mainnet'); + const pollingToken2 = + controller.startPollingByNetworkClientId('mainnet'); - await advanceTime({ clock, duration: 5 }); + await advanceTime({ clock, duration: 5 }); - expect(controller._executePoll.mock.calls).toMatchObject([ - ['mainnet', {}, 1], - ['mainnet', {}, 2], - ]); + expect(controller._executePoll.mock.calls).toMatchObject([ + ['mainnet', {}, 1], + ['mainnet', {}, 2], + ]); - controller.startPollingByNetworkClientId('goerli'); - await advanceTime({ clock, duration: 5 }); + controller.startPollingByNetworkClientId('goerli'); + await advanceTime({ clock, duration: 5 }); - // 3 blocks have passed for mainnet, 1 for goerli but we only started listening to goerli after 5ms - // so the next block will come at 20ms and be the 2nd block for goerli - expect(controller._executePoll.mock.calls).toMatchObject([ - ['mainnet', {}, 1], - ['mainnet', {}, 2], - ['mainnet', {}, 3], - ]); + // 3 blocks have passed for mainnet, 1 for goerli but we only started listening to goerli after 5ms + // so the next block will come at 20ms and be the 2nd block for goerli + expect(controller._executePoll.mock.calls).toMatchObject([ + ['mainnet', {}, 1], + ['mainnet', {}, 2], + ['mainnet', {}, 3], + ]); - controller.stopPollingByPollingToken(pollingToken1); + controller.stopPollingByPollingToken(pollingToken1); - await advanceTime({ clock, duration: 5 }); + await advanceTime({ clock, duration: 5 }); - // 20ms have passed, 4 blocks for mainnet, 2 for goerli - expect(controller._executePoll.mock.calls).toMatchObject([ - ['mainnet', {}, 1], - ['mainnet', {}, 2], - ['mainnet', {}, 3], - ['mainnet', {}, 4], - ['goerli', {}, 2], - ]); + // 20ms have passed, 4 blocks for mainnet, 2 for goerli + expect(controller._executePoll.mock.calls).toMatchObject([ + ['mainnet', {}, 1], + ['mainnet', {}, 2], + ['mainnet', {}, 3], + ['mainnet', {}, 4], + ['goerli', {}, 2], + ]); - controller.stopPollingByPollingToken(pollingToken2); + controller.stopPollingByPollingToken(pollingToken2); - await advanceTime({ clock, duration: 20 }); + await advanceTime({ clock, duration: 20 }); - // no further polling for mainnet should occur - expect(controller._executePoll.mock.calls).toMatchObject([ - ['mainnet', {}, 1], - ['mainnet', {}, 2], - ['mainnet', {}, 3], - ['mainnet', {}, 4], - ['goerli', {}, 2], - ['goerli', {}, 3], - ['goerli', {}, 4], - ]); + // no further polling for mainnet should occur + expect(controller._executePoll.mock.calls).toMatchObject([ + ['mainnet', {}, 1], + ['mainnet', {}, 2], + ['mainnet', {}, 3], + ['mainnet', {}, 4], + ['goerli', {}, 2], + ['goerli', {}, 3], + ['goerli', {}, 4], + ]); - controller.stopAllPolling(); + controller.stopAllPolling(); + }); }); }); }); diff --git a/packages/polling-controller/src/PollingController.ts b/packages/polling-controller/src/PollingController.ts index 5468d6f11f..4f524e01ef 100644 --- a/packages/polling-controller/src/PollingController.ts +++ b/packages/polling-controller/src/PollingController.ts @@ -61,6 +61,10 @@ function PollingControllerMixin(Base: TBase) { return this.#intervalLength; } + getPollingWithBlockTracker() { + return this.#getNetworkClientById !== undefined; + } + /** * Sets the length of the polling interval * @@ -68,17 +72,22 @@ function PollingControllerMixin(Base: TBase) { */ setIntervalLength(length: number) { this.#intervalLength = length; + + // setting and using an interval is mutually exclusive with polling on new blocks + this.#getNetworkClientById = undefined; } - setPollOnNewBlocks( + setPollWithBlockTracker( getNetworkClientById: (networkClientId: NetworkClientId) => NetworkClient, ) { if (!getNetworkClientById) { throw new Error('getNetworkClientById callback required'); } - this.#intervalLength = 0; this.#getNetworkClientById = getNetworkClientById; + + // using block times is mutually exclusive with polling on a static interval + this.#intervalLength = 0; } /** From eaec80e9268992851e09a86909df33ba77383792 Mon Sep 17 00:00:00 2001 From: Alex Date: Wed, 6 Dec 2023 17:27:33 -0600 Subject: [PATCH 6/9] cleanup --- packages/polling-controller/src/PollingController.ts | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/packages/polling-controller/src/PollingController.ts b/packages/polling-controller/src/PollingController.ts index 4f524e01ef..0080387f1f 100644 --- a/packages/polling-controller/src/PollingController.ts +++ b/packages/polling-controller/src/PollingController.ts @@ -143,8 +143,10 @@ function PollingControllerMixin(Base: TBase) { found = true; tokenSet.delete(pollingToken); if (tokenSet.size === 0) { - clearTimeout(this.#intervalIds[key]); - delete this.#intervalIds[key]; + if (this.#intervalIds[key]) { + clearTimeout(this.#intervalIds[key]); + delete this.#intervalIds[key]; + } // if applicable stop listening for new blocks if (this.#getNetworkClientById !== undefined) { From ac734711918bfab60bb2e48d10a246e3355f4f0d Mon Sep 17 00:00:00 2001 From: Alex Date: Thu, 7 Dec 2023 09:36:23 -0600 Subject: [PATCH 7/9] small cleanups --- .../src/PollingController.test.ts | 26 +++++++++---------- .../src/PollingController.ts | 16 +++++++----- 2 files changed, 21 insertions(+), 21 deletions(-) diff --git a/packages/polling-controller/src/PollingController.test.ts b/packages/polling-controller/src/PollingController.test.ts index b7571c8f2c..693edf028e 100644 --- a/packages/polling-controller/src/PollingController.test.ts +++ b/packages/polling-controller/src/PollingController.test.ts @@ -85,7 +85,7 @@ describe('PollingController', () => { }); describe('setIntervalLength', () => { - it('should set getNetworkClientById if previously set to undefined when setting interval length', async () => { + it('should set getNetworkClientById (if previously set by setPollWithBlockTracker) to undefined when setting interval length', async () => { controller.setPollWithBlockTracker(() => { throw new Error('should not be called'); }); @@ -136,6 +136,7 @@ describe('PollingController', () => { expect(controller._executePoll).toHaveBeenCalledTimes(2); }); it('should start and stop polling sessions for different networkClientIds with the same options', async () => { + controller.setIntervalLength(TICK_TIME); const pollToken1 = controller.startPollingByNetworkClientId('mainnet', { address: '0x1', }); @@ -180,30 +181,30 @@ describe('PollingController', () => { controller.startPollingByNetworkClientId('mainnet'); await advanceTime({ clock, duration: 0 }); - controller.startPollingByNetworkClientId('goerli'); + controller.startPollingByNetworkClientId('rinkeby'); await advanceTime({ clock, duration: 0 }); expect(controller._executePoll.mock.calls).toMatchObject([ ['mainnet', {}], - ['goerli', {}], + ['rinkeby', {}], ]); await advanceTime({ clock, duration: TICK_TIME }); expect(controller._executePoll.mock.calls).toMatchObject([ ['mainnet', {}], - ['goerli', {}], + ['rinkeby', {}], ['mainnet', {}], - ['goerli', {}], + ['rinkeby', {}], ]); await advanceTime({ clock, duration: TICK_TIME }); expect(controller._executePoll.mock.calls).toMatchObject([ ['mainnet', {}], - ['goerli', {}], + ['rinkeby', {}], ['mainnet', {}], - ['goerli', {}], + ['rinkeby', {}], ['mainnet', {}], - ['goerli', {}], + ['rinkeby', {}], ]); controller.stopAllPolling(); }); @@ -327,10 +328,10 @@ describe('PollingController', () => { }); }); - it('should set the interval length to 0', () => { + it('should set the interval length to undefined', () => { controller.setPollWithBlockTracker(getNetworkClientById); - expect(controller.getIntervalLength()).toBe(0); + expect(controller.getIntervalLength()).toBeUndefined(); }); it('should start polling for the specified networkClientId', async () => { @@ -368,7 +369,6 @@ describe('PollingController', () => { expect(controller._executePoll).toHaveBeenCalledTimes(1); expect(controller._executePoll).toHaveBeenCalledWith('mainnet', {}, 1); - // Start polling for goerli, 10ms interval await advanceTime({ clock, duration: 5 }); expect(controller._executePoll.mock.calls).toMatchObject([ @@ -450,11 +450,9 @@ describe('PollingController', () => { ['mainnet', {}, 2], ['mainnet', {}, 3], ]); - - controller.stopAllPolling(); }); - it('should should stop polling when all polling tokens for a networkClientId are deleted, even if other networkClientIds are still polling', async () => { + it('should should stop polling for one networkClientId when all polling tokens for that networkClientId are deleted, without stopping polling for networkClientIds with active pollingTokens', async () => { controller.setPollWithBlockTracker(getNetworkClientById); const pollingToken1 = diff --git a/packages/polling-controller/src/PollingController.ts b/packages/polling-controller/src/PollingController.ts index 0080387f1f..4442391316 100644 --- a/packages/polling-controller/src/PollingController.ts +++ b/packages/polling-controller/src/PollingController.ts @@ -51,7 +51,7 @@ function PollingControllerMixin(Base: TBase) { Set<(networkClientId: NetworkClientId) => void> > = new Map(); - #intervalLength = 1000; + #intervalLength: number | undefined = 1000; #getNetworkClientById: | ((networkClientId: NetworkClientId) => NetworkClient) @@ -87,7 +87,7 @@ function PollingControllerMixin(Base: TBase) { this.#getNetworkClientById = getNetworkClientById; // using block times is mutually exclusive with polling on a static interval - this.#intervalLength = 0; + this.#intervalLength = undefined; } /** @@ -143,13 +143,15 @@ function PollingControllerMixin(Base: TBase) { found = true; tokenSet.delete(pollingToken); if (tokenSet.size === 0) { + // if applicable stop polling on a static interval if (this.#intervalIds[key]) { clearTimeout(this.#intervalIds[key]); delete this.#intervalIds[key]; - } - - // if applicable stop listening for new blocks - if (this.#getNetworkClientById !== undefined) { + } else if ( + // if applicable stop listening for new blocks + this.#getNetworkClientById !== undefined && + this.#activeListeners[key] + ) { const [networkClientId] = key.split(':'); const { blockTracker } = this.#getNetworkClientById(networkClientId); @@ -210,7 +212,7 @@ function PollingControllerMixin(Base: TBase) { } throw new Error(` - Unable to retreive blockTracker for networkClientId ${networkClientId} `); + Unable to retrieve blockTracker for networkClientId ${networkClientId} `); } // if we're not polling on new blocks, use setTimeout From e632190d43a4218c5e75ac2747a3a3d61b6435e5 Mon Sep 17 00:00:00 2001 From: Alex Date: Thu, 7 Dec 2023 09:48:10 -0600 Subject: [PATCH 8/9] move tests --- .../src/PollingController.test.ts | 375 +++++++++--------- 1 file changed, 185 insertions(+), 190 deletions(-) diff --git a/packages/polling-controller/src/PollingController.test.ts b/packages/polling-controller/src/PollingController.test.ts index 693edf028e..601b79a3c1 100644 --- a/packages/polling-controller/src/PollingController.test.ts +++ b/packages/polling-controller/src/PollingController.test.ts @@ -276,243 +276,238 @@ describe('PollingController', () => { expect(c.stopPollingByPollingToken).toBeDefined(); }); }); - describe('startPollingByNetworkClientId after setPollWithBlockTracker', () => { - class TestBlockTracker extends EventEmitter { - private latestBlockNumber: number; + }); - public interval: number; + describe('Polling on block times', () => { + class TestBlockTracker extends EventEmitter { + private latestBlockNumber: number; - constructor({ interval } = { interval: 1000 }) { - super(); - this.latestBlockNumber = 0; - this.interval = interval; - this.start(interval); - } + public interval: number; - private start(interval: number) { - setInterval(() => { - this.latestBlockNumber += 1; - this.emit('latest', this.latestBlockNumber); - }, interval); - } + constructor({ interval } = { interval: 1000 }) { + super(); + this.latestBlockNumber = 0; + this.interval = interval; + this.start(interval); } - let getNetworkClientById: jest.Mock; - let mainnetBlockTracker: TestBlockTracker; - let goerliBlockTracker: TestBlockTracker; - let sepoliaBlockTracker: TestBlockTracker; - beforeEach(() => { - mainnetBlockTracker = new TestBlockTracker({ interval: 5 }); - goerliBlockTracker = new TestBlockTracker({ interval: 10 }); - sepoliaBlockTracker = new TestBlockTracker({ interval: 15 }); - - getNetworkClientById = jest - .fn() - .mockImplementation((networkClientId) => { - switch (networkClientId) { - case 'mainnet': - return { - blockTracker: mainnetBlockTracker, - }; - case 'goerli': - return { - blockTracker: goerliBlockTracker, - }; - case 'sepolia': - return { - blockTracker: sepoliaBlockTracker, - }; - default: - throw new Error(`Unknown networkClientId: ${networkClientId}`); - } - }); + private start(interval: number) { + setInterval(() => { + this.latestBlockNumber += 1; + this.emit('latest', this.latestBlockNumber); + }, interval); + } + } + + let getNetworkClientById: jest.Mock; + let mainnetBlockTracker: TestBlockTracker; + let goerliBlockTracker: TestBlockTracker; + let sepoliaBlockTracker: TestBlockTracker; + beforeEach(() => { + mainnetBlockTracker = new TestBlockTracker({ interval: 5 }); + goerliBlockTracker = new TestBlockTracker({ interval: 10 }); + sepoliaBlockTracker = new TestBlockTracker({ interval: 15 }); + + getNetworkClientById = jest.fn().mockImplementation((networkClientId) => { + switch (networkClientId) { + case 'mainnet': + return { + blockTracker: mainnetBlockTracker, + }; + case 'goerli': + return { + blockTracker: goerliBlockTracker, + }; + case 'sepolia': + return { + blockTracker: sepoliaBlockTracker, + }; + default: + throw new Error(`Unknown networkClientId: ${networkClientId}`); + } }); + }); - it('should set the interval length to undefined', () => { - controller.setPollWithBlockTracker(getNetworkClientById); + it('should set the interval length to undefined', () => { + controller.setPollWithBlockTracker(getNetworkClientById); - expect(controller.getIntervalLength()).toBeUndefined(); - }); + expect(controller.getIntervalLength()).toBeUndefined(); + }); - it('should start polling for the specified networkClientId', async () => { - controller.setPollWithBlockTracker(getNetworkClientById); + it('should start polling for the specified networkClientId', async () => { + controller.setPollWithBlockTracker(getNetworkClientById); - controller.startPollingByNetworkClientId('mainnet'); + controller.startPollingByNetworkClientId('mainnet'); - expect(getNetworkClientById).toHaveBeenCalledWith('mainnet'); + expect(getNetworkClientById).toHaveBeenCalledWith('mainnet'); - await advanceTime({ clock, duration: 5 }); + await advanceTime({ clock, duration: 5 }); - expect(controller._executePoll).toHaveBeenCalledTimes(1); + expect(controller._executePoll).toHaveBeenCalledTimes(1); - await advanceTime({ clock, duration: 1 }); + await advanceTime({ clock, duration: 1 }); - expect(controller._executePoll).toHaveBeenCalledTimes(1); + expect(controller._executePoll).toHaveBeenCalledTimes(1); - await advanceTime({ clock, duration: 4 }); + await advanceTime({ clock, duration: 4 }); - expect(controller._executePoll.mock.calls).toMatchObject([ - expect.arrayContaining(['mainnet', {}]), - expect.arrayContaining(['mainnet', {}]), - ]); + expect(controller._executePoll.mock.calls).toMatchObject([ + expect.arrayContaining(['mainnet', {}]), + expect.arrayContaining(['mainnet', {}]), + ]); - controller.stopAllPolling(); - }); + controller.stopAllPolling(); + }); - it('should poll on new block intervals for each networkClientId', async () => { - controller.setPollWithBlockTracker(getNetworkClientById); - - controller.startPollingByNetworkClientId('mainnet'); - controller.startPollingByNetworkClientId('goerli'); - await advanceTime({ clock, duration: 5 }); - - expect(controller._executePoll).toHaveBeenCalledTimes(1); - expect(controller._executePoll).toHaveBeenCalledWith('mainnet', {}, 1); - - await advanceTime({ clock, duration: 5 }); - - expect(controller._executePoll.mock.calls).toMatchObject([ - ['mainnet', {}, 1], - ['mainnet', {}, 2], - ['goerli', {}, 1], - ]); - - await advanceTime({ clock, duration: 5 }); - - expect(controller._executePoll.mock.calls).toMatchObject([ - ['mainnet', {}, 1], - ['mainnet', {}, 2], - ['goerli', {}, 1], - ['mainnet', {}, 3], - ]); - - // 15ms have passed - // Start polling for sepolia, 15ms interval - controller.startPollingByNetworkClientId('sepolia'); - - await advanceTime({ clock, duration: 15 }); - - // at 30ms, 6 blocks have passed for mainnet (every 5ms), 3 for goerli (every 10ms), and 2 for sepolia (every 15ms) - // Didn't start listening to sepolia until 15ms had passed, so we only call executePoll on the 2nd block - expect(controller._executePoll.mock.calls).toMatchObject([ - ['mainnet', {}, 1], - ['mainnet', {}, 2], - ['goerli', {}, 1], - ['mainnet', {}, 3], - ['mainnet', {}, 4], - ['goerli', {}, 2], - ['mainnet', {}, 5], - ['mainnet', {}, 6], - ['goerli', {}, 3], - ['sepolia', {}, 2], - ]); - - controller.stopAllPolling(); - }); + it('should poll on new block intervals for each networkClientId', async () => { + controller.setPollWithBlockTracker(getNetworkClientById); - it('should should stop polling when all polling tokens for a networkClientId are deleted', async () => { - controller.setPollWithBlockTracker(getNetworkClientById); + controller.startPollingByNetworkClientId('mainnet'); + controller.startPollingByNetworkClientId('goerli'); + await advanceTime({ clock, duration: 5 }); - const pollingToken1 = - controller.startPollingByNetworkClientId('mainnet'); + expect(controller._executePoll).toHaveBeenCalledTimes(1); + expect(controller._executePoll).toHaveBeenCalledWith('mainnet', {}, 1); - await advanceTime({ clock, duration: 5 }); + await advanceTime({ clock, duration: 5 }); - expect(controller._executePoll).toHaveBeenCalledTimes(1); - expect(controller._executePoll).toHaveBeenCalledWith('mainnet', {}, 1); + expect(controller._executePoll.mock.calls).toMatchObject([ + ['mainnet', {}, 1], + ['mainnet', {}, 2], + ['goerli', {}, 1], + ]); - const pollingToken2 = - controller.startPollingByNetworkClientId('mainnet'); - await advanceTime({ clock, duration: 5 }); + await advanceTime({ clock, duration: 5 }); - expect(controller._executePoll.mock.calls).toMatchObject([ - ['mainnet', {}, 1], - ['mainnet', {}, 2], - ]); + expect(controller._executePoll.mock.calls).toMatchObject([ + ['mainnet', {}, 1], + ['mainnet', {}, 2], + ['goerli', {}, 1], + ['mainnet', {}, 3], + ]); - controller.stopPollingByPollingToken(pollingToken1); + // 15ms have passed + // Start polling for sepolia, 15ms interval + controller.startPollingByNetworkClientId('sepolia'); - await advanceTime({ clock, duration: 5 }); + await advanceTime({ clock, duration: 15 }); - expect(controller._executePoll.mock.calls).toMatchObject([ - ['mainnet', {}, 1], - ['mainnet', {}, 2], - ['mainnet', {}, 3], - ]); + // at 30ms, 6 blocks have passed for mainnet (every 5ms), 3 for goerli (every 10ms), and 2 for sepolia (every 15ms) + // Didn't start listening to sepolia until 15ms had passed, so we only call executePoll on the 2nd block + expect(controller._executePoll.mock.calls).toMatchObject([ + ['mainnet', {}, 1], + ['mainnet', {}, 2], + ['goerli', {}, 1], + ['mainnet', {}, 3], + ['mainnet', {}, 4], + ['goerli', {}, 2], + ['mainnet', {}, 5], + ['mainnet', {}, 6], + ['goerli', {}, 3], + ['sepolia', {}, 2], + ]); - controller.stopPollingByPollingToken(pollingToken2); + controller.stopAllPolling(); + }); - await advanceTime({ clock, duration: 15 }); + it('should should stop polling when all polling tokens for a networkClientId are deleted', async () => { + controller.setPollWithBlockTracker(getNetworkClientById); - // no further polling should occur - expect(controller._executePoll.mock.calls).toMatchObject([ - ['mainnet', {}, 1], - ['mainnet', {}, 2], - ['mainnet', {}, 3], - ]); - }); + const pollingToken1 = controller.startPollingByNetworkClientId('mainnet'); - it('should should stop polling for one networkClientId when all polling tokens for that networkClientId are deleted, without stopping polling for networkClientIds with active pollingTokens', async () => { - controller.setPollWithBlockTracker(getNetworkClientById); + await advanceTime({ clock, duration: 5 }); - const pollingToken1 = - controller.startPollingByNetworkClientId('mainnet'); + expect(controller._executePoll).toHaveBeenCalledTimes(1); + expect(controller._executePoll).toHaveBeenCalledWith('mainnet', {}, 1); - await advanceTime({ clock, duration: 5 }); + const pollingToken2 = controller.startPollingByNetworkClientId('mainnet'); + await advanceTime({ clock, duration: 5 }); - expect(controller._executePoll).toHaveBeenCalledWith('mainnet', {}, 1); + expect(controller._executePoll.mock.calls).toMatchObject([ + ['mainnet', {}, 1], + ['mainnet', {}, 2], + ]); - const pollingToken2 = - controller.startPollingByNetworkClientId('mainnet'); + controller.stopPollingByPollingToken(pollingToken1); - await advanceTime({ clock, duration: 5 }); + await advanceTime({ clock, duration: 5 }); - expect(controller._executePoll.mock.calls).toMatchObject([ - ['mainnet', {}, 1], - ['mainnet', {}, 2], - ]); + expect(controller._executePoll.mock.calls).toMatchObject([ + ['mainnet', {}, 1], + ['mainnet', {}, 2], + ['mainnet', {}, 3], + ]); - controller.startPollingByNetworkClientId('goerli'); - await advanceTime({ clock, duration: 5 }); + controller.stopPollingByPollingToken(pollingToken2); - // 3 blocks have passed for mainnet, 1 for goerli but we only started listening to goerli after 5ms - // so the next block will come at 20ms and be the 2nd block for goerli - expect(controller._executePoll.mock.calls).toMatchObject([ - ['mainnet', {}, 1], - ['mainnet', {}, 2], - ['mainnet', {}, 3], - ]); + await advanceTime({ clock, duration: 15 }); - controller.stopPollingByPollingToken(pollingToken1); + // no further polling should occur + expect(controller._executePoll.mock.calls).toMatchObject([ + ['mainnet', {}, 1], + ['mainnet', {}, 2], + ['mainnet', {}, 3], + ]); + }); - await advanceTime({ clock, duration: 5 }); + it('should should stop polling for one networkClientId when all polling tokens for that networkClientId are deleted, without stopping polling for networkClientIds with active pollingTokens', async () => { + controller.setPollWithBlockTracker(getNetworkClientById); - // 20ms have passed, 4 blocks for mainnet, 2 for goerli - expect(controller._executePoll.mock.calls).toMatchObject([ - ['mainnet', {}, 1], - ['mainnet', {}, 2], - ['mainnet', {}, 3], - ['mainnet', {}, 4], - ['goerli', {}, 2], - ]); + const pollingToken1 = controller.startPollingByNetworkClientId('mainnet'); - controller.stopPollingByPollingToken(pollingToken2); + await advanceTime({ clock, duration: 5 }); - await advanceTime({ clock, duration: 20 }); + expect(controller._executePoll).toHaveBeenCalledWith('mainnet', {}, 1); - // no further polling for mainnet should occur - expect(controller._executePoll.mock.calls).toMatchObject([ - ['mainnet', {}, 1], - ['mainnet', {}, 2], - ['mainnet', {}, 3], - ['mainnet', {}, 4], - ['goerli', {}, 2], - ['goerli', {}, 3], - ['goerli', {}, 4], - ]); + const pollingToken2 = controller.startPollingByNetworkClientId('mainnet'); - controller.stopAllPolling(); - }); + await advanceTime({ clock, duration: 5 }); + + expect(controller._executePoll.mock.calls).toMatchObject([ + ['mainnet', {}, 1], + ['mainnet', {}, 2], + ]); + + controller.startPollingByNetworkClientId('goerli'); + await advanceTime({ clock, duration: 5 }); + + // 3 blocks have passed for mainnet, 1 for goerli but we only started listening to goerli after 5ms + // so the next block will come at 20ms and be the 2nd block for goerli + expect(controller._executePoll.mock.calls).toMatchObject([ + ['mainnet', {}, 1], + ['mainnet', {}, 2], + ['mainnet', {}, 3], + ]); + + controller.stopPollingByPollingToken(pollingToken1); + + await advanceTime({ clock, duration: 5 }); + + // 20ms have passed, 4 blocks for mainnet, 2 for goerli + expect(controller._executePoll.mock.calls).toMatchObject([ + ['mainnet', {}, 1], + ['mainnet', {}, 2], + ['mainnet', {}, 3], + ['mainnet', {}, 4], + ['goerli', {}, 2], + ]); + + controller.stopPollingByPollingToken(pollingToken2); + + await advanceTime({ clock, duration: 20 }); + + // no further polling for mainnet should occur + expect(controller._executePoll.mock.calls).toMatchObject([ + ['mainnet', {}, 1], + ['mainnet', {}, 2], + ['mainnet', {}, 3], + ['mainnet', {}, 4], + ['goerli', {}, 2], + ['goerli', {}, 3], + ['goerli', {}, 4], + ]); + + controller.stopAllPolling(); }); }); }); From 1729db8de6346567f4c7946928748a39e6cd571b Mon Sep 17 00:00:00 2001 From: Alex Date: Thu, 7 Dec 2023 09:49:45 -0600 Subject: [PATCH 9/9] reorganize new tests --- .../src/PollingController.test.ts | 287 +++++++++--------- 1 file changed, 148 insertions(+), 139 deletions(-) diff --git a/packages/polling-controller/src/PollingController.test.ts b/packages/polling-controller/src/PollingController.test.ts index 601b79a3c1..0a095c4547 100644 --- a/packages/polling-controller/src/PollingController.test.ts +++ b/packages/polling-controller/src/PollingController.test.ts @@ -327,187 +327,196 @@ describe('PollingController', () => { } }); }); + describe('setPollWithBlockTracker', () => { + it('should set the interval length to undefined', () => { + controller.setPollWithBlockTracker(getNetworkClientById); - it('should set the interval length to undefined', () => { - controller.setPollWithBlockTracker(getNetworkClientById); - - expect(controller.getIntervalLength()).toBeUndefined(); - }); - - it('should start polling for the specified networkClientId', async () => { - controller.setPollWithBlockTracker(getNetworkClientById); - - controller.startPollingByNetworkClientId('mainnet'); - - expect(getNetworkClientById).toHaveBeenCalledWith('mainnet'); - - await advanceTime({ clock, duration: 5 }); - - expect(controller._executePoll).toHaveBeenCalledTimes(1); - - await advanceTime({ clock, duration: 1 }); - - expect(controller._executePoll).toHaveBeenCalledTimes(1); - - await advanceTime({ clock, duration: 4 }); - - expect(controller._executePoll.mock.calls).toMatchObject([ - expect.arrayContaining(['mainnet', {}]), - expect.arrayContaining(['mainnet', {}]), - ]); - - controller.stopAllPolling(); + expect(controller.getIntervalLength()).toBeUndefined(); + }); }); - it('should poll on new block intervals for each networkClientId', async () => { - controller.setPollWithBlockTracker(getNetworkClientById); + describe('startPollingByNetworkClientId', () => { + it('should start polling for the specified networkClientId', async () => { + controller.setPollWithBlockTracker(getNetworkClientById); - controller.startPollingByNetworkClientId('mainnet'); - controller.startPollingByNetworkClientId('goerli'); - await advanceTime({ clock, duration: 5 }); + controller.startPollingByNetworkClientId('mainnet'); - expect(controller._executePoll).toHaveBeenCalledTimes(1); - expect(controller._executePoll).toHaveBeenCalledWith('mainnet', {}, 1); + expect(getNetworkClientById).toHaveBeenCalledWith('mainnet'); - await advanceTime({ clock, duration: 5 }); + await advanceTime({ clock, duration: 5 }); - expect(controller._executePoll.mock.calls).toMatchObject([ - ['mainnet', {}, 1], - ['mainnet', {}, 2], - ['goerli', {}, 1], - ]); + expect(controller._executePoll).toHaveBeenCalledTimes(1); - await advanceTime({ clock, duration: 5 }); + await advanceTime({ clock, duration: 1 }); - expect(controller._executePoll.mock.calls).toMatchObject([ - ['mainnet', {}, 1], - ['mainnet', {}, 2], - ['goerli', {}, 1], - ['mainnet', {}, 3], - ]); + expect(controller._executePoll).toHaveBeenCalledTimes(1); - // 15ms have passed - // Start polling for sepolia, 15ms interval - controller.startPollingByNetworkClientId('sepolia'); + await advanceTime({ clock, duration: 4 }); - await advanceTime({ clock, duration: 15 }); + expect(controller._executePoll.mock.calls).toMatchObject([ + expect.arrayContaining(['mainnet', {}]), + expect.arrayContaining(['mainnet', {}]), + ]); - // at 30ms, 6 blocks have passed for mainnet (every 5ms), 3 for goerli (every 10ms), and 2 for sepolia (every 15ms) - // Didn't start listening to sepolia until 15ms had passed, so we only call executePoll on the 2nd block - expect(controller._executePoll.mock.calls).toMatchObject([ - ['mainnet', {}, 1], - ['mainnet', {}, 2], - ['goerli', {}, 1], - ['mainnet', {}, 3], - ['mainnet', {}, 4], - ['goerli', {}, 2], - ['mainnet', {}, 5], - ['mainnet', {}, 6], - ['goerli', {}, 3], - ['sepolia', {}, 2], - ]); + controller.stopAllPolling(); + }); - controller.stopAllPolling(); + it('should poll on new block intervals for each networkClientId', async () => { + controller.setPollWithBlockTracker(getNetworkClientById); + + controller.startPollingByNetworkClientId('mainnet'); + controller.startPollingByNetworkClientId('goerli'); + await advanceTime({ clock, duration: 5 }); + + expect(controller._executePoll).toHaveBeenCalledTimes(1); + expect(controller._executePoll).toHaveBeenCalledWith('mainnet', {}, 1); + + await advanceTime({ clock, duration: 5 }); + + expect(controller._executePoll.mock.calls).toMatchObject([ + ['mainnet', {}, 1], + ['mainnet', {}, 2], + ['goerli', {}, 1], + ]); + + await advanceTime({ clock, duration: 5 }); + + expect(controller._executePoll.mock.calls).toMatchObject([ + ['mainnet', {}, 1], + ['mainnet', {}, 2], + ['goerli', {}, 1], + ['mainnet', {}, 3], + ]); + + // 15ms have passed + // Start polling for sepolia, 15ms interval + controller.startPollingByNetworkClientId('sepolia'); + + await advanceTime({ clock, duration: 15 }); + + // at 30ms, 6 blocks have passed for mainnet (every 5ms), 3 for goerli (every 10ms), and 2 for sepolia (every 15ms) + // Didn't start listening to sepolia until 15ms had passed, so we only call executePoll on the 2nd block + expect(controller._executePoll.mock.calls).toMatchObject([ + ['mainnet', {}, 1], + ['mainnet', {}, 2], + ['goerli', {}, 1], + ['mainnet', {}, 3], + ['mainnet', {}, 4], + ['goerli', {}, 2], + ['mainnet', {}, 5], + ['mainnet', {}, 6], + ['goerli', {}, 3], + ['sepolia', {}, 2], + ]); + + controller.stopAllPolling(); + }); }); - it('should should stop polling when all polling tokens for a networkClientId are deleted', async () => { - controller.setPollWithBlockTracker(getNetworkClientById); + describe('stopPollingByPollingToken', () => { + it('should should stop polling when all polling tokens for a networkClientId are deleted', async () => { + controller.setPollWithBlockTracker(getNetworkClientById); - const pollingToken1 = controller.startPollingByNetworkClientId('mainnet'); + const pollingToken1 = + controller.startPollingByNetworkClientId('mainnet'); - await advanceTime({ clock, duration: 5 }); + await advanceTime({ clock, duration: 5 }); - expect(controller._executePoll).toHaveBeenCalledTimes(1); - expect(controller._executePoll).toHaveBeenCalledWith('mainnet', {}, 1); + expect(controller._executePoll).toHaveBeenCalledTimes(1); + expect(controller._executePoll).toHaveBeenCalledWith('mainnet', {}, 1); - const pollingToken2 = controller.startPollingByNetworkClientId('mainnet'); - await advanceTime({ clock, duration: 5 }); + const pollingToken2 = + controller.startPollingByNetworkClientId('mainnet'); + await advanceTime({ clock, duration: 5 }); - expect(controller._executePoll.mock.calls).toMatchObject([ - ['mainnet', {}, 1], - ['mainnet', {}, 2], - ]); + expect(controller._executePoll.mock.calls).toMatchObject([ + ['mainnet', {}, 1], + ['mainnet', {}, 2], + ]); - controller.stopPollingByPollingToken(pollingToken1); + controller.stopPollingByPollingToken(pollingToken1); - await advanceTime({ clock, duration: 5 }); + await advanceTime({ clock, duration: 5 }); - expect(controller._executePoll.mock.calls).toMatchObject([ - ['mainnet', {}, 1], - ['mainnet', {}, 2], - ['mainnet', {}, 3], - ]); + expect(controller._executePoll.mock.calls).toMatchObject([ + ['mainnet', {}, 1], + ['mainnet', {}, 2], + ['mainnet', {}, 3], + ]); - controller.stopPollingByPollingToken(pollingToken2); + controller.stopPollingByPollingToken(pollingToken2); - await advanceTime({ clock, duration: 15 }); + await advanceTime({ clock, duration: 15 }); - // no further polling should occur - expect(controller._executePoll.mock.calls).toMatchObject([ - ['mainnet', {}, 1], - ['mainnet', {}, 2], - ['mainnet', {}, 3], - ]); - }); + // no further polling should occur + expect(controller._executePoll.mock.calls).toMatchObject([ + ['mainnet', {}, 1], + ['mainnet', {}, 2], + ['mainnet', {}, 3], + ]); + }); - it('should should stop polling for one networkClientId when all polling tokens for that networkClientId are deleted, without stopping polling for networkClientIds with active pollingTokens', async () => { - controller.setPollWithBlockTracker(getNetworkClientById); + it('should should stop polling for one networkClientId when all polling tokens for that networkClientId are deleted, without stopping polling for networkClientIds with active pollingTokens', async () => { + controller.setPollWithBlockTracker(getNetworkClientById); - const pollingToken1 = controller.startPollingByNetworkClientId('mainnet'); + const pollingToken1 = + controller.startPollingByNetworkClientId('mainnet'); - await advanceTime({ clock, duration: 5 }); + await advanceTime({ clock, duration: 5 }); - expect(controller._executePoll).toHaveBeenCalledWith('mainnet', {}, 1); + expect(controller._executePoll).toHaveBeenCalledWith('mainnet', {}, 1); - const pollingToken2 = controller.startPollingByNetworkClientId('mainnet'); + const pollingToken2 = + controller.startPollingByNetworkClientId('mainnet'); - await advanceTime({ clock, duration: 5 }); + await advanceTime({ clock, duration: 5 }); - expect(controller._executePoll.mock.calls).toMatchObject([ - ['mainnet', {}, 1], - ['mainnet', {}, 2], - ]); + expect(controller._executePoll.mock.calls).toMatchObject([ + ['mainnet', {}, 1], + ['mainnet', {}, 2], + ]); - controller.startPollingByNetworkClientId('goerli'); - await advanceTime({ clock, duration: 5 }); + controller.startPollingByNetworkClientId('goerli'); + await advanceTime({ clock, duration: 5 }); - // 3 blocks have passed for mainnet, 1 for goerli but we only started listening to goerli after 5ms - // so the next block will come at 20ms and be the 2nd block for goerli - expect(controller._executePoll.mock.calls).toMatchObject([ - ['mainnet', {}, 1], - ['mainnet', {}, 2], - ['mainnet', {}, 3], - ]); + // 3 blocks have passed for mainnet, 1 for goerli but we only started listening to goerli after 5ms + // so the next block will come at 20ms and be the 2nd block for goerli + expect(controller._executePoll.mock.calls).toMatchObject([ + ['mainnet', {}, 1], + ['mainnet', {}, 2], + ['mainnet', {}, 3], + ]); - controller.stopPollingByPollingToken(pollingToken1); + controller.stopPollingByPollingToken(pollingToken1); - await advanceTime({ clock, duration: 5 }); + await advanceTime({ clock, duration: 5 }); - // 20ms have passed, 4 blocks for mainnet, 2 for goerli - expect(controller._executePoll.mock.calls).toMatchObject([ - ['mainnet', {}, 1], - ['mainnet', {}, 2], - ['mainnet', {}, 3], - ['mainnet', {}, 4], - ['goerli', {}, 2], - ]); + // 20ms have passed, 4 blocks for mainnet, 2 for goerli + expect(controller._executePoll.mock.calls).toMatchObject([ + ['mainnet', {}, 1], + ['mainnet', {}, 2], + ['mainnet', {}, 3], + ['mainnet', {}, 4], + ['goerli', {}, 2], + ]); - controller.stopPollingByPollingToken(pollingToken2); + controller.stopPollingByPollingToken(pollingToken2); - await advanceTime({ clock, duration: 20 }); + await advanceTime({ clock, duration: 20 }); - // no further polling for mainnet should occur - expect(controller._executePoll.mock.calls).toMatchObject([ - ['mainnet', {}, 1], - ['mainnet', {}, 2], - ['mainnet', {}, 3], - ['mainnet', {}, 4], - ['goerli', {}, 2], - ['goerli', {}, 3], - ['goerli', {}, 4], - ]); + // no further polling for mainnet should occur + expect(controller._executePoll.mock.calls).toMatchObject([ + ['mainnet', {}, 1], + ['mainnet', {}, 2], + ['mainnet', {}, 3], + ['mainnet', {}, 4], + ['goerli', {}, 2], + ['goerli', {}, 3], + ['goerli', {}, 4], + ]); - controller.stopAllPolling(); + controller.stopAllPolling(); + }); }); }); });