Skip to content

Commit

Permalink
feat: add custom cacheable decorator and fix eslint issue
Browse files Browse the repository at this point in the history
  • Loading branch information
ahonn committed Aug 2, 2024
1 parent 62fccfe commit 9b26ce2
Show file tree
Hide file tree
Showing 39 changed files with 448 additions and 56 deletions.
15 changes: 10 additions & 5 deletions backend/.eslintrc.js
Original file line number Diff line number Diff line change
Expand Up @@ -5,11 +5,8 @@ module.exports = {
tsconfigRootDir: __dirname,
sourceType: 'module',
},
plugins: ['@typescript-eslint/eslint-plugin'],
extends: [
'plugin:@typescript-eslint/recommended',
'plugin:prettier/recommended',
],
plugins: ['@typescript-eslint/eslint-plugin', 'import'],
extends: ['plugin:@typescript-eslint/recommended', 'plugin:prettier/recommended'],
root: true,
env: {
node: true,
Expand All @@ -21,5 +18,13 @@ module.exports = {
'@typescript-eslint/explicit-function-return-type': 'off',
'@typescript-eslint/explicit-module-boundary-types': 'off',
'@typescript-eslint/no-explicit-any': 'off',
'no-restricted-imports': [
'error',
{
name: 'nestjs-cacheable',
importNames: ['Cacheable'],
message: "Please use 'src/decorators/cacheable.decorator' instead",
},
],
},
};
1 change: 1 addition & 0 deletions backend/.husky/pre-commit
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
npm
14 changes: 12 additions & 2 deletions backend/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -18,7 +18,12 @@
"test:cov": "jest --coverage",
"test:debug": "node --inspect-brk -r tsconfig-paths/register -r ts-node/register node_modules/.bin/jest --runInBand",
"test:e2e": "jest --config ./test/jest-e2e.json",
"postinstall": "npx prisma generate"
"postinstall": "npx prisma generate",
"precommit": "lint-staged",
"prepare": "husky"
},
"lint-staged": {
"*.ts": "eslint \"{src,apps,libs,test}/**/*.ts\" --fix"
},
"dependencies": {
"@applifting-io/nestjs-dataloader": "^1.1.5",
Expand Down Expand Up @@ -70,13 +75,16 @@
"@types/jest": "^29.5.2",
"@types/lodash": "^4.17.7",
"@types/node": "^20.3.1",
"@types/serialize-javascript": "^5.0.4",
"@types/supertest": "^6.0.0",
"@typescript-eslint/eslint-plugin": "^7.0.0",
"@typescript-eslint/parser": "^7.0.0",
"eslint": "^8.42.0",
"eslint-config-prettier": "^9.0.0",
"eslint-plugin-prettier": "^5.0.0",
"husky": "^9.1.4",
"jest": "^29.5.0",
"lint-staged": "^15.2.7",
"prettier": "^3.0.0",
"prisma": "^5.16.2",
"source-map-support": "^0.5.21",
Expand All @@ -94,7 +102,9 @@
"json",
"ts"
],
"modulePaths": ["."],
"modulePaths": [
"."
],
"testRegex": ".*\\.(spec|test)\\.ts$",
"transform": {
"^.+\\.(t|j)s$": "ts-jest"
Expand Down
1 change: 1 addition & 0 deletions backend/src/common/date.ts
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
export const ONE_MONTH_MS = 30 * 24 * 60 * 60 * 1000;
export const ONE_HOUR_MS = 60 * 60 * 1000;
export const TEN_MINUTES_MS = 10 * 60 * 1000;
export const ONE_MINUTE_MS = 60 * 1000;
1 change: 0 additions & 1 deletion backend/src/constants.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,3 @@
import { Script } from '@ckb-lumos/lumos';
import * as RgbppBtc from '@rgbpp-sdk/btc';
import { BTCTestnetType } from '@rgbpp-sdk/ckb';

Expand Down
2 changes: 1 addition & 1 deletion backend/src/core/bitcoin-api/bitcoin-api.service.ts
Original file line number Diff line number Diff line change
Expand Up @@ -8,8 +8,8 @@ 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 { Cacheable } from 'nestjs-cacheable';
import { 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;
type MethodReturnType<T, K extends keyof T> = T[K] extends (...args: any[]) => infer R ? R : never;
Expand Down
4 changes: 2 additions & 2 deletions backend/src/core/ckb-explorer/ckb-explorer.interface.ts
Original file line number Diff line number Diff line change
Expand Up @@ -52,7 +52,7 @@ export interface CkbExplorerResponse<T, IsPaginated extends boolean = false> {
meta: IsPaginated extends true ? PaginationMeta : never;
}

export type NonPaginatedResponse<T extends {}> = CkbExplorerResponse<
export type NonPaginatedResponse<T extends object> = CkbExplorerResponse<
{
id: string;
type: string;
Expand All @@ -61,7 +61,7 @@ export type NonPaginatedResponse<T extends {}> = CkbExplorerResponse<
false
>;

export type PaginatedResponse<T extends {}> = CkbExplorerResponse<
export type PaginatedResponse<T extends object> = CkbExplorerResponse<
{
id: string;
type: string;
Expand Down
34 changes: 32 additions & 2 deletions backend/src/core/ckb-explorer/ckb-explorer.service.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import { Injectable, Logger } from '@nestjs/common';
import { Inject, Injectable, Logger } from '@nestjs/common';
import { ConfigService } from '@nestjs/config';
import axios, { Axios } from 'axios';
import { Env } from 'src/env';
Expand All @@ -22,6 +22,10 @@ import {
TransactionFeesStatistic,
TransactionListSortType,
} from './ckb-explorer.interface';
import { ONE_HOUR_MS, ONE_MONTH_MS } from 'src/common/date';
import { CACHE_MANAGER, Cache } from '@nestjs/cache-manager';
import { toNumber } from 'lodash';
import { Cacheable } from 'src/decorators/cacheable.decorator';

type BasePaginationParams = {
page?: number;
Expand Down Expand Up @@ -62,7 +66,10 @@ export class CkbExplorerService {
private logger = new Logger(CkbExplorerService.name);
private request: Axios;

constructor(private configService: ConfigService<Env>) {
constructor(
private configService: ConfigService<Env>,
@Inject(CACHE_MANAGER) protected cacheManager: Cache,
) {
this.request = axios.create({
baseURL: this.configService.get('CKB_EXPLORER_API_URL'),
headers: {
Expand Down Expand Up @@ -118,11 +125,20 @@ export class CkbExplorerService {
return response.data;
}

@Cacheable({
key: (heightOrHash: string) => `CkbExplorerService:getBlock:${heightOrHash}`,
ttl: ONE_MONTH_MS,
})
public async getBlock(heightOrHash: string): Promise<NonPaginatedResponse<Block>> {
const response = await this.request.get(`/v1/blocks/${heightOrHash}`);
return response.data;
}

@Cacheable({
key: (heightOrHash: string, { page = 1, pageSize = 10 }: BasePaginationParams = {}) =>
`CkbExplorerService:getBlockTransactions:${heightOrHash},${page},${pageSize}`,
ttl: ONE_MONTH_MS,
})
public async getBlockTransactions(
blockHash: string,
{ page = 1, pageSize = 10 }: BasePaginationParams = {},
Expand Down Expand Up @@ -173,7 +189,21 @@ export class CkbExplorerService {
return response.data;
}

@Cacheable({
key: (txHash: string) => `CkbExplorerService: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
const { tx_status, block_timestamp } = tx.data.attributes;
return tx_status === 'committed' && Date.now() - toNumber(block_timestamp) > ONE_HOUR_MS;
},
})
public async getTransaction(txHash: string): Promise<NonPaginatedResponse<DetailTransaction>> {
const key = `CkbExplorerService:getTransaction:${txHash}`;
const cached = await this.cacheManager.get(key);
if (cached) {
return cached as NonPaginatedResponse<DetailTransaction>;
}
const response = await this.request.get(`/v1/transactions/${txHash}`);
return response.data;
}
Expand Down
2 changes: 0 additions & 2 deletions backend/src/core/ckb-rpc/ckb-rpc.interface.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,3 @@
import { RPC } from '@ckb-lumos/rpc';

export interface CellDep {
dep_type: string;
out_point: {
Expand Down
40 changes: 40 additions & 0 deletions backend/src/decorators/cacheable.decorator.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,40 @@
// eslint-disable-next-line no-restricted-imports
import { CacheableRegisterOptions, Cacheable as _Cacheable } from 'nestjs-cacheable';

export interface CustomCacheableRegisterOptions extends CacheableRegisterOptions {
shouldCache?: (result: any) => boolean | Promise<boolean>;
}

/**
* 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.
*
* @example
* @Cacheable({
* ttl: 1000,
* key: (args: any[]) => args[0],
* shouldCache: (result: any) => result !== null,
* });
*/
export function Cacheable(options: CustomCacheableRegisterOptions): MethodDecorator {
return function (_, propertyKey, descriptor) {
// eslint-disable-next-line @typescript-eslint/ban-types
const originalMethod = descriptor.value as unknown as Function;
return {
...descriptor,
value: async function (...args: any[]) {
const returnVal = await originalMethod.apply(this, args);

const cacheable = options.shouldCache ? await options.shouldCache(returnVal) : true;
if (!cacheable) {
return returnVal;
}

const fakeDescriptor = {
value: () => returnVal,
};
return _Cacheable(options)(this, propertyKey, fakeDescriptor);
} as any,
};
};
}
1 change: 0 additions & 1 deletion backend/src/decorators/parent-field.decorator.ts
Original file line number Diff line number Diff line change
Expand Up @@ -6,4 +6,3 @@ export const ParentField = createParamDecorator((param: string, ctx: ExecutionCo
const value = root[param];
return value;
});

19 changes: 19 additions & 0 deletions backend/src/middlewares/field-performance.middleware.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@
import { Logger } from '@nestjs/common';
import { FieldMiddleware, MiddlewareContext, NextFn } from '@nestjs/graphql';

export const fieldPerformanceMiddleware: FieldMiddleware = async (
ctx: MiddlewareContext,
next: NextFn,
) => {
const now = performance.now();
const value = await next();

const executionTime = performance.now() - now;
if (executionTime > 100) {
const { path } = ctx.info;
logger.debug(`[${path.typename}.${path.key}]: ${executionTime}ms`);
}
return value;
};

const logger = new Logger(fieldPerformanceMiddleware.name);
2 changes: 1 addition & 1 deletion backend/src/modules/api.model.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import { registerEnumType } from "@nestjs/graphql";
import { registerEnumType } from '@nestjs/graphql';

export enum OrderType {
Desc = 'desc',
Expand Down
4 changes: 3 additions & 1 deletion backend/src/modules/api.module.ts
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,7 @@ import { RgbppModule } from './rgbpp/rgbpp.module';
import { BitcoinModule } from './bitcoin/bitcoin.module';
import { FastifyReply, FastifyRequest } from 'fastify';
import { SearchModule } from './search/search.module';
import { fieldPerformanceMiddleware } from 'src/middlewares/field-performance.middleware';

@Module({
imports: [
Expand All @@ -26,6 +27,7 @@ import { SearchModule } from './search/search.module';
autoSchemaFile: join(process.cwd(), 'src/schema.gql'),
buildSchemaOptions: {
dateScalarMode: 'timestamp',
fieldMiddleware: [fieldPerformanceMiddleware],
},
context: (req: FastifyRequest, res: FastifyReply) => {
if (req.method === 'GET') {
Expand Down Expand Up @@ -53,4 +55,4 @@ import { SearchModule } from './search/search.module';
},
],
})
export class ApiModule { }
export class ApiModule {}
2 changes: 1 addition & 1 deletion backend/src/modules/bitcoin/bitcoin.module.ts
Original file line number Diff line number Diff line change
Expand Up @@ -18,4 +18,4 @@ import { BitcoinOutputModule } from './output/output.module';
],
providers: [BitcoinResolver],
})
export class BitcoinModule { }
export class BitcoinModule {}
7 changes: 5 additions & 2 deletions backend/src/modules/bitcoin/bitcoin.resolver.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,14 +2,17 @@ import { Float, Parent, Query, ResolveField, Resolver } from '@nestjs/graphql';
import { BitcoinApiService } from 'src/core/bitcoin-api/bitcoin-api.service';
import { BitcoinBaseChainInfo, BitcoinChainInfo, BitcoinFees } from './bitcoin.model';
import { Loader } from '@applifting-io/nestjs-dataloader';
import { BitcoinBlockTxidsLoader, BitcoinBlockTxidsLoaderType } from './block/dataloader/block-txids.loader';
import {
BitcoinBlockTxidsLoader,
BitcoinBlockTxidsLoaderType,
} from './block/dataloader/block-txids.loader';

// 60 * 24 = 1440 minutes
const BLOCK_NUMBER_OF_24_HOURS = 144;

@Resolver(() => BitcoinChainInfo)
export class BitcoinResolver {
constructor(private bitcoinApiService: BitcoinApiService) { }
constructor(private bitcoinApiService: BitcoinApiService) {}

@Query(() => BitcoinChainInfo, { name: 'btcChainInfo' })
public async chainInfo(): Promise<BitcoinBaseChainInfo> {
Expand Down
5 changes: 4 additions & 1 deletion backend/src/modules/bitcoin/block/block.resolver.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,10 @@ import { BitcoinBaseTransaction, BitcoinTransaction } from '../transaction/trans
import { BitcoinAddress, BitcoinBaseAddress } from '../address/address.model';
import { BitcoinBaseBlock, BitcoinBlock, FeeRateRange } from './block.model';
import { BitcoinBlockLoader, BitcoinBlockLoaderType } from './dataloader/block.loader';
import { BitcoinBlockTransactionsLoader, BitcoinBlockTransactionsLoaderType } from './dataloader/block-transactions.loader';
import {
BitcoinBlockTransactionsLoader,
BitcoinBlockTransactionsLoaderType,
} from './dataloader/block-transactions.loader';

@Resolver(() => BitcoinBlock)
export class BitcoinBlockResolver {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -17,7 +17,8 @@ export interface BitcoinBlockTransactionsLoaderParams {
@Injectable()
export class BitcoinBlockTransactionsLoader
extends BitcoinBaseLoader
implements NestDataLoader<BitcoinBlockTransactionsLoaderParams, BitcoinApi.Transaction[] | null> {
implements NestDataLoader<BitcoinBlockTransactionsLoaderParams, BitcoinApi.Transaction[] | null>
{
protected logger = new Logger(BitcoinBlockTransactionsLoader.name);

constructor(
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -15,7 +15,8 @@ export interface BitcoinBlockTxidsLoaderParams {
@Injectable()
export class BitcoinBlockTxidsLoader
extends BitcoinBaseLoader
implements NestDataLoader<BitcoinBlockTxidsLoaderParams, string[] | null> {
implements NestDataLoader<BitcoinBlockTxidsLoaderParams, string[] | null>
{
protected logger = new Logger(BitcoinBlockTxidsLoader.name);

constructor(
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -16,7 +16,7 @@ import { BitcoinBlockLoader, BitcoinBlockLoaderType } from '../block/dataloader/

@Resolver(() => BitcoinTransaction)
export class BitcoinTransactionResolver {
constructor(private bitcoinApiService: BitcoinApiService) { }
constructor(private bitcoinApiService: BitcoinApiService) {}

@Query(() => BitcoinTransaction, { name: 'btcTransaction', nullable: true })
public async getTransaction(
Expand Down Expand Up @@ -47,7 +47,7 @@ export class BitcoinTransactionResolver {
@Loader(BitcoinBlockLoader) blockLoader: BitcoinBlockLoaderType,
): Promise<BitcoinBaseBlock | null> {
if (!tx.blockHash) {
return null
return null;
}
const block = await blockLoader.load(tx.blockHash);
if (!block) {
Expand Down
10 changes: 7 additions & 3 deletions backend/src/modules/ckb/block/block.dataloader.ts
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,7 @@ import { CkbBlockService } from './block.service';
import { InjectSentry, SentryService } from '@ntegral/nestjs-sentry';

@Injectable()
export class CkbRpcBlockLoader implements NestDataLoader<string, CkbRpc.Block | null> {
export class CkbRpcBlockLoader implements NestDataLoader<string, CkbRpc.Block | null> {
private logger = new Logger(CkbRpcBlockLoader.name);

constructor(
Expand All @@ -26,7 +26,9 @@ export class CkbRpcBlockLoader implements NestDataLoader<string, CkbRpc.Block |
if (result.status === 'fulfilled') {
return result.value;
}
this.logger.error(`Requesting: ${heightOrHashList[index]}, occurred error: ${result.reason}`);
this.logger.error(
`Requesting: ${heightOrHashList[index]}, occurred error: ${result.reason}`,
);
this.sentryService.instance().captureException(result.reason);
return null;
});
Expand Down Expand Up @@ -57,7 +59,9 @@ export class CkbExplorerBlockLoader implements NestDataLoader<string, CkbExplore
if (result.status === 'fulfilled') {
return result.value;
}
this.logger.error(`Requesting: ${heightOrHashList[index]}, occurred error: ${result.reason}`);
this.logger.error(
`Requesting: ${heightOrHashList[index]}, occurred error: ${result.reason}`,
);
this.sentryService.instance().captureException(result.reason);
return null;
});
Expand Down
2 changes: 1 addition & 1 deletion backend/src/modules/ckb/cell/cell.resolver.ts
Original file line number Diff line number Diff line change
Expand Up @@ -17,7 +17,7 @@ export class CkbCellResolver {
constructor(
private ckbCellService: CkbCellService,
private ckbScriptService: CkbScriptService,
) { }
) {}

@ResolveField(() => CkbXUDTInfo, { nullable: true })
public async xudtInfo(
Expand Down
Loading

0 comments on commit 9b26ce2

Please sign in to comment.