125 lines
3.5 KiB
TypeScript
125 lines
3.5 KiB
TypeScript
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 = 500;
|
|
const KEEP_RECENT_BLOCKS = 50;
|
|
|
|
@Injectable({
|
|
providedIn: 'root'
|
|
})
|
|
export class CacheService {
|
|
loadedBlocks$ = new Subject<BlockExtended>();
|
|
tip: number = 0;
|
|
|
|
txCache: { [txid: string]: Transaction } = {};
|
|
|
|
network: string;
|
|
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;
|
|
});
|
|
this.stateService.networkChanged$.subscribe((network) => {
|
|
this.network = network;
|
|
this.resetBlockCache();
|
|
});
|
|
}
|
|
|
|
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;
|
|
}
|
|
let result;
|
|
try {
|
|
result = await firstValueFrom(this.apiService.getBlocks$(maxHeight));
|
|
} catch (e) {
|
|
console.log("failed to load blocks: ", e.message);
|
|
}
|
|
if (result && result.length) {
|
|
result.forEach(block => {
|
|
if (this.blockLoading[block.height]) {
|
|
this.addBlockToCache(block);
|
|
this.loadedBlocks$.next(block);
|
|
}
|
|
});
|
|
}
|
|
for (let i = 0; i < chunkSize; i++) {
|
|
delete this.blockLoading[maxHeight - i];
|
|
}
|
|
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];
|
|
}
|
|
}
|
|
}
|
|
|
|
// remove all blocks from the cache
|
|
resetBlockCache() {
|
|
this.blockCache = {};
|
|
this.blockLoading = {};
|
|
this.copiesInBlockQueue = {};
|
|
this.blockPriorities = [];
|
|
}
|
|
|
|
getCachedBlock(height) {
|
|
return this.blockCache[height];
|
|
}
|
|
} |