9aa12184a1
- Implemented a unified `getAndSet` method in RedisService to handle caching for single, list, and paginated responses. - Removed redundant cache checks and writes in various services, simplifying the code and improving readability. - Updated GoodsService, StockKeepingUnitsService, PartnerActivatedLicensesService, and others to utilize the new caching mechanism. - Adjusted Prisma connection limit for better resource management.
258 lines
6.6 KiB
TypeScript
258 lines
6.6 KiB
TypeScript
import {
|
|
MapperWrapper,
|
|
Paginated,
|
|
ResponseMapper,
|
|
} from '@/common/response/response-mapper'
|
|
import { Injectable, Logger, OnModuleDestroy } from '@nestjs/common'
|
|
import Redis from 'ioredis'
|
|
|
|
interface PaginatedCache<T> {
|
|
items: T[]
|
|
total: number
|
|
page?: number
|
|
perPage?: number
|
|
}
|
|
|
|
@Injectable()
|
|
export class RedisService implements OnModuleDestroy {
|
|
private static readonly scanCount = 100
|
|
private readonly logger = new Logger(RedisService.name)
|
|
private readonly client: Redis
|
|
|
|
constructor() {
|
|
this.client = new Redis({
|
|
host: process.env.REDIS_HOST || 'redis',
|
|
port: Number(process.env.REDIS_PORT || 6379),
|
|
password: process.env.REDIS_PASSWORD || undefined,
|
|
db: Number(process.env.REDIS_DB || 0),
|
|
lazyConnect: true,
|
|
maxRetriesPerRequest: 3,
|
|
})
|
|
|
|
this.client.on('error', error => {
|
|
this.logger.error(`Redis error: ${error.message}`)
|
|
})
|
|
}
|
|
|
|
async getClient(): Promise<Redis> {
|
|
if (this.client.status !== 'ready') {
|
|
await this.client.connect()
|
|
}
|
|
|
|
return this.client
|
|
}
|
|
|
|
async get(key: string): Promise<string | null> {
|
|
const client = await this.getClient()
|
|
return client.get(key)
|
|
}
|
|
|
|
async getJson<T>(key: string): Promise<T | null> {
|
|
const value = await this.get(key)
|
|
if (!value) {
|
|
return null
|
|
}
|
|
|
|
try {
|
|
return JSON.parse(value) as T
|
|
} catch {
|
|
this.logger.warn(`Invalid JSON for key "${key}"`)
|
|
return null
|
|
}
|
|
}
|
|
|
|
async set(key: string, value: string, ttlSeconds?: number): Promise<'OK' | null> {
|
|
const client = await this.getClient()
|
|
if (ttlSeconds && ttlSeconds > 0) {
|
|
return client.set(key, value, 'EX', ttlSeconds)
|
|
}
|
|
return client.set(key, value)
|
|
}
|
|
|
|
async setJson(key: string, value: unknown, ttlSeconds?: number): Promise<'OK' | null> {
|
|
return this.set(key, JSON.stringify(value), ttlSeconds)
|
|
}
|
|
|
|
async delete(key: string): Promise<number> {
|
|
const client = await this.getClient()
|
|
return client.del(key)
|
|
}
|
|
|
|
async exists(key: string): Promise<boolean> {
|
|
const client = await this.getClient()
|
|
const result = await client.exists(key)
|
|
return result === 1
|
|
}
|
|
|
|
async expire(key: string, ttlSeconds: number): Promise<boolean> {
|
|
const client = await this.getClient()
|
|
const result = await client.expire(key, ttlSeconds)
|
|
return result === 1
|
|
}
|
|
|
|
async deleteByPattern(pattern: string): Promise<number> {
|
|
const client = await this.getClient()
|
|
let deletedCount = 0
|
|
let cursor = '0'
|
|
|
|
do {
|
|
const [nextCursor, keys] = await client.scan(
|
|
cursor,
|
|
'MATCH',
|
|
pattern,
|
|
'COUNT',
|
|
RedisService.scanCount,
|
|
)
|
|
cursor = nextCursor
|
|
|
|
if (keys.length > 0) {
|
|
const results = await this.deleteKeys(client, keys)
|
|
if (results) {
|
|
deletedCount += results.reduce((sum, [, value]) => {
|
|
return sum + (typeof value === 'number' ? value : 0)
|
|
}, 0)
|
|
}
|
|
}
|
|
} while (cursor !== '0')
|
|
|
|
return deletedCount
|
|
}
|
|
|
|
async deleteByPatterns(patterns: string[]): Promise<number> {
|
|
const deletedCounts = await Promise.all(
|
|
patterns.map(pattern => this.deleteByPattern(pattern)),
|
|
)
|
|
return deletedCounts.reduce((sum, count) => sum + count, 0)
|
|
}
|
|
|
|
async getAndSet<T>(
|
|
key: string,
|
|
type: 'single',
|
|
callback: () => Promise<T>,
|
|
ttlSeconds?: number,
|
|
): Promise<MapperWrapper<T>>
|
|
async getAndSet<T>(
|
|
key: string,
|
|
type: 'list',
|
|
callback: () => Promise<T[]>,
|
|
ttlSeconds?: number,
|
|
): Promise<MapperWrapper<T>>
|
|
async getAndSet<T>(
|
|
key: string,
|
|
type: 'paginate',
|
|
callback: () => Promise<PaginatedCache<T>>,
|
|
ttlSeconds?: number,
|
|
): Promise<Paginated<T>>
|
|
async getAndSet<T>(
|
|
key: string,
|
|
type: 'single' | 'list' | 'paginate',
|
|
callback: () => Promise<T | T[] | PaginatedCache<T>>,
|
|
ttlSeconds?: number,
|
|
): Promise<MapperWrapper<T> | Paginated<T>> {
|
|
switch (type) {
|
|
case 'single':
|
|
return this.getAndSetSingle(key, callback as () => Promise<T>, ttlSeconds)
|
|
case 'list':
|
|
return this.getAndSetList(key, callback as () => Promise<T[]>, ttlSeconds)
|
|
case 'paginate':
|
|
return this.getAndSetPaginate(
|
|
key,
|
|
callback as () => Promise<PaginatedCache<T>>,
|
|
ttlSeconds,
|
|
)
|
|
default: {
|
|
const neverType: never = type
|
|
throw new Error(`Unsupported cache type: ${neverType}`)
|
|
}
|
|
}
|
|
}
|
|
|
|
private async getAndSetSingle<T>(
|
|
key: string,
|
|
callback: () => Promise<T>,
|
|
ttlSeconds?: number,
|
|
): Promise<MapperWrapper<T>> {
|
|
const cached = await this.tryReadJson<T>(key)
|
|
if (cached !== null) {
|
|
return ResponseMapper.single(cached)
|
|
}
|
|
|
|
const data = await callback()
|
|
await this.tryWriteJson(key, data, ttlSeconds)
|
|
return ResponseMapper.single(data)
|
|
}
|
|
|
|
private async getAndSetList<T>(
|
|
key: string,
|
|
callback: () => Promise<T[]>,
|
|
ttlSeconds?: number,
|
|
): Promise<MapperWrapper<T>> {
|
|
const cached = await this.tryReadJson<T[]>(key)
|
|
if (cached !== null) {
|
|
return ResponseMapper.list(cached)
|
|
}
|
|
|
|
const data = await callback()
|
|
await this.tryWriteJson(key, data, ttlSeconds)
|
|
return ResponseMapper.list(data)
|
|
}
|
|
|
|
private async getAndSetPaginate<T>(
|
|
key: string,
|
|
callback: () => Promise<PaginatedCache<T>>,
|
|
ttlSeconds?: number,
|
|
): Promise<Paginated<T>> {
|
|
const cached = await this.tryReadJson<PaginatedCache<T>>(key)
|
|
if (cached !== null) {
|
|
return ResponseMapper.paginate(cached.items, {
|
|
total: cached.total,
|
|
page: cached.page,
|
|
perPage: cached.perPage,
|
|
})
|
|
}
|
|
|
|
const data = await callback()
|
|
await this.tryWriteJson(key, data, ttlSeconds)
|
|
return ResponseMapper.paginate(data.items, {
|
|
total: data.total,
|
|
page: data.page,
|
|
perPage: data.perPage,
|
|
})
|
|
}
|
|
|
|
private async tryReadJson<T>(key: string): Promise<T | null> {
|
|
try {
|
|
return await this.getJson<T>(key)
|
|
} catch (error) {
|
|
this.logger.warn(`Redis read failed for "${key}": ${(error as Error).message}`)
|
|
return null
|
|
}
|
|
}
|
|
|
|
private async tryWriteJson(
|
|
key: string,
|
|
value: unknown,
|
|
ttlSeconds: number = 300,
|
|
): Promise<void> {
|
|
try {
|
|
await this.setJson(key, value, ttlSeconds)
|
|
} catch (error) {
|
|
this.logger.warn(`Redis write failed for "${key}": ${(error as Error).message}`)
|
|
}
|
|
}
|
|
|
|
private async deleteKeys(
|
|
client: Redis,
|
|
keys: string[],
|
|
): Promise<Array<[Error | null, unknown]>> {
|
|
const pipeline = client.pipeline()
|
|
keys.forEach(key => pipeline.del(key))
|
|
const results = await pipeline.exec()
|
|
return results ?? []
|
|
}
|
|
|
|
async onModuleDestroy() {
|
|
await this.client.quit()
|
|
}
|
|
}
|