Merge pull request #5035 from mempool/mononaut/accelerator-websocket

Replace acceleration API polling with websocket
This commit is contained in:
softsimon 2024-05-06 20:57:41 +07:00 committed by GitHub
commit feab4d2a51
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
8 changed files with 136 additions and 32 deletions

View File

@ -404,6 +404,10 @@ class Mempool {
const newAccelerationMap: { [txid: string]: Acceleration } = {}; const newAccelerationMap: { [txid: string]: Acceleration } = {};
for (const acceleration of newAccelerations) { for (const acceleration of newAccelerations) {
// skip transactions we don't know about
if (!this.mempoolCache[acceleration.txid]) {
continue;
}
newAccelerationMap[acceleration.txid] = acceleration; newAccelerationMap[acceleration.txid] = acceleration;
if (this.accelerations[acceleration.txid] == null) { if (this.accelerations[acceleration.txid] == null) {
// new acceleration // new acceleration

View File

@ -347,6 +347,17 @@ class WebsocketHandler {
} }
} }
if (parsedMessage && parsedMessage['track-accelerations'] != null) {
if (parsedMessage['track-accelerations']) {
client['track-accelerations'] = true;
response['accelerations'] = JSON.stringify({
accelerations: Object.values(memPool.getAccelerations()),
});
} else {
client['track-accelerations'] = false;
}
}
if (parsedMessage.action === 'init') { if (parsedMessage.action === 'init') {
if (!this.socketData['blocks']?.length || !this.socketData['da'] || !this.socketData['backendInfo'] || !this.socketData['conversions']) { if (!this.socketData['blocks']?.length || !this.socketData['da'] || !this.socketData['backendInfo'] || !this.socketData['conversions']) {
this.updateSocketData(); this.updateSocketData();
@ -537,6 +548,7 @@ class WebsocketHandler {
const vBytesPerSecond = memPool.getVBytesPerSecond(); const vBytesPerSecond = memPool.getVBytesPerSecond();
const rbfTransactions = Common.findRbfTransactions(newTransactions, deletedTransactions); const rbfTransactions = Common.findRbfTransactions(newTransactions, deletedTransactions);
const da = difficultyAdjustment.getDifficultyAdjustment(); const da = difficultyAdjustment.getDifficultyAdjustment();
const accelerations = memPool.getAccelerations();
memPool.handleRbfTransactions(rbfTransactions); memPool.handleRbfTransactions(rbfTransactions);
const rbfChanges = rbfCache.getRbfChanges(); const rbfChanges = rbfCache.getRbfChanges();
let rbfReplacements; let rbfReplacements;
@ -644,6 +656,12 @@ class WebsocketHandler {
const addressCache = this.makeAddressCache(newTransactions); const addressCache = this.makeAddressCache(newTransactions);
const removedAddressCache = this.makeAddressCache(deletedTransactions); const removedAddressCache = this.makeAddressCache(deletedTransactions);
// pre-compute acceleration delta
const accelerationUpdate = {
added: accelerationDelta.map(txid => accelerations[txid]).filter(acc => acc != null),
removed: accelerationDelta.filter(txid => !accelerations[txid]),
};
// TODO - Fix indentation after PR is merged // TODO - Fix indentation after PR is merged
for (const server of this.webSocketServers) { for (const server of this.webSocketServers) {
server.clients.forEach(async (client) => { server.clients.forEach(async (client) => {
@ -891,6 +909,10 @@ class WebsocketHandler {
response['mempool-transactions'] = getCachedResponse('mempool-transactions', mempoolDelta); response['mempool-transactions'] = getCachedResponse('mempool-transactions', mempoolDelta);
} }
if (client['track-accelerations'] && (accelerationUpdate.added.length || accelerationUpdate.removed.length)) {
response['accelerations'] = getCachedResponse('accelerations', accelerationUpdate);
}
if (Object.keys(response).length) { if (Object.keys(response).length) {
client.send(this.serializeResponse(response)); client.send(this.serializeResponse(response));
} }

View File

@ -1,5 +1,5 @@
import { Component, OnInit, ChangeDetectionStrategy, Input, ChangeDetectorRef } from '@angular/core'; import { Component, OnInit, ChangeDetectionStrategy, Input, ChangeDetectorRef, OnDestroy } from '@angular/core';
import { combineLatest, BehaviorSubject, Observable, catchError, of, switchMap, tap } from 'rxjs'; import { BehaviorSubject, Observable, catchError, of, switchMap, tap } from 'rxjs';
import { Acceleration, BlockExtended } from '../../../interfaces/node-api.interface'; import { Acceleration, BlockExtended } from '../../../interfaces/node-api.interface';
import { StateService } from '../../../services/state.service'; import { StateService } from '../../../services/state.service';
import { WebsocketService } from '../../../services/websocket.service'; import { WebsocketService } from '../../../services/websocket.service';
@ -11,7 +11,7 @@ import { ServicesApiServices } from '../../../services/services-api.service';
styleUrls: ['./accelerations-list.component.scss'], styleUrls: ['./accelerations-list.component.scss'],
changeDetection: ChangeDetectionStrategy.OnPush, changeDetection: ChangeDetectionStrategy.OnPush,
}) })
export class AccelerationsListComponent implements OnInit { export class AccelerationsListComponent implements OnInit, OnDestroy {
@Input() widget: boolean = false; @Input() widget: boolean = false;
@Input() pending: boolean = false; @Input() pending: boolean = false;
@Input() accelerations$: Observable<Acceleration[]>; @Input() accelerations$: Observable<Acceleration[]>;
@ -44,7 +44,10 @@ export class AccelerationsListComponent implements OnInit {
this.accelerationList$ = this.pageSubject.pipe( this.accelerationList$ = this.pageSubject.pipe(
switchMap((page) => { switchMap((page) => {
const accelerationObservable$ = this.accelerations$ || (this.pending ? this.servicesApiService.getAccelerations$() : this.servicesApiService.getAccelerationHistoryObserveResponse$({ page: page })); const accelerationObservable$ = this.accelerations$ || (this.pending ? this.stateService.liveAccelerations$ : this.servicesApiService.getAccelerationHistoryObserveResponse$({ page: page }));
if (!this.accelerations$ && this.pending) {
this.websocketService.ensureTrackAccelerations();
}
return accelerationObservable$.pipe( return accelerationObservable$.pipe(
switchMap(response => { switchMap(response => {
let accelerations = response; let accelerations = response;
@ -85,4 +88,8 @@ export class AccelerationsListComponent implements OnInit {
trackByBlock(index: number, block: BlockExtended): number { trackByBlock(index: number, block: BlockExtended): number {
return block.height; return block.height;
} }
ngOnDestroy(): void {
this.websocketService.stopTrackAccelerations();
}
} }

View File

@ -1,10 +1,10 @@
import { ChangeDetectionStrategy, Component, HostListener, Inject, OnInit, PLATFORM_ID } from '@angular/core'; import { ChangeDetectionStrategy, Component, HostListener, Inject, OnDestroy, OnInit, PLATFORM_ID } from '@angular/core';
import { SeoService } from '../../../services/seo.service'; import { SeoService } from '../../../services/seo.service';
import { OpenGraphService } from '../../../services/opengraph.service'; import { OpenGraphService } from '../../../services/opengraph.service';
import { WebsocketService } from '../../../services/websocket.service'; import { WebsocketService } from '../../../services/websocket.service';
import { Acceleration, BlockExtended } from '../../../interfaces/node-api.interface'; import { Acceleration, BlockExtended } from '../../../interfaces/node-api.interface';
import { StateService } from '../../../services/state.service'; import { StateService } from '../../../services/state.service';
import { Observable, catchError, combineLatest, distinctUntilChanged, interval, map, of, share, startWith, switchMap, tap } from 'rxjs'; import { Observable, Subscription, catchError, combineLatest, distinctUntilChanged, map, of, share, switchMap, tap } from 'rxjs';
import { Color } from '../../block-overview-graph/sprite-types'; import { Color } from '../../block-overview-graph/sprite-types';
import { hexToColor } from '../../block-overview-graph/utils'; import { hexToColor } from '../../block-overview-graph/utils';
import TxView from '../../block-overview-graph/tx-view'; import TxView from '../../block-overview-graph/tx-view';
@ -28,7 +28,7 @@ interface AccelerationBlock extends BlockExtended {
styleUrls: ['./accelerator-dashboard.component.scss'], styleUrls: ['./accelerator-dashboard.component.scss'],
changeDetection: ChangeDetectionStrategy.OnPush, changeDetection: ChangeDetectionStrategy.OnPush,
}) })
export class AcceleratorDashboardComponent implements OnInit { export class AcceleratorDashboardComponent implements OnInit, OnDestroy {
blocks$: Observable<AccelerationBlock[]>; blocks$: Observable<AccelerationBlock[]>;
accelerations$: Observable<Acceleration[]>; accelerations$: Observable<Acceleration[]>;
pendingAccelerations$: Observable<Acceleration[]>; pendingAccelerations$: Observable<Acceleration[]>;
@ -39,6 +39,8 @@ export class AcceleratorDashboardComponent implements OnInit {
firstLoad = true; firstLoad = true;
timespan: '3d' | '1w' | '1m' = '1w'; timespan: '3d' | '1w' | '1m' = '1w';
accelerationDeltaSubscription: Subscription;
graphHeight: number = 300; graphHeight: number = 300;
theme: ThemeService; theme: ThemeService;
@ -59,27 +61,28 @@ export class AcceleratorDashboardComponent implements OnInit {
ngOnInit(): void { ngOnInit(): void {
this.onResize(); this.onResize();
this.websocketService.want(['blocks', 'mempool-blocks', 'stats']); this.websocketService.want(['blocks', 'mempool-blocks', 'stats']);
this.websocketService.startTrackAccelerations();
this.pendingAccelerations$ = (this.stateService.isBrowser ? interval(30000) : of(null)).pipe( this.pendingAccelerations$ = this.stateService.liveAccelerations$.pipe(
startWith(true),
switchMap(() => {
return this.serviceApiServices.getAccelerations$().pipe(
catchError(() => {
return of([]);
}),
);
}),
tap(accelerations => {
if (!this.firstLoad && accelerations.some(acc => !this.seen.has(acc.txid))) {
this.audioService.playSound('bright-harmony');
}
for(const acc of accelerations) {
this.seen.add(acc.txid);
}
this.firstLoad = false;
}),
share(), share(),
); );
this.accelerationDeltaSubscription = this.stateService.accelerations$.subscribe((delta) => {
if (!delta.reset) {
let hasNewAcceleration = false;
for (const acc of delta.added) {
if (!this.seen.has(acc.txid)) {
hasNewAcceleration = true;
}
this.seen.add(acc.txid);
}
for (const txid of delta.removed) {
this.seen.delete(txid);
}
if (hasNewAcceleration) {
this.audioService.playSound('bright-harmony');
}
}
});
this.accelerations$ = this.stateService.chainTip$.pipe( this.accelerations$ = this.stateService.chainTip$.pipe(
distinctUntilChanged(), distinctUntilChanged(),
@ -154,6 +157,11 @@ export class AcceleratorDashboardComponent implements OnInit {
return false; return false;
} }
ngOnDestroy(): void {
this.accelerationDeltaSubscription.unsubscribe();
this.websocketService.stopTrackAccelerations();
}
@HostListener('window:resize', ['$event']) @HostListener('window:resize', ['$event'])
onResize(): void { onResize(): void {
if (window.innerWidth >= 992) { if (window.innerWidth >= 992) {

View File

@ -2,7 +2,8 @@ import { ChangeDetectionStrategy, Component, Input, OnInit } from '@angular/core
import { Observable, of } from 'rxjs'; import { Observable, of } from 'rxjs';
import { switchMap } from 'rxjs/operators'; import { switchMap } from 'rxjs/operators';
import { Acceleration } from '../../../interfaces/node-api.interface'; import { Acceleration } from '../../../interfaces/node-api.interface';
import { ServicesApiServices } from '../../../services/services-api.service'; import { StateService } from '../../../services/state.service';
import { WebsocketService } from '../../../services/websocket.service';
@Component({ @Component({
selector: 'app-pending-stats', selector: 'app-pending-stats',
@ -15,11 +16,12 @@ export class PendingStatsComponent implements OnInit {
public accelerationStats$: Observable<any>; public accelerationStats$: Observable<any>;
constructor( constructor(
private servicesApiService: ServicesApiServices, private stateService: StateService,
private websocketService: WebsocketService,
) { } ) { }
ngOnInit(): void { ngOnInit(): void {
this.accelerationStats$ = (this.accelerations$ || this.servicesApiService.getAccelerations$()).pipe( this.accelerationStats$ = (this.accelerations$ || this.stateService.liveAccelerations$).pipe(
switchMap(accelerations => { switchMap(accelerations => {
let totalAccelerations = 0; let totalAccelerations = 0;
let totalFeeDelta = 0; let totalFeeDelta = 0;

View File

@ -1,7 +1,7 @@
import { SafeResourceUrl } from '@angular/platform-browser'; import { SafeResourceUrl } from '@angular/platform-browser';
import { ILoadingIndicators } from '../services/state.service'; import { ILoadingIndicators } from '../services/state.service';
import { Transaction } from './electrs.interface'; import { Transaction } from './electrs.interface';
import { BlockExtended, DifficultyAdjustment, RbfTree, TransactionStripped } from './node-api.interface'; import { Acceleration, BlockExtended, DifficultyAdjustment, RbfTree, TransactionStripped } from './node-api.interface';
export interface WebsocketResponse { export interface WebsocketResponse {
backend?: 'esplora' | 'electrum' | 'none'; backend?: 'esplora' | 'electrum' | 'none';
@ -35,6 +35,7 @@ export interface WebsocketResponse {
'track-mempool-block'?: number; 'track-mempool-block'?: number;
'track-rbf'?: string; 'track-rbf'?: string;
'track-rbf-summary'?: boolean; 'track-rbf-summary'?: boolean;
'track-accelerations'?: boolean;
'watch-mempool'?: boolean; 'watch-mempool'?: boolean;
'refresh-blocks'?: boolean; 'refresh-blocks'?: boolean;
} }
@ -92,6 +93,12 @@ export interface MempoolBlockDeltaCompressed {
changed: MempoolDeltaChange[]; changed: MempoolDeltaChange[];
} }
export interface AccelerationDelta {
added: Acceleration[];
removed: string[];
reset?: boolean;
}
export interface MempoolInfo { export interface MempoolInfo {
loaded: boolean; // (boolean) True if the mempool is fully loaded loaded: boolean; // (boolean) True if the mempool is fully loaded
size: number; // (numeric) Current tx count size: number; // (numeric) Current tx count

View File

@ -1,8 +1,8 @@
import { Inject, Injectable, PLATFORM_ID, LOCALE_ID } from '@angular/core'; import { Inject, Injectable, PLATFORM_ID, LOCALE_ID } from '@angular/core';
import { ReplaySubject, BehaviorSubject, Subject, fromEvent, Observable, merge } from 'rxjs'; import { ReplaySubject, BehaviorSubject, Subject, fromEvent, Observable } from 'rxjs';
import { Transaction } from '../interfaces/electrs.interface'; import { Transaction } from '../interfaces/electrs.interface';
import { HealthCheckHost, IBackendInfo, MempoolBlock, MempoolBlockDelta, MempoolBlockUpdate, MempoolInfo, Recommendedfees, ReplacedTransaction, ReplacementInfo, isMempoolState } from '../interfaces/websocket.interface'; import { AccelerationDelta, HealthCheckHost, IBackendInfo, MempoolBlock, MempoolBlockUpdate, MempoolInfo, Recommendedfees, ReplacedTransaction, ReplacementInfo, isMempoolState } from '../interfaces/websocket.interface';
import { BlockExtended, CpfpInfo, DifficultyAdjustment, MempoolPosition, OptimizedMempoolStats, RbfTree, TransactionStripped } from '../interfaces/node-api.interface'; import { Acceleration, BlockExtended, CpfpInfo, DifficultyAdjustment, MempoolPosition, OptimizedMempoolStats, RbfTree, TransactionStripped } from '../interfaces/node-api.interface';
import { Router, NavigationStart } from '@angular/router'; import { Router, NavigationStart } from '@angular/router';
import { isPlatformBrowser } from '@angular/common'; import { isPlatformBrowser } from '@angular/common';
import { filter, map, scan, shareReplay } from 'rxjs/operators'; import { filter, map, scan, shareReplay } from 'rxjs/operators';
@ -129,6 +129,8 @@ export class StateService {
mempoolBlocks$ = new ReplaySubject<MempoolBlock[]>(1); mempoolBlocks$ = new ReplaySubject<MempoolBlock[]>(1);
mempoolBlockUpdate$ = new Subject<MempoolBlockUpdate>(); mempoolBlockUpdate$ = new Subject<MempoolBlockUpdate>();
liveMempoolBlockTransactions$: Observable<{ [txid: string]: TransactionStripped}>; liveMempoolBlockTransactions$: Observable<{ [txid: string]: TransactionStripped}>;
accelerations$ = new Subject<AccelerationDelta>();
liveAccelerations$: Observable<Acceleration[]>;
txConfirmed$ = new Subject<[string, BlockExtended]>(); txConfirmed$ = new Subject<[string, BlockExtended]>();
txReplaced$ = new Subject<ReplacedTransaction>(); txReplaced$ = new Subject<ReplacedTransaction>();
txRbfInfo$ = new Subject<RbfTree>(); txRbfInfo$ = new Subject<RbfTree>();
@ -238,6 +240,24 @@ export class StateService {
} }
}, {})); }, {}));
// Emits the full list of pending accelerations each time it changes
this.liveAccelerations$ = this.accelerations$.pipe(
scan((accelerations: { [txid: string]: Acceleration }, delta: AccelerationDelta) => {
if (delta.reset) {
accelerations = {};
} else {
for (const txid of delta.removed) {
delete accelerations[txid];
}
}
for (const acc of delta.added) {
accelerations[acc.txid] = acc;
}
return accelerations;
}, {}),
map((accMap) => Object.values(accMap).sort((a,b) => b.added - a.added))
);
this.networkChanged$.subscribe((network) => { this.networkChanged$.subscribe((network) => {
this.transactions$ = new BehaviorSubject<TransactionStripped[]>(null); this.transactions$ = new BehaviorSubject<TransactionStripped[]>(null);
this.blocksSubject$.next([]); this.blocksSubject$.next([]);

View File

@ -33,6 +33,7 @@ export class WebsocketService {
private isTrackingRbfSummary = false; private isTrackingRbfSummary = false;
private isTrackingAddress: string | false = false; private isTrackingAddress: string | false = false;
private isTrackingAddresses: string[] | false = false; private isTrackingAddresses: string[] | false = false;
private isTrackingAccelerations: boolean = false;
private trackingMempoolBlock: number; private trackingMempoolBlock: number;
private latestGitCommit = ''; private latestGitCommit = '';
private onlineCheckTimeout: number; private onlineCheckTimeout: number;
@ -132,6 +133,9 @@ export class WebsocketService {
if (this.isTrackingAddresses) { if (this.isTrackingAddresses) {
this.startTrackAddresses(this.isTrackingAddresses); this.startTrackAddresses(this.isTrackingAddresses);
} }
if (this.isTrackingAccelerations) {
this.startTrackAccelerations();
}
this.stateService.connectionState$.next(2); this.stateService.connectionState$.next(2);
} }
@ -235,6 +239,24 @@ export class WebsocketService {
this.isTrackingRbfSummary = false; this.isTrackingRbfSummary = false;
} }
startTrackAccelerations() {
this.websocketSubject.next({ 'track-accelerations': true });
this.isTrackingAccelerations = true;
}
stopTrackAccelerations() {
if (this.isTrackingAccelerations) {
this.websocketSubject.next({ 'track-accelerations': false });
this.isTrackingAccelerations = false;
}
}
ensureTrackAccelerations() {
if (!this.isTrackingAccelerations) {
this.startTrackAccelerations();
}
}
fetchStatistics(historicalDate: string) { fetchStatistics(historicalDate: string) {
this.websocketSubject.next({ historicalDate }); this.websocketSubject.next({ historicalDate });
} }
@ -416,6 +438,18 @@ export class WebsocketService {
} }
} }
if (response['accelerations']) {
if (response['accelerations'].accelerations) {
this.stateService.accelerations$.next({
added: response['accelerations'].accelerations,
removed: [],
reset: true,
});
} else {
this.stateService.accelerations$.next(response['accelerations']);
}
}
if (response['live-2h-chart']) { if (response['live-2h-chart']) {
this.stateService.live2Chart$.next(response['live-2h-chart']); this.stateService.live2Chart$.next(response['live-2h-chart']);
} }