refactor: streamline Redis caching logic across services

- 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.
This commit is contained in:
2026-05-21 17:27:37 +03:30
parent 1d47fb1a1d
commit 9aa12184a1
16 changed files with 689 additions and 1896 deletions
+148 -24
View File
@@ -1,27 +1,35 @@
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() {
const host = process.env.REDIS_HOST || 'redis'
const port = Number(process.env.REDIS_PORT || 6379)
const password = process.env.REDIS_PASSWORD || undefined
const db = Number(process.env.REDIS_DB || 0)
this.client = new Redis({
host,
port,
password,
db,
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.client.on('error', error => {
this.logger.error(`Redis error: ${error.message}`)
})
}
@@ -53,11 +61,7 @@ export class RedisService implements OnModuleDestroy {
}
}
async set(
key: string,
value: string,
ttlSeconds?: number,
): Promise<'OK' | 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)
@@ -65,11 +69,7 @@ export class RedisService implements OnModuleDestroy {
return client.set(key, value)
}
async setJson(
key: string,
value: unknown,
ttlSeconds?: number,
): Promise<'OK' | null> {
async setJson(key: string, value: unknown, ttlSeconds?: number): Promise<'OK' | null> {
return this.set(key, JSON.stringify(value), ttlSeconds)
}
@@ -101,14 +101,12 @@ export class RedisService implements OnModuleDestroy {
'MATCH',
pattern,
'COUNT',
100,
RedisService.scanCount,
)
cursor = nextCursor
if (keys.length > 0) {
const pipeline = client.pipeline()
keys.forEach(k => pipeline.del(k))
const results = await pipeline.exec()
const results = await this.deleteKeys(client, keys)
if (results) {
deletedCount += results.reduce((sum, [, value]) => {
return sum + (typeof value === 'number' ? value : 0)
@@ -127,6 +125,132 @@ export class RedisService implements OnModuleDestroy {
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()
}