diff --git a/backend/src/core/bitcoin-api/bitcoin-api.service.ts b/backend/src/core/bitcoin-api/bitcoin-api.service.ts index 54815e61..dd8ddf73 100644 --- a/backend/src/core/bitcoin-api/bitcoin-api.service.ts +++ b/backend/src/core/bitcoin-api/bitcoin-api.service.ts @@ -7,8 +7,8 @@ import { Env } from 'src/env'; import { IBitcoinDataProvider } from './bitcoin-api.interface'; import { ElectrsService } from './provider/electrs.service'; import { MempoolService } from './provider/mempool.service'; -import { ChainInfo } from './bitcoin-api.schema'; -import { ONE_MONTH_MS, TEN_MINUTES_MS } from 'src/common/date'; +import { ChainInfo, Transaction } from './bitcoin-api.schema'; +import { ONE_HOUR_MS, ONE_MONTH_MS, TEN_MINUTES_MS } from 'src/common/date'; import { Cacheable } from 'src/decorators/cacheable.decorator'; type MethodParameters = T[K] extends (...args: infer P) => any ? P : never; @@ -185,9 +185,13 @@ export class BitcoinApiService { } @Cacheable({ - key: ({ txid }) => `getTx:${txid}`, namespace: 'bitcoinApiService', + key: ({ txid }) => `getTx:${txid}`, ttl: ONE_MONTH_MS, + shouldCache: (tx: Transaction) => + tx.status.confirmed && + !!tx.status.block_time && + tx.status.block_time - Date.now() < ONE_HOUR_MS, }) public async getTx({ txid }: { txid: string }) { return this.call('getTx', { txid }); @@ -202,8 +206,8 @@ export class BitcoinApiService { } @Cacheable({ - key: ({ hash }) => `getBlock:${hash}`, namespace: 'bitcoinApiService', + key: ({ hash }) => `getBlock:${hash}`, ttl: ONE_MONTH_MS, }) public async getBlock({ hash }: { hash: string }) { @@ -211,8 +215,8 @@ export class BitcoinApiService { } @Cacheable({ - key: ({ hash, startIndex }) => `getBlockTxs:${hash}:${startIndex}`, namespace: 'bitcoinApiService', + key: ({ hash, startIndex }) => `getBlockTxs:${hash}:${startIndex}`, ttl: ONE_MONTH_MS, }) public async getBlockTxs({ hash, startIndex }: { hash: string; startIndex?: number }) { @@ -220,8 +224,8 @@ export class BitcoinApiService { } @Cacheable({ - key: ({ height }) => `getBlockHeight:${height}`, namespace: 'bitcoinApiService', + key: ({ height }) => `getBlockHeight:${height}`, ttl: TEN_MINUTES_MS, }) public async getBlockHeight({ height }: { height: number }) { @@ -229,8 +233,8 @@ export class BitcoinApiService { } @Cacheable({ - key: ({ hash }) => `getBlockTxids:${hash}`, namespace: 'bitcoinApiService', + key: ({ hash }) => `getBlockTxids:${hash}`, ttl: ONE_MONTH_MS, }) public async getBlockTxids({ hash }: { hash: string }) { diff --git a/backend/src/core/ckb-explorer/ckb-explorer.service.ts b/backend/src/core/ckb-explorer/ckb-explorer.service.ts index 22fd97f2..19f701f3 100644 --- a/backend/src/core/ckb-explorer/ckb-explorer.service.ts +++ b/backend/src/core/ckb-explorer/ckb-explorer.service.ts @@ -126,7 +126,8 @@ export class CkbExplorerService { } @Cacheable({ - key: (heightOrHash: string) => `CkbExplorerService:getBlock:${heightOrHash}`, + namespace: 'CkbExplorerService', + key: (heightOrHash: string) => `getBlock:${heightOrHash}`, ttl: ONE_MONTH_MS, }) public async getBlock(heightOrHash: string): Promise> { @@ -135,8 +136,9 @@ export class CkbExplorerService { } @Cacheable({ + namespace: 'CkbExplorerService', key: (heightOrHash: string, { page = 1, pageSize = 10 }: BasePaginationParams = {}) => - `CkbExplorerService:getBlockTransactions:${heightOrHash},${page},${pageSize}`, + `getBlockTransactions:${heightOrHash},${page},${pageSize}`, ttl: ONE_MONTH_MS, }) public async getBlockTransactions( @@ -190,7 +192,8 @@ export class CkbExplorerService { } @Cacheable({ - key: (txHash: string) => `CkbExplorerService:getTransaction:${txHash}`, + namespace: 'CkbExplorerService', + key: (txHash: string) => `getTransaction:${txHash}`, ttl: ONE_HOUR_MS, shouldCache: async (tx: NonPaginatedResponse) => { // cache tx for 1 month if it's committed and older than 1 hour diff --git a/backend/src/core/ckb-rpc/ckb-rpc-websocket.service.ts b/backend/src/core/ckb-rpc/ckb-rpc-websocket.service.ts index 61637bbb..c0bf95db 100644 --- a/backend/src/core/ckb-rpc/ckb-rpc-websocket.service.ts +++ b/backend/src/core/ckb-rpc/ckb-rpc-websocket.service.ts @@ -11,6 +11,8 @@ import { SearchKey, TransactionWithStatusResponse, } from './ckb-rpc.interface'; +import { Cacheable } from 'src/decorators/cacheable.decorator'; +import { ONE_MONTH_MS } from 'src/common/date'; @Injectable() export class CkbRpcWebsocketService { @@ -19,11 +21,26 @@ export class CkbRpcWebsocketService { constructor(private configService: ConfigService) { this.websocket = new RpcWebsocketsClient(this.configService.get('CKB_RPC_WEBSOCKET_URL')); - this.websocket.on('error', (error) => { - this.logger.error(error.message); + + this.websocket.on('open', () => { + this.websocket.on('error', (error) => { + this.logger.error(error.message); + }); }); } + @Cacheable({ + namespace: 'CkbRpcWebsocketService', + key: (txHash: string) => `getTransaction:${txHash}`, + ttl: ONE_MONTH_MS, + shouldCache: async (tx: TransactionWithStatusResponse, that: CkbRpcWebsocketService) => { + if (tx.tx_status.status !== 'committed') { + return false; + } + const block = await that.getTipBlockNumber(); + return BI.from(tx.tx_status.block_number).lt(BI.from(block)); + }, + }) public async getTransaction(txHash: string): Promise { this.logger.debug(`get_transaction - txHash: ${txHash}`); const tx = await this.websocket.call('get_transaction', [txHash]); @@ -50,6 +67,12 @@ export class CkbRpcWebsocketService { return blockEconomicState as BlockEconomicState; } + @Cacheable({ + namespace: 'CkbRpcWebsocketService', + key: 'getTipBlockNumber', + // just cache for 1 second to avoid too many requests + ttl: 1000, + }) public async getTipBlockNumber(): Promise { this.logger.debug('get_tip_block_number'); const tipBlockNumber = await this.websocket.call('get_tip_block_number', []); diff --git a/backend/src/decorators/cacheable.decorator.ts b/backend/src/decorators/cacheable.decorator.ts index b99e588d..f0295535 100644 --- a/backend/src/decorators/cacheable.decorator.ts +++ b/backend/src/decorators/cacheable.decorator.ts @@ -1,10 +1,15 @@ // eslint-disable-next-line no-restricted-imports +import { Cache, CACHE_MANAGER } from '@nestjs/cache-manager'; +import { Inject, Logger } from '@nestjs/common'; import { CacheableRegisterOptions, Cacheable as _Cacheable } from 'nestjs-cacheable'; +import { cacheableHandle, generateComposedKey } from 'nestjs-cacheable/dist/cacheable.helper'; export interface CustomCacheableRegisterOptions extends CacheableRegisterOptions { - shouldCache?: (result: any) => boolean | Promise; + shouldCache?: (result: any, target: any) => boolean | Promise; } +const logger = new Logger('Cacheable'); + /** * Cacheable decorator with custom options, based on the original Cacheable decorator from the nestjs-cacheable package. * Adds a shouldCache option to determine whether the result should be cached. @@ -17,23 +22,40 @@ export interface CustomCacheableRegisterOptions extends CacheableRegisterOptions * }); */ export function Cacheable(options: CustomCacheableRegisterOptions): MethodDecorator { - return function (_, propertyKey, descriptor) { + const injectCacheService = Inject(CACHE_MANAGER); + + return function(target, propertyKey, descriptor) { // eslint-disable-next-line @typescript-eslint/ban-types const originalMethod = descriptor.value as unknown as Function; + + injectCacheService(target, '__cacheManager'); return { ...descriptor, - value: async function (...args: any[]) { - const returnVal = await originalMethod.apply(this, args); + value: async function(...args: any[]) { + const cacheManager = this.__cacheManager as Cache; + if (!cacheManager) return originalMethod.apply(this, args); + const composeOptions: Parameters[0] = { + methodName: String(propertyKey), + key: options.key, + namespace: options.namespace, + args, + }; + const [key] = generateComposedKey(composeOptions); + const returnVal = await cacheableHandle( + key, + () => originalMethod.apply(this, args), + options.ttl, + ); - const cacheable = options.shouldCache ? await options.shouldCache(returnVal) : true; - if (!cacheable) { - return returnVal; + // Remove the cache if shouldCache returns false + const shouldCache = options.shouldCache + ? await options.shouldCache(returnVal, this) + : true; + if (!shouldCache) { + logger.debug(`Removing cache for key: ${key}`); + await cacheManager.del(key); } - - const fakeDescriptor = { - value: () => returnVal, - }; - return _Cacheable(options)(this, propertyKey, fakeDescriptor); + return returnVal; } as any, }; };