From 581eeaca19ebe53aaf619f595e34fa01831d4386 Mon Sep 17 00:00:00 2001 From: rodribp Date: Thu, 16 Jul 2026 01:37:49 -0600 Subject: [PATCH] fix: empty incoming transactions chart --- backend/src/api/statistics/statistics-api.ts | 22 +++--- .../src/api/statistics/statistics.routes.ts | 6 +- .../custom-dashboard.component.html | 4 +- .../custom-dashboard.component.ts | 28 +++++-- ...incoming-transactions-graph.component.html | 2 +- .../incoming-transactions-graph.component.ts | 37 +++++++-- .../mempool-graph.component.html | 2 +- .../mempool-graph/mempool-graph.component.ts | 28 ++++++- .../statistics/statistics.component.html | 8 +- .../statistics/statistics.component.ts | 76 +++++++++++++------ .../app/dashboard/dashboard.component.html | 4 +- .../src/app/dashboard/dashboard.component.ts | 31 ++++++-- 12 files changed, 182 insertions(+), 66 deletions(-) diff --git a/backend/src/api/statistics/statistics-api.ts b/backend/src/api/statistics/statistics-api.ts index 5a3222e82..4bf86ff74 100644 --- a/backend/src/api/statistics/statistics-api.ts +++ b/backend/src/api/statistics/statistics-api.ts @@ -295,7 +295,7 @@ class StatisticsApi { return this.mapStatisticToOptimizedStatistic(rows as Statistic[]); } catch (e) { logger.err('$list2H() error' + (e instanceof Error ? e.message : e)); - return []; + throw e; } } @@ -306,7 +306,7 @@ class StatisticsApi { return this.mapStatisticToOptimizedStatistic(rows as Statistic[]); } catch (e) { logger.err('$list24h() error' + (e instanceof Error ? e.message : e)); - return []; + throw e; } } @@ -317,7 +317,7 @@ class StatisticsApi { return this.mapStatisticToOptimizedStatistic(rows as Statistic[]); } catch (e) { logger.err('$list1W() error' + (e instanceof Error ? e.message : e)); - return []; + throw e; } } @@ -328,7 +328,7 @@ class StatisticsApi { return this.mapStatisticToOptimizedStatistic(rows as Statistic[]); } catch (e) { logger.err('$list1M() error' + (e instanceof Error ? e.message : e)); - return []; + throw e; } } @@ -339,7 +339,7 @@ class StatisticsApi { return this.mapStatisticToOptimizedStatistic(rows as Statistic[]); } catch (e) { logger.err('$list3M() error' + (e instanceof Error ? e.message : e)); - return []; + throw e; } } @@ -350,7 +350,7 @@ class StatisticsApi { return this.mapStatisticToOptimizedStatistic(rows as Statistic[]); } catch (e) { logger.err('$list6M() error' + (e instanceof Error ? e.message : e)); - return []; + throw e; } } @@ -361,7 +361,7 @@ class StatisticsApi { return this.mapStatisticToOptimizedStatistic(rows as Statistic[]); } catch (e) { logger.err('$list1Y() error' + (e instanceof Error ? e.message : e)); - return []; + throw e; } } @@ -372,7 +372,7 @@ class StatisticsApi { return this.mapStatisticToOptimizedStatistic(rows as Statistic[]); } catch (e) { logger.err('$list2Y() error' + (e instanceof Error ? e.message : e)); - return []; + throw e; } } @@ -383,7 +383,7 @@ class StatisticsApi { return this.mapStatisticToOptimizedStatistic(rows as Statistic[]); } catch (e) { logger.err('$list3Y() error' + (e instanceof Error ? e.message : e)); - return []; + throw e; } } @@ -394,7 +394,7 @@ class StatisticsApi { return this.mapStatisticToOptimizedStatistic(rows as Statistic[]); } catch (e) { logger.err('$list4Y() error' + (e instanceof Error ? e.message : e)); - return []; + throw e; } } @@ -405,7 +405,7 @@ class StatisticsApi { return this.mapStatisticToOptimizedStatistic(rows as Statistic[]); } catch (e) { logger.err('$listAll() error' + (e instanceof Error ? e.message : e)); - return []; + throw e; } } diff --git a/backend/src/api/statistics/statistics.routes.ts b/backend/src/api/statistics/statistics.routes.ts index ec05bf032..52f16ab2c 100644 --- a/backend/src/api/statistics/statistics.routes.ts +++ b/backend/src/api/statistics/statistics.routes.ts @@ -22,7 +22,6 @@ class StatisticsRoutes { private async $getStatisticsByTime(time: '2h' | '24h' | '1w' | '1m' | '3m' | '6m' | '1y' | '2y' | '3y' | '4y' | 'all', req: Request, res: Response) { res.header('Pragma', 'public'); res.header('Cache-control', 'public'); - res.setHeader('Expires', new Date(Date.now() + 1000 * 300).toUTCString()); try { let result; @@ -63,6 +62,11 @@ class StatisticsRoutes { res.setHeader('Expires', new Date(Date.now() + 1000 * 30).toUTCString()); break; } + + if (!res.hasHeader('Expires')) { + res.setHeader('Expires', new Date(Date.now() + 1000 * 300).toUTCString()); + } + res.json(result); } catch (e) { handleError(req, res, 500, 'Failed to get statistics'); diff --git a/frontend/src/app/components/custom-dashboard/custom-dashboard.component.html b/frontend/src/app/components/custom-dashboard/custom-dashboard.component.html index e54603789..a5fba287f 100644 --- a/frontend/src/app/components/custom-dashboard/custom-dashboard.component.html +++ b/frontend/src/app/components/custom-dashboard/custom-dashboard.component.html @@ -55,13 +55,15 @@
Incoming Transactions
-
+
diff --git a/frontend/src/app/components/custom-dashboard/custom-dashboard.component.ts b/frontend/src/app/components/custom-dashboard/custom-dashboard.component.ts index 62576e399..215177d70 100644 --- a/frontend/src/app/components/custom-dashboard/custom-dashboard.component.ts +++ b/frontend/src/app/components/custom-dashboard/custom-dashboard.component.ts @@ -1,6 +1,6 @@ import { AfterViewInit, ChangeDetectionStrategy, ChangeDetectorRef, Component, HostListener, Inject, OnDestroy, OnInit, PLATFORM_ID } from '@angular/core'; import { combineLatest, merge, Observable, of, Subject, Subscription } from 'rxjs'; -import { catchError, filter, map, scan, shareReplay, startWith, switchMap, tap } from 'rxjs/operators'; +import { catchError, filter, map, retry, scan, shareReplay, startWith, switchMap, tap } from 'rxjs/operators'; import { BlockExtended, OptimizedMempoolStats, TransactionStripped } from '@interfaces/node-api.interface'; import { MempoolInfo, ReplacementInfo } from '@interfaces/websocket.interface'; import { ApiService } from '@app/services/api.service'; @@ -47,10 +47,13 @@ export class CustomDashboardComponent implements OnInit, OnDestroy, AfterViewIni replacements$: Observable; latestBlockHeight: number; mempoolTransactionsWeightPerSecondData: any; - mempoolStats$: Observable; + mempoolStats: MempoolStatsData; + mempoolStatsError: any; + isMempoolStatsLoading: boolean = true; transactionsWeightPerSecondOptions: any; isLoadingWebSocket$: Observable; isLoad: boolean = true; + mempoolStatsSubscription: Subscription; filterSubscription: Subscription; mempoolInfoSubscription: Subscription; currencySubscription: Subscription; @@ -100,6 +103,7 @@ export class CustomDashboardComponent implements OnInit, OnDestroy, AfterViewIni } ngOnDestroy(): void { + this.mempoolStatsSubscription.unsubscribe(); this.filterSubscription.unsubscribe(); this.mempoolInfoSubscription.unsubscribe(); this.currencySubscription.unsubscribe(); @@ -225,13 +229,19 @@ export class CustomDashboardComponent implements OnInit, OnDestroy, AfterViewIni this.replacements$ = this.stateService.rbfLatestSummary$; - this.mempoolStats$ = this.stateService.connectionState$ + this.mempoolStatsSubscription = this.stateService.connectionState$ .pipe( filter((state) => state === 2), + tap(() => { + this.isMempoolStatsLoading = true; + this.mempoolStatsError = null; + }), switchMap(() => this.apiService.list2HStatistics$().pipe( - catchError((e) => { + retry({ count: 3, delay: 1000 }), + catchError((err) => { + this.mempoolStatsError = err; return of(null); - }) + }), )), switchMap((mempoolStats) => { return merge( @@ -240,6 +250,7 @@ export class CustomDashboardComponent implements OnInit, OnDestroy, AfterViewIni scan((acc, stats) => { const now = Date.now() / 1000; const start = now - (2 * 60 * 60); + this.mempoolStatsError = null; acc.unshift(stats); acc = acc.filter(p => p.added >= start); return acc; @@ -258,8 +269,11 @@ export class CustomDashboardComponent implements OnInit, OnDestroy, AfterViewIni return null; } }), - shareReplay(1), - ); + ).subscribe((mempoolStats) => { + this.isMempoolStatsLoading = false; + this.mempoolStats = mempoolStats; + this.cd.markForCheck(); + }); this.currencySubscription = this.stateService.fiatCurrency$.subscribe((fiat) => { this.currency = fiat; diff --git a/frontend/src/app/components/incoming-transactions-graph/incoming-transactions-graph.component.html b/frontend/src/app/components/incoming-transactions-graph/incoming-transactions-graph.component.html index 2bbb3080e..10f0706ae 100644 --- a/frontend/src/app/components/incoming-transactions-graph/incoming-transactions-graph.component.html +++ b/frontend/src/app/components/incoming-transactions-graph/incoming-transactions-graph.component.html @@ -1,6 +1,6 @@
-
+
\ No newline at end of file diff --git a/frontend/src/app/components/incoming-transactions-graph/incoming-transactions-graph.component.ts b/frontend/src/app/components/incoming-transactions-graph/incoming-transactions-graph.component.ts index dfec9f355..aceb5b99f 100644 --- a/frontend/src/app/components/incoming-transactions-graph/incoming-transactions-graph.component.ts +++ b/frontend/src/app/components/incoming-transactions-graph/incoming-transactions-graph.component.ts @@ -33,7 +33,8 @@ export class IncomingTransactionsGraphComponent implements OnInit, OnChanges, On @Input() template: ('widget' | 'advanced') = 'widget'; @Input() windowPreferenceOverride: string; @Input() outlierCappingEnabled: boolean = false; - @Input() isLoading: boolean; + @Input() isLoading: boolean = true; + @Input() error: any; mempoolStatsChartOption: EChartsOption = {}; mempoolStatsChartInitOption = { @@ -55,14 +56,18 @@ export class IncomingTransactionsGraphComponent implements OnInit, OnChanges, On ngOnInit() { this.rateUnitSub = this.stateService.rateUnits$.subscribe(rateUnits => { this.weightMode = rateUnits === 'wu'; - if (this.data) { + if (this.data && !this.error) { this.mountChart(); } }); } ngOnChanges(): void { - if (!this.data) { + if (this.isLoading) { + return; + } + if (!this.data || (this.data && this.data.series?.[0]?.length === 0) || this.error) { + this.mountMessageOnChart(); return; } this.windowPreference = (this.windowPreferenceOverride ? this.windowPreferenceOverride : this.storageService.getValue('graphWindowPreference')) || '2h'; @@ -259,8 +264,8 @@ export class IncomingTransactionsGraphComponent implements OnInit, OnChanges, On ], yAxis: { max: (value): number => { - let cappedMax = value.max; - if (this.outlierCappingEnabled && value.max >= (this.medianVbytesPerSecond * OUTLIERS_MEDIAN_MULTIPLIER)) { + let cappedMax = Number.isFinite(value.max) ? value.max : 0; + if (this.outlierCappingEnabled && cappedMax >= (this.medianVbytesPerSecond * OUTLIERS_MEDIAN_MULTIPLIER)) { cappedMax = Math.round(this.medianVbytesPerSecond * OUTLIERS_MEDIAN_MULTIPLIER); } // always show the clearing rate line, plus a small margin @@ -322,6 +327,28 @@ export class IncomingTransactionsGraphComponent implements OnInit, OnChanges, On }; } + mountMessageOnChart(): void { + let errorMessage = $localize`:error.general-loading-data:Error loading data.`; + if (!this.error && this.data && this.data.series[0]?.length === 0) { // Empty array + errorMessage = $localize`No data to display yet. Try again later.`; + } + if (this.template === 'widget') { + errorMessage = $localize`Awaiting live update...`; + } + + this.mempoolStatsChartOption = { + title: { + textStyle: { + color: 'grey', + fontSize: 15 + }, + text: errorMessage, + left: 'center', + top: 'center', + }, + }; + } + onChartInit(ec) { this.chartInstance = ec; } diff --git a/frontend/src/app/components/mempool-graph/mempool-graph.component.html b/frontend/src/app/components/mempool-graph/mempool-graph.component.html index 155d73ecf..7d6c9eeea 100644 --- a/frontend/src/app/components/mempool-graph/mempool-graph.component.html +++ b/frontend/src/app/components/mempool-graph/mempool-graph.component.html @@ -1,5 +1,5 @@
-
+
\ No newline at end of file diff --git a/frontend/src/app/components/mempool-graph/mempool-graph.component.ts b/frontend/src/app/components/mempool-graph/mempool-graph.component.ts index 1409ea8bb..583f07779 100644 --- a/frontend/src/app/components/mempool-graph/mempool-graph.component.ts +++ b/frontend/src/app/components/mempool-graph/mempool-graph.component.ts @@ -25,7 +25,7 @@ import { download, formatterXAxis, formatterXAxisLabel } from '@app/shared/graph changeDetection: ChangeDetectionStrategy.OnPush, }) export class MempoolGraphComponent implements OnInit, OnChanges { - @Input() data: any[]; + @Input() data: OptimizedMempoolStats[]; @Input() filterSize = 100000; @Input() limitFilterFee = 1; @Input() hideCount: boolean = true; @@ -37,6 +37,7 @@ export class MempoolGraphComponent implements OnInit, OnChanges { @Input() showZoom = true; @Input() windowPreferenceOverride: string; @Input() isLoading: boolean; + @Input() error: any; mempoolVsizeFeesData: any; mempoolVsizeFeesOptions: EChartsOption; @@ -72,7 +73,11 @@ export class MempoolGraphComponent implements OnInit, OnChanges { } ngOnChanges(changes) { - if (!this.data) { + if (this.isLoading) { + return; + } + if (!this.data || (this.data && this.data.length === 0) || this.error) { + this.mountMessageOnChart(); return; } this.isWidget = this.template === 'widget'; @@ -455,6 +460,25 @@ export class MempoolGraphComponent implements OnInit, OnChanges { }; } + mountMessageOnChart() { + let errorMessage = $localize`:error.general-loading-data:Error loading data.`; + if (!this.error && this.data && this.data.length === 0) { // Empty array + errorMessage = $localize`No data to display yet. Try again later.`; + } + + this.mempoolVsizeFeesOptions = { + title: { + textStyle: { + color: 'grey', + fontSize: 15 + }, + text: errorMessage, + left: 'center', + top: 'center', + }, + }; + } + getTotalValues = (values: any) => { let totalValueTemp = 0; const totalValueArray = []; diff --git a/frontend/src/app/components/statistics/statistics.component.html b/frontend/src/app/components/statistics/statistics.component.html index acd7a64f1..98116df2d 100644 --- a/frontend/src/app/components/statistics/statistics.component.html +++ b/frontend/src/app/components/statistics/statistics.component.html @@ -5,7 +5,7 @@
Mempool by vBytes (sat/vByte) -
@@ -83,7 +83,7 @@
+ [data]="mempoolStats" [isLoading]="isLoading" [error]="statsError">
@@ -95,7 +95,7 @@
Transaction vBytes per second (vB/s) -
@@ -111,7 +111,7 @@
+ [data]="mempoolTransactionsWeightPerSecondData" [outlierCappingEnabled]="outlierCappingEnabled" [isLoading]="isLoading" [error]="statsError">
diff --git a/frontend/src/app/components/statistics/statistics.component.ts b/frontend/src/app/components/statistics/statistics.component.ts index af486970e..87601dee7 100644 --- a/frontend/src/app/components/statistics/statistics.component.ts +++ b/frontend/src/app/components/statistics/statistics.component.ts @@ -1,8 +1,8 @@ import { Component, OnInit, LOCALE_ID, Inject, ViewChild, ElementRef } from '@angular/core'; import { ActivatedRoute } from '@angular/router'; import { UntypedFormGroup, UntypedFormBuilder } from '@angular/forms'; -import { of, merge} from 'rxjs'; -import { switchMap } from 'rxjs/operators'; +import { of, merge, Observable} from 'rxjs'; +import { catchError, retry, scan, switchMap, tap } from 'rxjs/operators'; import { OptimizedMempoolStats } from '@interfaces/node-api.interface'; import { WebsocketService } from '@app/services/websocket.service'; @@ -36,7 +36,7 @@ export class StatisticsComponent implements OnInit { maxFeeIndex: number; dropDownOpen = false; outlierCappingEnabled = false; - mempoolStats: OptimizedMempoolStats[] = []; + mempoolStats: OptimizedMempoolStats[]; mempoolVsizeFeesData: any; mempoolUnconfirmedTransactionsData: any; @@ -48,6 +48,7 @@ export class StatisticsComponent implements OnInit { feeLevelDropdownData = []; timespan = ''; titleCount = $localize`Count`; + statsError: any; constructor( @Inject(LOCALE_ID) private locale: string, @@ -88,58 +89,75 @@ export class StatisticsComponent implements OnInit { this.radioGroupForm.controls.dateSpan.valueChanges ) .pipe( + tap(() => { + this.statsError = null; + this.isLoading = true; + }), switchMap(() => { this.timespan = this.radioGroupForm.controls.dateSpan.value; - this.isLoading = true; if (this.radioGroupForm.controls.dateSpan.value === '2h') { this.websocketService.want(['blocks', 'live-2h-chart']); - return this.apiService.list2HStatistics$(); + return this.errorHandlerPipe(this.apiService.list2HStatistics$()); } this.websocketService.want(['blocks']); if (this.radioGroupForm.controls.dateSpan.value === '24h') { - return this.apiService.list24HStatistics$(); + return this.errorHandlerPipe(this.apiService.list24HStatistics$()); } if (this.radioGroupForm.controls.dateSpan.value === '1w') { - return this.apiService.list1WStatistics$(); + return this.errorHandlerPipe(this.apiService.list1WStatistics$()); } if (this.radioGroupForm.controls.dateSpan.value === '1m') { - return this.apiService.list1MStatistics$(); + return this.errorHandlerPipe(this.apiService.list1MStatistics$()); } if (this.radioGroupForm.controls.dateSpan.value === '3m') { - return this.apiService.list3MStatistics$(); + return this.errorHandlerPipe(this.apiService.list3MStatistics$()); } if (this.radioGroupForm.controls.dateSpan.value === '6m') { - return this.apiService.list6MStatistics$(); + return this.errorHandlerPipe(this.apiService.list6MStatistics$()); } if (this.radioGroupForm.controls.dateSpan.value === '1y') { - return this.apiService.list1YStatistics$(); + return this.errorHandlerPipe(this.apiService.list1YStatistics$()); } if (this.radioGroupForm.controls.dateSpan.value === '2y') { - return this.apiService.list2YStatistics$(); + return this.errorHandlerPipe(this.apiService.list2YStatistics$()); } if (this.radioGroupForm.controls.dateSpan.value === '3y') { - return this.apiService.list3YStatistics$(); + return this.errorHandlerPipe(this.apiService.list3YStatistics$()); } if (this.radioGroupForm.controls.dateSpan.value === '4y') { - return this.apiService.list4YStatistics$(); + return this.errorHandlerPipe(this.apiService.list4YStatistics$()); } if (this.radioGroupForm.controls.dateSpan.value === 'all') { - return this.apiService.listAllTimeStatistics$(); + return this.errorHandlerPipe(this.apiService.listAllTimeStatistics$()); } - }) + }), + switchMap((mempoolStats) => { + if (this.radioGroupForm.controls.dateSpan.value === '2h') { + return merge( + this.stateService.live2Chart$.pipe( + scan((acc, stats) => { + const now = Date.now() / 1000; + const start = now - (2 * 60 * 60); + this.statsError = null; + acc.unshift(stats); + acc = acc.filter(p => p.added >= start); + return acc; + }, (mempoolStats || [])) + ), + of(mempoolStats) + ); + } else { + return of(mempoolStats); + } + }), ) .subscribe((mempoolStats: any) => { this.mempoolStats = mempoolStats; - this.handleNewMempoolData(this.mempoolStats.concat([])); + if (this.mempoolStats) { + this.handleNewMempoolData(this.mempoolStats.concat([])); + } this.isLoading = false; }); - - this.stateService.live2Chart$ - .subscribe((mempoolStats) => { - this.mempoolStats.unshift(mempoolStats); - this.mempoolStats = this.mempoolStats.slice(0, this.mempoolStats.length - 1); - this.handleNewMempoolData(this.mempoolStats.concat([])); - }); } handleNewMempoolData(mempoolStats: OptimizedMempoolStats[]) { @@ -171,6 +189,16 @@ export class StatisticsComponent implements OnInit { document.location.reload(); } + errorHandlerPipe(obs: Observable) { + return obs.pipe( + retry({ count: 3, delay: 1000 }), + catchError((err) => { + this.statsError = err; + return of(null); + }), + ); + } + setFeeLevelDropdownData() { let _feeLevels = feeLevels; let _chartColors = chartColors; diff --git a/frontend/src/app/dashboard/dashboard.component.html b/frontend/src/app/dashboard/dashboard.component.html index 90454d9a3..e485bcf9b 100644 --- a/frontend/src/app/dashboard/dashboard.component.html +++ b/frontend/src/app/dashboard/dashboard.component.html @@ -48,13 +48,15 @@
Incoming Transactions
-
+
diff --git a/frontend/src/app/dashboard/dashboard.component.ts b/frontend/src/app/dashboard/dashboard.component.ts index 61a2ce3f0..bad66ba90 100644 --- a/frontend/src/app/dashboard/dashboard.component.ts +++ b/frontend/src/app/dashboard/dashboard.component.ts @@ -1,6 +1,6 @@ -import { AfterViewInit, ChangeDetectionStrategy, Component, HostListener, Inject, OnDestroy, OnInit, PLATFORM_ID } from '@angular/core'; +import { AfterViewInit, ChangeDetectionStrategy, ChangeDetectorRef, Component, HostListener, Inject, OnDestroy, OnInit, PLATFORM_ID } from '@angular/core'; import { combineLatest, EMPTY, fromEvent, interval, merge, Observable, of, Subject, Subscription, timer } from 'rxjs'; -import { catchError, delayWhen, distinctUntilChanged, filter, map, scan, share, shareReplay, startWith, switchMap, takeUntil, tap, throttleTime } from 'rxjs/operators'; +import { catchError, delayWhen, distinctUntilChanged, filter, map, retry, scan, share, shareReplay, startWith, switchMap, takeUntil, tap, throttleTime } from 'rxjs/operators'; import { AuditStatus, BlockExtended, CurrentPegs, FederationAddress, FederationUtxo, OptimizedMempoolStats, RecentPeg } from '@interfaces/node-api.interface'; import { MempoolInfo, ReplacementInfo } from '@interfaces/websocket.interface'; import { ApiService } from '@app/services/api.service'; @@ -44,7 +44,9 @@ export class DashboardComponent implements OnInit, OnDestroy, AfterViewInit { replacements$: Observable; latestBlockHeight: number; mempoolTransactionsWeightPerSecondData: any; - mempoolStats$: Observable; + mempoolStats: MempoolStatsData; + mempoolStatsError: any; + isMempoolStatsLoading = true; transactionsWeightPerSecondOptions: any; isLoadingWebSocket$: Observable; liquidPegsMonth$: Observable; @@ -59,6 +61,7 @@ export class DashboardComponent implements OnInit, OnDestroy, AfterViewInit { emergencySpentUtxosStats$: Observable; fullHistory$: Observable; isLoad: boolean = true; + mempoolStatsSubscription: Subscription; filterSubscription: Subscription; mempoolInfoSubscription: Subscription; currencySubscription: Subscription; @@ -90,6 +93,7 @@ export class DashboardComponent implements OnInit, OnDestroy, AfterViewInit { private websocketService: WebsocketService, private seoService: SeoService, @Inject(PLATFORM_ID) private platformId: object, + private cd: ChangeDetectorRef, ) { this.webGlEnabled = this.stateService.isBrowser && detectWebGL(); } @@ -99,6 +103,7 @@ export class DashboardComponent implements OnInit, OnDestroy, AfterViewInit { } ngOnDestroy(): void { + this.mempoolStatsSubscription.unsubscribe(); this.filterSubscription.unsubscribe(); this.mempoolInfoSubscription.unsubscribe(); this.currencySubscription.unsubscribe(); @@ -213,13 +218,19 @@ export class DashboardComponent implements OnInit, OnDestroy, AfterViewInit { this.replacements$ = this.stateService.rbfLatestSummary$; - this.mempoolStats$ = this.stateService.connectionState$ + this.mempoolStatsSubscription = this.stateService.connectionState$ .pipe( filter((state) => state === 2), + tap(() => { + this.isMempoolStatsLoading = true; + this.mempoolStatsError = null; + }), switchMap(() => this.apiService.list2HStatistics$().pipe( - catchError((e) => { + retry({ count: 3, delay: 1000 }), + catchError((err) => { + this.mempoolStatsError = err; return of(null); - }) + }), )), switchMap((mempoolStats) => { return merge( @@ -228,6 +239,7 @@ export class DashboardComponent implements OnInit, OnDestroy, AfterViewInit { scan((acc, stats) => { const now = Date.now() / 1000; const start = now - (2 * 60 * 60); + this.mempoolStatsError = null; acc.unshift(stats); acc = acc.filter(p => p.added >= start); return acc; @@ -246,8 +258,11 @@ export class DashboardComponent implements OnInit, OnDestroy, AfterViewInit { return null; } }), - shareReplay(1), - ); + ).subscribe((mempoolStats) => { + this.isMempoolStatsLoading = false; + this.mempoolStats = mempoolStats; + this.cd.markForCheck(); + }); if (this.stateService.network === 'liquid') { this.auditStatus$ = this.stateService.blocks$.pipe(