create separate service for short term tx & block caching

This commit is contained in:
Mononaut 2022-12-27 05:36:58 -06:00
parent befafaa60c
commit 7be3ed416e
No known key found for this signature in database
GPG Key ID: A3F058E41374C04E
9 changed files with 147 additions and 30 deletions

View File

@ -6,6 +6,7 @@ import { AppRoutingModule } from './app-routing.module';
import { AppComponent } from './components/app/app.component'; import { AppComponent } from './components/app/app.component';
import { ElectrsApiService } from './services/electrs-api.service'; import { ElectrsApiService } from './services/electrs-api.service';
import { StateService } from './services/state.service'; import { StateService } from './services/state.service';
import { CacheService } from './services/cache.service';
import { EnterpriseService } from './services/enterprise.service'; import { EnterpriseService } from './services/enterprise.service';
import { WebsocketService } from './services/websocket.service'; import { WebsocketService } from './services/websocket.service';
import { AudioService } from './services/audio.service'; import { AudioService } from './services/audio.service';
@ -23,6 +24,7 @@ import { AppPreloadingStrategy } from './app.preloading-strategy';
const providers = [ const providers = [
ElectrsApiService, ElectrsApiService,
StateService, StateService,
CacheService,
WebsocketService, WebsocketService,
AudioService, AudioService,
SeoService, SeoService,

View File

@ -138,7 +138,6 @@ export class BlockComponent implements OnInit, OnDestroy {
this.page = 1; this.page = 1;
this.error = undefined; this.error = undefined;
this.fees = undefined; this.fees = undefined;
this.stateService.markBlock$.next({});
this.auditDataMissing = false; this.auditDataMissing = false;
if (history.state.data && history.state.data.blockHeight) { if (history.state.data && history.state.data.blockHeight) {

View File

@ -5,6 +5,7 @@ import { specialBlocks } from '../../app.constants';
import { BlockExtended } from '../../interfaces/node-api.interface'; import { BlockExtended } from '../../interfaces/node-api.interface';
import { Location } from '@angular/common'; import { Location } from '@angular/common';
import { config } from 'process'; import { config } from 'process';
import { CacheService } from 'src/app/services/cache.service';
interface BlockchainBlock extends BlockExtended { interface BlockchainBlock extends BlockExtended {
loading?: boolean; loading?: boolean;
@ -28,6 +29,7 @@ export class BlockchainBlocksComponent implements OnInit, OnChanges, OnDestroy {
emptyBlocks: BlockExtended[] = this.mountEmptyBlocks(); emptyBlocks: BlockExtended[] = this.mountEmptyBlocks();
markHeight: number; markHeight: number;
blocksSubscription: Subscription; blocksSubscription: Subscription;
blockPageSubscription: Subscription;
networkSubscription: Subscription; networkSubscription: Subscription;
tabHiddenSubscription: Subscription; tabHiddenSubscription: Subscription;
markBlockSubscription: Subscription; markBlockSubscription: Subscription;
@ -56,6 +58,7 @@ export class BlockchainBlocksComponent implements OnInit, OnChanges, OnDestroy {
constructor( constructor(
public stateService: StateService, public stateService: StateService,
public cacheService: CacheService,
private cd: ChangeDetectorRef, private cd: ChangeDetectorRef,
private location: Location, private location: Location,
) { ) {
@ -123,6 +126,12 @@ export class BlockchainBlocksComponent implements OnInit, OnChanges, OnDestroy {
} }
this.cd.markForCheck(); this.cd.markForCheck();
}); });
} else {
this.blockPageSubscription = this.cacheService.loadedBlocks$.subscribe((block) => {
if (block.height <= this.height && block.height > this.height - this.count) {
this.onBlockLoaded(block);
}
});
} }
this.markBlockSubscription = this.stateService.markBlock$ this.markBlockSubscription = this.stateService.markBlock$
@ -151,6 +160,9 @@ export class BlockchainBlocksComponent implements OnInit, OnChanges, OnDestroy {
if (this.blocksSubscription) { if (this.blocksSubscription) {
this.blocksSubscription.unsubscribe(); this.blocksSubscription.unsubscribe();
} }
if (this.blockPageSubscription) {
this.blockPageSubscription.unsubscribe();
}
this.networkSubscription.unsubscribe(); this.networkSubscription.unsubscribe();
this.tabHiddenSubscription.unsubscribe(); this.tabHiddenSubscription.unsubscribe();
this.markBlockSubscription.unsubscribe(); this.markBlockSubscription.unsubscribe();
@ -201,12 +213,9 @@ export class BlockchainBlocksComponent implements OnInit, OnChanges, OnDestroy {
while (this.blocks.length < Math.min(this.height + 1, this.count)) { while (this.blocks.length < Math.min(this.height + 1, this.count)) {
const height = this.height - this.blocks.length; const height = this.height - this.blocks.length;
if (height >= 0) { if (height >= 0) {
// const block = this.cacheService.getCachedBlock(height) || null; this.cacheService.loadBlock(height);
// if (!block) { const block = this.cacheService.getCachedBlock(height) || null;
// this.cacheService.loadBlock(height); this.blocks.push(block || {
// }
// this.blocks.push(block || {
this.blocks.push({
loading: true, loading: true,
id: '', id: '',
height, height,
@ -236,6 +245,15 @@ export class BlockchainBlocksComponent implements OnInit, OnChanges, OnDestroy {
} }
} }
onBlockLoaded(block: BlockExtended) {
const blockIndex = this.height - block.height;
if (blockIndex >= 0 && blockIndex < this.blocks.length) {
this.blocks[blockIndex] = block;
this.blockStyles[blockIndex] = this.getStyleForBlock(block, blockIndex);
}
this.cd.markForCheck();
}
getStyleForBlock(block: BlockchainBlock, index: number, animateSlideStart: boolean = false) { getStyleForBlock(block: BlockchainBlock, index: number, animateSlideStart: boolean = false) {
if (!block || block.loading) { if (!block || block.loading) {
return this.getStyleForLoadingBlock(index, animateSlideStart); return this.getStyleForLoadingBlock(index, animateSlideStart);

View File

@ -46,6 +46,11 @@ export class StartComponent implements OnInit, OnDestroy {
this.chainTip = height; this.chainTip = height;
this.updatePages(); this.updatePages();
}); });
this.markBlockSubscription = this.stateService.markBlock$.subscribe((mark) => {
if (mark?.blockHeight != null) {
this.scrollToBlock(mark.blockHeight);
}
});
this.stateService.blocks$ this.stateService.blocks$
.subscribe((blocks: any) => { .subscribe((blocks: any) => {
if (this.stateService.network !== '') { if (this.stateService.network !== '') {

View File

@ -11,6 +11,7 @@ import {
import { Transaction, Vout } from '../../interfaces/electrs.interface'; import { Transaction, Vout } from '../../interfaces/electrs.interface';
import { of, merge, Subscription, Observable, Subject, from } from 'rxjs'; import { of, merge, Subscription, Observable, Subject, from } from 'rxjs';
import { StateService } from '../../services/state.service'; import { StateService } from '../../services/state.service';
import { CacheService } from '../../services/cache.service';
import { OpenGraphService } from '../../services/opengraph.service'; import { OpenGraphService } from '../../services/opengraph.service';
import { ApiService } from '../../services/api.service'; import { ApiService } from '../../services/api.service';
import { SeoService } from '../../services/seo.service'; import { SeoService } from '../../services/seo.service';
@ -45,6 +46,7 @@ export class TransactionPreviewComponent implements OnInit, OnDestroy {
private route: ActivatedRoute, private route: ActivatedRoute,
private electrsApiService: ElectrsApiService, private electrsApiService: ElectrsApiService,
private stateService: StateService, private stateService: StateService,
private cacheService: CacheService,
private apiService: ApiService, private apiService: ApiService,
private seoService: SeoService, private seoService: SeoService,
private openGraphService: OpenGraphService, private openGraphService: OpenGraphService,
@ -97,7 +99,7 @@ export class TransactionPreviewComponent implements OnInit, OnDestroy {
}), }),
switchMap(() => { switchMap(() => {
let transactionObservable$: Observable<Transaction>; let transactionObservable$: Observable<Transaction>;
const cached = this.stateService.getTxFromCache(this.txId); const cached = this.cacheService.getTxFromCache(this.txId);
if (cached && cached.fee !== -1) { if (cached && cached.fee !== -1) {
transactionObservable$ = of(cached); transactionObservable$ = of(cached);
} else { } else {

View File

@ -13,6 +13,7 @@ import {
import { Transaction } from '../../interfaces/electrs.interface'; import { Transaction } from '../../interfaces/electrs.interface';
import { of, merge, Subscription, Observable, Subject, timer, combineLatest, from, throwError } from 'rxjs'; import { of, merge, Subscription, Observable, Subject, timer, combineLatest, from, throwError } from 'rxjs';
import { StateService } from '../../services/state.service'; import { StateService } from '../../services/state.service';
import { CacheService } from '../../services/cache.service';
import { WebsocketService } from '../../services/websocket.service'; import { WebsocketService } from '../../services/websocket.service';
import { AudioService } from '../../services/audio.service'; import { AudioService } from '../../services/audio.service';
import { ApiService } from '../../services/api.service'; import { ApiService } from '../../services/api.service';
@ -74,6 +75,7 @@ export class TransactionComponent implements OnInit, AfterViewInit, OnDestroy {
private relativeUrlPipe: RelativeUrlPipe, private relativeUrlPipe: RelativeUrlPipe,
private electrsApiService: ElectrsApiService, private electrsApiService: ElectrsApiService,
private stateService: StateService, private stateService: StateService,
private cacheService: CacheService,
private websocketService: WebsocketService, private websocketService: WebsocketService,
private audioService: AudioService, private audioService: AudioService,
private apiService: ApiService, private apiService: ApiService,
@ -203,7 +205,7 @@ export class TransactionComponent implements OnInit, AfterViewInit, OnDestroy {
}), }),
switchMap(() => { switchMap(() => {
let transactionObservable$: Observable<Transaction>; let transactionObservable$: Observable<Transaction>;
const cached = this.stateService.getTxFromCache(this.txId); const cached = this.cacheService.getTxFromCache(this.txId);
if (cached && cached.fee !== -1) { if (cached && cached.fee !== -1) {
transactionObservable$ = of(cached); transactionObservable$ = of(cached);
} else { } else {
@ -302,7 +304,7 @@ export class TransactionComponent implements OnInit, AfterViewInit, OnDestroy {
this.waitingForTransaction = false; this.waitingForTransaction = false;
} }
this.rbfTransaction = rbfTransaction; this.rbfTransaction = rbfTransaction;
this.stateService.setTxCache([this.rbfTransaction]); this.cacheService.setTxCache([this.rbfTransaction]);
}); });
this.queryParamsSubscription = this.route.queryParams.subscribe((params) => { this.queryParamsSubscription = this.route.queryParams.subscribe((params) => {

View File

@ -1,5 +1,6 @@
import { Component, OnInit, Input, ChangeDetectionStrategy, OnChanges, Output, EventEmitter, ChangeDetectorRef } from '@angular/core'; import { Component, OnInit, Input, ChangeDetectionStrategy, OnChanges, Output, EventEmitter, ChangeDetectorRef } from '@angular/core';
import { StateService } from '../../services/state.service'; import { StateService } from '../../services/state.service';
import { CacheService } from '../../services/cache.service';
import { Observable, ReplaySubject, BehaviorSubject, merge, Subscription } from 'rxjs'; import { Observable, ReplaySubject, BehaviorSubject, merge, Subscription } from 'rxjs';
import { Outspend, Transaction, Vin, Vout } from '../../interfaces/electrs.interface'; import { Outspend, Transaction, Vin, Vout } from '../../interfaces/electrs.interface';
import { ElectrsApiService } from '../../services/electrs-api.service'; import { ElectrsApiService } from '../../services/electrs-api.service';
@ -44,6 +45,7 @@ export class TransactionsListComponent implements OnInit, OnChanges {
constructor( constructor(
public stateService: StateService, public stateService: StateService,
private cacheService: CacheService,
private electrsApiService: ElectrsApiService, private electrsApiService: ElectrsApiService,
private apiService: ApiService, private apiService: ApiService,
private assetsService: AssetsService, private assetsService: AssetsService,
@ -123,7 +125,7 @@ export class TransactionsListComponent implements OnInit, OnChanges {
} }
this.transactionsLength = this.transactions.length; this.transactionsLength = this.transactions.length;
this.stateService.setTxCache(this.transactions); this.cacheService.setTxCache(this.transactions);
this.transactions.forEach((tx) => { this.transactions.forEach((tx) => {
tx['@voutLimit'] = true; tx['@voutLimit'] = true;

View File

@ -0,0 +1,105 @@
import { Injectable } from '@angular/core';
import { firstValueFrom, Subject, Subscription} from 'rxjs';
import { Transaction } from '../interfaces/electrs.interface';
import { BlockExtended } from '../interfaces/node-api.interface';
import { StateService } from './state.service';
import { ApiService } from './api.service';
const BLOCK_CACHE_SIZE = 50;
const KEEP_RECENT_BLOCKS = 50;
@Injectable({
providedIn: 'root'
})
export class CacheService {
loadedBlocks$ = new Subject<BlockExtended>();
tip: number = 0;
txCache: { [txid: string]: Transaction } = {};
blockCache: { [height: number]: BlockExtended } = {};
blockLoading: { [height: number]: boolean } = {};
copiesInBlockQueue: { [height: number]: number } = {};
blockPriorities: number[] = [];
constructor(
private stateService: StateService,
private apiService: ApiService,
) {
this.stateService.blocks$.subscribe(([block]) => {
this.addBlockToCache(block);
this.clearBlocks();
});
this.stateService.chainTip$.subscribe((height) => {
this.tip = height;
});
}
setTxCache(transactions) {
this.txCache = {};
transactions.forEach(tx => {
this.txCache[tx.txid] = tx;
});
}
getTxFromCache(txid) {
if (this.txCache && this.txCache[txid]) {
return this.txCache[txid];
} else {
return null;
}
}
addBlockToCache(block: BlockExtended) {
this.blockCache[block.height] = block;
this.bumpBlockPriority(block.height);
}
async loadBlock(height) {
if (!this.blockCache[height] && !this.blockLoading[height]) {
const chunkSize = 10;
const maxHeight = Math.ceil(height / chunkSize) * chunkSize;
for (let i = 0; i < chunkSize; i++) {
this.blockLoading[maxHeight - i] = true;
}
const result = await firstValueFrom(this.apiService.getBlocks$(maxHeight));
for (let i = 0; i < chunkSize; i++) {
delete this.blockLoading[maxHeight - i];
}
if (result && result.length) {
result.forEach(block => {
this.addBlockToCache(block);
this.loadedBlocks$.next(block);
});
}
this.clearBlocks();
} else {
this.bumpBlockPriority(height);
}
}
// increase the priority of a block, to delay removal
bumpBlockPriority(height) {
this.blockPriorities.push(height);
this.copiesInBlockQueue[height] = (this.copiesInBlockQueue[height] || 0) + 1;
}
// remove lowest priority blocks from the cache
clearBlocks() {
while (Object.keys(this.blockCache).length > (BLOCK_CACHE_SIZE + KEEP_RECENT_BLOCKS) && this.blockPriorities.length > KEEP_RECENT_BLOCKS) {
const height = this.blockPriorities.shift();
if (this.copiesInBlockQueue[height] > 1) {
this.copiesInBlockQueue[height]--;
} else if ((this.tip - height) < KEEP_RECENT_BLOCKS) {
this.bumpBlockPriority(height);
} else {
delete this.blockCache[height];
delete this.copiesInBlockQueue[height];
}
}
}
getCachedBlock(height) {
return this.blockCache[height];
}
}

View File

@ -1,6 +1,6 @@
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 } from 'rxjs'; import { ReplaySubject, BehaviorSubject, Subject, fromEvent, Observable } from 'rxjs';
import { Block, Transaction } from '../interfaces/electrs.interface'; import { Transaction } from '../interfaces/electrs.interface';
import { IBackendInfo, MempoolBlock, MempoolBlockWithTransactions, MempoolBlockDelta, MempoolInfo, Recommendedfees, ReplacedTransaction, TransactionStripped } from '../interfaces/websocket.interface'; import { IBackendInfo, MempoolBlock, MempoolBlockWithTransactions, MempoolBlockDelta, MempoolInfo, Recommendedfees, ReplacedTransaction, TransactionStripped } from '../interfaces/websocket.interface';
import { BlockExtended, DifficultyAdjustment, OptimizedMempoolStats } from '../interfaces/node-api.interface'; import { BlockExtended, DifficultyAdjustment, OptimizedMempoolStats } from '../interfaces/node-api.interface';
import { Router, NavigationStart } from '@angular/router'; import { Router, NavigationStart } from '@angular/router';
@ -119,8 +119,6 @@ export class StateService {
timeLtr: BehaviorSubject<boolean>; timeLtr: BehaviorSubject<boolean>;
hideFlow: BehaviorSubject<boolean>; hideFlow: BehaviorSubject<boolean>;
txCache: { [txid: string]: Transaction } = {};
constructor( constructor(
@Inject(PLATFORM_ID) private platformId: any, @Inject(PLATFORM_ID) private platformId: any,
@Inject(LOCALE_ID) private locale: string, @Inject(LOCALE_ID) private locale: string,
@ -275,28 +273,12 @@ export class StateService {
return this.network === 'liquid' || this.network === 'liquidtestnet'; return this.network === 'liquid' || this.network === 'liquidtestnet';
} }
setTxCache(transactions) {
this.txCache = {};
transactions.forEach(tx => {
this.txCache[tx.txid] = tx;
});
}
getTxFromCache(txid) {
if (this.txCache && this.txCache[txid]) {
return this.txCache[txid];
} else {
return null;
}
}
resetChainTip() { resetChainTip() {
this.latestBlockHeight = -1; this.latestBlockHeight = -1;
this.chainTip$.next(-1); this.chainTip$.next(-1);
} }
updateChainTip(height) { updateChainTip(height) {
console.log('updating chain tip to ', height);
if (height > this.latestBlockHeight) { if (height > this.latestBlockHeight) {
this.latestBlockHeight = height; this.latestBlockHeight = height;
this.chainTip$.next(height); this.chainTip$.next(height);