From b272ea9a1a53cc21eacba08a2554233a4345b6eb Mon Sep 17 00:00:00 2001 From: Mircea Hasegan Date: Fri, 18 Oct 2024 23:19:38 +0200 Subject: [PATCH] fix(wallet): one ledger-tip req per pollInterval WHAT: replaced `delay` operator with `auditTime` operator, which starts a timer once the first isSettled$ emission, and emits the last value, ignoring previous ones. WHY: isSettled$ observable emissions triggered ledger-tip requests, delayed by pollingInterval. Flapping isSettled$ caused multiple requests to be done, because the flapping was delayed. Throttling is in place for the case where the ledger-tip request is not resolved yet. --- packages/wallet/src/services/TipTracker.ts | 10 ++-- .../wallet/test/services/TipTracker.test.ts | 48 ++++++++++++++++++- 2 files changed, 52 insertions(+), 6 deletions(-) diff --git a/packages/wallet/src/services/TipTracker.ts b/packages/wallet/src/services/TipTracker.ts index fc3fb96bfc2..2dab08cc569 100644 --- a/packages/wallet/src/services/TipTracker.ts +++ b/packages/wallet/src/services/TipTracker.ts @@ -5,9 +5,9 @@ import { EMPTY, Observable, Subject, + auditTime, combineLatest, concat, - delay, distinctUntilChanged, exhaustMap, filter, @@ -56,17 +56,19 @@ export class TipTracker extends PersistentDocumentTrackerSubject { // - if it's not settled for maxPollInterval combineLatest([ triggerOrInterval$(syncStatus.isSettled$.pipe(filter((isSettled) => isSettled)), maxPollInterval).pipe( - // trigger fetch after some delay once fully synced and online - delay(minPollInterval), + // Do not allow more than one fetch per minPollInterval. + // The first syncStatus starts a minPollInterval timer. Only the latest emission from the minPollInterval + // is emitted in case the syncStatus changes multiple times. + auditTime(minPollInterval), // trigger fetch on start startWith(null) ), connectionStatus$ ]).pipe( - // Throttle syncing by interval, cancel ongoing request on external trigger tap(([, connectionStatus]) => { logger.debug(connectionStatus === ConnectionStatus.down ? 'Skipping fetch tip' : 'Fetching tip...'); }), + // Throttle syncing by interval, cancel ongoing request on external trigger exhaustMap(([, connectionStatus]) => connectionStatus === ConnectionStatus.down ? EMPTY diff --git a/packages/wallet/test/services/TipTracker.test.ts b/packages/wallet/test/services/TipTracker.test.ts index 93ed128ed10..7dedeab3219 100644 --- a/packages/wallet/test/services/TipTracker.test.ts +++ b/packages/wallet/test/services/TipTracker.test.ts @@ -2,7 +2,7 @@ import { Cardano } from '@cardano-sdk/core'; import { ConnectionStatus, TipTracker } from '../../src/services'; import { InMemoryDocumentStore } from '../../src/persistence'; import { Milliseconds, SyncStatus } from '../../src'; -import { Observable, firstValueFrom, of } from 'rxjs'; +import { NEVER, Observable, firstValueFrom, of, take, takeUntil, timer } from 'rxjs'; import { createStubObservable, createTestScheduler } from '@cardano-sdk/util-dev'; import { dummyLogger } from 'ts-log'; @@ -15,6 +15,8 @@ const mockTips = { y: { hash: 'hy' } } as unknown as Record; +const trueFalse = { f: false, t: true }; + describe('TipTracker', () => { const pollInterval: Milliseconds = 1; // delays emission after trigger let store: InMemoryDocumentStore; @@ -26,6 +28,48 @@ describe('TipTracker', () => { connectionStatus$ = of(ConnectionStatus.up); }); + it('calls the provider as soon as subscribed', () => { + createTestScheduler().run(({ cold, expectObservable }) => { + const provider$ = createStubObservable(cold('a|', mockTips)); + const tracker$ = new TipTracker({ + connectionStatus$, + logger, + maxPollInterval: Number.MAX_VALUE, + minPollInterval: pollInterval, + provider$, + store, + syncStatus: { isSettled$: NEVER } as unknown as SyncStatus + }); + expectObservable(tracker$.asObservable().pipe(take(1))).toBe('(a|)', mockTips); + }); + }); + + it('LW-11686 ignores multiple syncStatus emissions during pollInterval', () => { + const poll: Milliseconds = 3; + const sync = '-ttt-t----|'; + const tipT = 'a---b---c-|'; + // a-b--c-d| + createTestScheduler().run(({ cold, expectObservable }) => { + const syncStatus: Partial = { isSettled$: cold(sync, trueFalse) }; + const provider$ = createStubObservable( + cold('(a|)', mockTips), + cold('(b|)', mockTips), + cold('(c|)', mockTips), + cold('(d|)', mockTips) + ); + const tracker$ = new TipTracker({ + connectionStatus$, + logger, + maxPollInterval: Number.MAX_VALUE, + minPollInterval: poll, + provider$, + store, + syncStatus: syncStatus as SyncStatus + }); + expectObservable(tracker$.asObservable().pipe(takeUntil(timer(10)))).toBe(tipT, mockTips); + }); + }); + it('calls the provider immediately, only emitting distinct values, with throttling', () => { createTestScheduler().run(({ cold, expectObservable }) => { const syncStatus: Partial = { isSettled$: cold('---a---bc--d|') }; @@ -48,7 +92,7 @@ describe('TipTracker', () => { }); }); - it('starting offline, then coming online should subscribe to provider immediatelly for initial fetch', () => { + it('starting offline, then coming online should subscribe to provider immediately for initial fetch', () => { createTestScheduler().run(({ cold, hot, expectObservable, expectSubscriptions }) => { const connectionStatusOffOn$ = hot('d--u----|', { d: ConnectionStatus.down,