Skip to content

Commit

Permalink
fix: fix cacheable decorator
Browse files Browse the repository at this point in the history
  • Loading branch information
ahonn committed Aug 2, 2024
1 parent 7746326 commit eaa040b
Show file tree
Hide file tree
Showing 4 changed files with 76 additions and 24 deletions.
18 changes: 11 additions & 7 deletions backend/src/core/bitcoin-api/bitcoin-api.service.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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 keyof T> = T[K] extends (...args: infer P) => any ? P : never;
Expand Down Expand Up @@ -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 });
Expand All @@ -202,35 +206,35 @@ export class BitcoinApiService {
}

@Cacheable({
key: ({ hash }) => `getBlock:${hash}`,
namespace: 'bitcoinApiService',
key: ({ hash }) => `getBlock:${hash}`,
ttl: ONE_MONTH_MS,
})
public async getBlock({ hash }: { hash: string }) {
return this.call('getBlock', { hash });
}

@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 }) {
return this.call('getBlockTxs', { hash, startIndex });
}

@Cacheable({
key: ({ height }) => `getBlockHeight:${height}`,
namespace: 'bitcoinApiService',
key: ({ height }) => `getBlockHeight:${height}`,
ttl: TEN_MINUTES_MS,
})
public async getBlockHeight({ height }: { height: number }) {
return this.call('getBlockHeight', { height });
}

@Cacheable({
key: ({ hash }) => `getBlockTxids:${hash}`,
namespace: 'bitcoinApiService',
key: ({ hash }) => `getBlockTxids:${hash}`,
ttl: ONE_MONTH_MS,
})
public async getBlockTxids({ hash }: { hash: string }) {
Expand Down
9 changes: 6 additions & 3 deletions backend/src/core/ckb-explorer/ckb-explorer.service.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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<NonPaginatedResponse<Block>> {
Expand All @@ -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(
Expand Down Expand Up @@ -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<DetailTransaction>) => {
// cache tx for 1 month if it's committed and older than 1 hour
Expand Down
27 changes: 25 additions & 2 deletions backend/src/core/ckb-rpc/ckb-rpc-websocket.service.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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 {
Expand All @@ -19,11 +21,26 @@ export class CkbRpcWebsocketService {

constructor(private configService: ConfigService<Env>) {
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<TransactionWithStatusResponse> {
this.logger.debug(`get_transaction - txHash: ${txHash}`);
const tx = await this.websocket.call('get_transaction', [txHash]);
Expand All @@ -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<number> {
this.logger.debug('get_tip_block_number');
const tipBlockNumber = await this.websocket.call('get_tip_block_number', []);
Expand Down
46 changes: 34 additions & 12 deletions backend/src/decorators/cacheable.decorator.ts
Original file line number Diff line number Diff line change
@@ -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<boolean>;
shouldCache?: (result: any, target: any) => boolean | Promise<boolean>;
}

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.
Expand All @@ -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<typeof generateComposedKey>[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,
};
};
Expand Down

0 comments on commit eaa040b

Please sign in to comment.