From 104c7f428534971f294c623042d59d49f3b7ba4b Mon Sep 17 00:00:00 2001 From: Mononaut Date: Thu, 8 Aug 2024 13:12:31 +0000 Subject: [PATCH] Persist mempool block visualization between pages --- .../block-overview-graph.component.ts | 4 +-- .../block-overview-graph/block-scene.ts | 11 +++--- .../mempool-block-overview.component.ts | 23 +++++++++++-- .../mempool-block/mempool-block.component.ts | 2 +- .../src/app/interfaces/websocket.interface.ts | 2 ++ frontend/src/app/services/state.service.ts | 34 +++++++++++++------ .../src/app/services/websocket.service.ts | 24 ++++++++++--- frontend/src/app/shared/common.utils.ts | 3 +- 8 files changed, 76 insertions(+), 27 deletions(-) diff --git a/frontend/src/app/components/block-overview-graph/block-overview-graph.component.ts b/frontend/src/app/components/block-overview-graph/block-overview-graph.component.ts index ab9a29293..3be0692a5 100644 --- a/frontend/src/app/components/block-overview-graph/block-overview-graph.component.ts +++ b/frontend/src/app/components/block-overview-graph/block-overview-graph.component.ts @@ -198,7 +198,7 @@ export class BlockOverviewGraphComponent implements AfterViewInit, OnDestroy, On } // initialize the scene without any entry transition - setup(transactions: TransactionStripped[]): void { + setup(transactions: TransactionStripped[], sort: boolean = false): void { const filtersAvailable = transactions.reduce((flagSet, tx) => flagSet || tx.flags > 0, false); if (filtersAvailable !== this.filtersAvailable) { this.setFilterFlags(); @@ -206,7 +206,7 @@ export class BlockOverviewGraphComponent implements AfterViewInit, OnDestroy, On this.filtersAvailable = filtersAvailable; if (this.scene) { this.clearUpdateQueue(); - this.scene.setup(transactions); + this.scene.setup(transactions, sort); this.readyNextFrame = true; this.start(); this.updateSearchHighlight(); diff --git a/frontend/src/app/components/block-overview-graph/block-scene.ts b/frontend/src/app/components/block-overview-graph/block-scene.ts index c59fcb7d4..4f07818a5 100644 --- a/frontend/src/app/components/block-overview-graph/block-scene.ts +++ b/frontend/src/app/components/block-overview-graph/block-scene.ts @@ -88,16 +88,19 @@ export default class BlockScene { } // set up the scene with an initial set of transactions, without any transition animation - setup(txs: TransactionStripped[]) { + setup(txs: TransactionStripped[], sort: boolean = false) { // clean up any old transactions Object.values(this.txs).forEach(tx => { tx.destroy(); delete this.txs[tx.txid]; }); this.layout = new BlockLayout({ width: this.gridWidth, height: this.gridHeight }); - txs.forEach(tx => { - const txView = new TxView(tx, this); - this.txs[tx.txid] = txView; + let txViews = txs.map(tx => new TxView(tx, this)); + if (sort) { + txViews = txViews.sort(feeRateDescending); + } + txViews.forEach(txView => { + this.txs[txView.txid] = txView; this.place(txView); this.saveGridToScreenPosition(txView); this.applyTxUpdate(txView, { diff --git a/frontend/src/app/components/mempool-block-overview/mempool-block-overview.component.ts b/frontend/src/app/components/mempool-block-overview/mempool-block-overview.component.ts index 2c564882e..50f8b650f 100644 --- a/frontend/src/app/components/mempool-block-overview/mempool-block-overview.component.ts +++ b/frontend/src/app/components/mempool-block-overview/mempool-block-overview.component.ts @@ -31,7 +31,7 @@ export class MempoolBlockOverviewComponent implements OnInit, OnDestroy, OnChang lastBlockHeight: number; blockIndex: number; - isLoading$ = new BehaviorSubject(true); + isLoading$ = new BehaviorSubject(false); timeLtrSubscription: Subscription; timeLtr: boolean; chainDirection: string = 'right'; @@ -95,6 +95,7 @@ export class MempoolBlockOverviewComponent implements OnInit, OnDestroy, OnChang } } this.updateBlock({ + block: this.blockIndex, removed, changed, added @@ -110,8 +111,11 @@ export class MempoolBlockOverviewComponent implements OnInit, OnDestroy, OnChang if (this.blockGraph) { this.blockGraph.clear(changes.index.currentValue > changes.index.previousValue ? this.chainDirection : this.poolDirection); } - this.isLoading$.next(true); - this.websocketService.startTrackMempoolBlock(changes.index.currentValue); + if (!this.websocketService.startTrackMempoolBlock(changes.index.currentValue) && this.stateService.mempoolBlockState && this.stateService.mempoolBlockState.block === changes.index.currentValue) { + this.resumeBlock(Object.values(this.stateService.mempoolBlockState.transactions)); + } else { + this.isLoading$.next(true); + } } } @@ -153,6 +157,19 @@ export class MempoolBlockOverviewComponent implements OnInit, OnDestroy, OnChang this.isLoading$.next(false); } + resumeBlock(transactionsStripped: TransactionStripped[]): void { + if (this.blockGraph) { + this.firstLoad = false; + this.blockGraph.setup(transactionsStripped, true); + this.blockIndex = this.index; + this.isLoading$.next(false); + } else { + requestAnimationFrame(() => { + this.resumeBlock(transactionsStripped); + }); + } + } + onTxClick(event: { tx: TransactionStripped, keyModifier: boolean }): void { const url = new RelativeUrlPipe(this.stateService).transform(`/tx/${event.tx.txid}`); if (!event.keyModifier) { diff --git a/frontend/src/app/components/mempool-block/mempool-block.component.ts b/frontend/src/app/components/mempool-block/mempool-block.component.ts index 430a456ec..d2e658302 100644 --- a/frontend/src/app/components/mempool-block/mempool-block.component.ts +++ b/frontend/src/app/components/mempool-block/mempool-block.component.ts @@ -71,7 +71,7 @@ export class MempoolBlockComponent implements OnInit, OnDestroy { }) ); - this.mempoolBlockTransactions$ = this.stateService.liveMempoolBlockTransactions$.pipe(map(txMap => Object.values(txMap))); + this.mempoolBlockTransactions$ = this.stateService.liveMempoolBlockTransactions$.pipe(map(({transactions}) => Object.values(transactions))); this.network$ = this.stateService.networkChanged$; } diff --git a/frontend/src/app/interfaces/websocket.interface.ts b/frontend/src/app/interfaces/websocket.interface.ts index 35e0ffa09..7552224f5 100644 --- a/frontend/src/app/interfaces/websocket.interface.ts +++ b/frontend/src/app/interfaces/websocket.interface.ts @@ -72,11 +72,13 @@ export interface MempoolBlockWithTransactions extends MempoolBlock { } export interface MempoolBlockDelta { + block: number; added: TransactionStripped[]; removed: string[]; changed: { txid: string, rate: number, flags: number, acc: boolean }[]; } export interface MempoolBlockState { + block: number; transactions: TransactionStripped[]; } export type MempoolBlockUpdate = MempoolBlockDelta | MempoolBlockState; diff --git a/frontend/src/app/services/state.service.ts b/frontend/src/app/services/state.service.ts index 365c1daa2..13ffc7fc5 100644 --- a/frontend/src/app/services/state.service.ts +++ b/frontend/src/app/services/state.service.ts @@ -5,7 +5,7 @@ import { AccelerationDelta, HealthCheckHost, IBackendInfo, MempoolBlock, Mempool import { Acceleration, AccelerationPosition, BlockExtended, CpfpInfo, DifficultyAdjustment, MempoolPosition, OptimizedMempoolStats, RbfTree, TransactionStripped } from '../interfaces/node-api.interface'; import { Router, NavigationStart } from '@angular/router'; import { isPlatformBrowser } from '@angular/common'; -import { filter, map, scan, shareReplay } from 'rxjs/operators'; +import { filter, map, scan, share, shareReplay } from 'rxjs/operators'; import { StorageService } from './storage.service'; import { hasTouchScreen } from '../shared/pipes/bytes-pipe/utils'; import { ActiveFilter } from '../shared/filters.utils'; @@ -131,6 +131,7 @@ export class StateService { latestBlockHeight = -1; blocks: BlockExtended[] = []; mempoolSequence: number; + mempoolBlockState: { block: number, transactions: { [txid: string]: TransactionStripped} }; backend$ = new BehaviorSubject<'esplora' | 'electrum' | 'none'>('esplora'); networkChanged$ = new ReplaySubject(1); @@ -143,7 +144,7 @@ export class StateService { mempoolInfo$ = new ReplaySubject(1); mempoolBlocks$ = new ReplaySubject(1); mempoolBlockUpdate$ = new Subject(); - liveMempoolBlockTransactions$: Observable<{ [txid: string]: TransactionStripped}>; + liveMempoolBlockTransactions$: Observable<{ block: number, transactions: { [txid: string]: TransactionStripped} }>; accelerations$ = new Subject(); liveAccelerations$: Observable; txConfirmed$ = new Subject<[string, BlockExtended]>(); @@ -231,29 +232,40 @@ export class StateService { } }); - this.liveMempoolBlockTransactions$ = this.mempoolBlockUpdate$.pipe(scan((transactions: { [txid: string]: TransactionStripped }, change: MempoolBlockUpdate): { [txid: string]: TransactionStripped } => { + this.liveMempoolBlockTransactions$ = this.mempoolBlockUpdate$.pipe(scan((acc: { block: number, transactions: { [txid: string]: TransactionStripped } }, change: MempoolBlockUpdate): { block: number, transactions: { [txid: string]: TransactionStripped } } => { if (isMempoolState(change)) { const txMap = {}; change.transactions.forEach(tx => { txMap[tx.txid] = tx; }); - return txMap; + this.mempoolBlockState = { + block: change.block, + transactions: txMap + }; + return this.mempoolBlockState; } else { change.added.forEach(tx => { - transactions[tx.txid] = tx; + acc.transactions[tx.txid] = tx; }); change.removed.forEach(txid => { - delete transactions[txid]; + delete acc.transactions[txid]; }); change.changed.forEach(tx => { - if (transactions[tx.txid]) { - transactions[tx.txid].rate = tx.rate; - transactions[tx.txid].acc = tx.acc; + if (acc.transactions[tx.txid]) { + acc.transactions[tx.txid].rate = tx.rate; + acc.transactions[tx.txid].acc = tx.acc; } }); - return transactions; + this.mempoolBlockState = { + block: change.block, + transactions: acc.transactions + }; + return this.mempoolBlockState; } - }, {})); + }, {}), + share() + ); + this.liveMempoolBlockTransactions$.subscribe(); // Emits the full list of pending accelerations each time it changes this.liveAccelerations$ = this.accelerations$.pipe( diff --git a/frontend/src/app/services/websocket.service.ts b/frontend/src/app/services/websocket.service.ts index fd67ddb2e..39e9d1af3 100644 --- a/frontend/src/app/services/websocket.service.ts +++ b/frontend/src/app/services/websocket.service.ts @@ -35,6 +35,7 @@ export class WebsocketService { private isTrackingAddresses: string[] | false = false; private isTrackingAccelerations: boolean = false; private trackingMempoolBlock: number; + private stoppingTrackMempoolBlock: any | null = null; private latestGitCommit = ''; private onlineCheckTimeout: number; private onlineCheckTimeoutTwo: number; @@ -203,19 +204,31 @@ export class WebsocketService { this.websocketSubject.next({ 'track-asset': 'stop' }); } - startTrackMempoolBlock(block: number, force: boolean = false) { + startTrackMempoolBlock(block: number, force: boolean = false): boolean { + if (this.stoppingTrackMempoolBlock) { + clearTimeout(this.stoppingTrackMempoolBlock); + } // skip duplicate tracking requests if (force || this.trackingMempoolBlock !== block) { this.websocketSubject.next({ 'track-mempool-block': block }); this.isTrackingMempoolBlock = true; this.trackingMempoolBlock = block; + return true; } + return false; } - stopTrackMempoolBlock() { - this.websocketSubject.next({ 'track-mempool-block': -1 }); + stopTrackMempoolBlock(): void { + if (this.stoppingTrackMempoolBlock) { + clearTimeout(this.stoppingTrackMempoolBlock); + } this.isTrackingMempoolBlock = false; - this.trackingMempoolBlock = null; + this.stoppingTrackMempoolBlock = setTimeout(() => { + this.stoppingTrackMempoolBlock = null; + this.websocketSubject.next({ 'track-mempool-block': -1 }); + this.trackingMempoolBlock = null; + this.stateService.mempoolBlockState = null; + }, 2000); } startTrackRbf(mode: 'all' | 'fullRbf') { @@ -424,6 +437,7 @@ export class WebsocketService { if (response['projected-block-transactions'].blockTransactions) { this.stateService.mempoolSequence = response['projected-block-transactions'].sequence; this.stateService.mempoolBlockUpdate$.next({ + block: this.trackingMempoolBlock, transactions: response['projected-block-transactions'].blockTransactions.map(uncompressTx), }); } else if (response['projected-block-transactions'].delta) { @@ -432,7 +446,7 @@ export class WebsocketService { this.startTrackMempoolBlock(this.trackingMempoolBlock, true); } else { this.stateService.mempoolSequence = response['projected-block-transactions'].sequence; - this.stateService.mempoolBlockUpdate$.next(uncompressDeltaChange(response['projected-block-transactions'].delta)); + this.stateService.mempoolBlockUpdate$.next(uncompressDeltaChange(this.trackingMempoolBlock, response['projected-block-transactions'].delta)); } } } diff --git a/frontend/src/app/shared/common.utils.ts b/frontend/src/app/shared/common.utils.ts index 697b11b5e..8c69c2319 100644 --- a/frontend/src/app/shared/common.utils.ts +++ b/frontend/src/app/shared/common.utils.ts @@ -170,8 +170,9 @@ export function uncompressTx(tx: TransactionCompressed): TransactionStripped { }; } -export function uncompressDeltaChange(delta: MempoolBlockDeltaCompressed): MempoolBlockDelta { +export function uncompressDeltaChange(block: number, delta: MempoolBlockDeltaCompressed): MempoolBlockDelta { return { + block, added: delta.added.map(uncompressTx), removed: delta.removed, changed: delta.changed.map(tx => ({