- Add ioredis dependency for Redis connection pooling - Create RedisService singleton with connection management - Add Redis config (url, dedupWindowSeconds) to notification.config.ts - Implement NotificationService.checkRateLimit using Redis INCR+EXPIRE - Implement NotificationService.deduplicateNotification using Redis SET/NX - Add configurable rate limit windows and thresholds via env vars - Add 29 unit tests covering Redis operations, rate limiting, and dedup - All tests pass, TypeScript compiles cleanly for new files
74 lines
1.9 KiB
TypeScript
74 lines
1.9 KiB
TypeScript
import { Redis, RedisOptions } from 'ioredis';
|
|
|
|
export interface RedisServiceConfig {
|
|
url?: string;
|
|
options?: RedisOptions;
|
|
}
|
|
|
|
export class RedisService {
|
|
private static instance: RedisService;
|
|
private client: Redis;
|
|
|
|
private constructor(config?: RedisServiceConfig) {
|
|
const url = config?.url || process.env.REDIS_URL || 'redis://localhost:6379';
|
|
this.client = new Redis(url, config?.options || {});
|
|
}
|
|
|
|
static getInstance(config?: RedisServiceConfig): RedisService {
|
|
if (!RedisService.instance) {
|
|
RedisService.instance = new RedisService(config);
|
|
}
|
|
return RedisService.instance;
|
|
}
|
|
|
|
getClient(): Redis {
|
|
return this.client;
|
|
}
|
|
|
|
async isConnected(): Promise<boolean> {
|
|
return this.client.status === 'ready';
|
|
}
|
|
|
|
async increment(key: string, expirySeconds?: number): Promise<number> {
|
|
const current = await this.client.incr(key);
|
|
if (current === 1 && expirySeconds) {
|
|
await this.client.expire(key, expirySeconds);
|
|
}
|
|
return current;
|
|
}
|
|
|
|
async getCounter(key: string): Promise<number> {
|
|
const value = await this.client.get(key);
|
|
return value ? parseInt(value, 10) : 0;
|
|
}
|
|
|
|
async setWithExpiry(key: string, value: string, seconds: number): Promise<'OK' | 1> {
|
|
return this.client.setex(key, seconds, value);
|
|
}
|
|
|
|
async setIfNotExists(key: string, value: string, seconds: number): Promise<boolean> {
|
|
const result = await this.client.set(key, value, 'EX', seconds, 'NX');
|
|
return result === 'OK';
|
|
}
|
|
|
|
async get(key: string): Promise<string | null> {
|
|
return this.client.get(key);
|
|
}
|
|
|
|
async ttl(key: string): Promise<number> {
|
|
return this.client.ttl(key);
|
|
}
|
|
|
|
async delete(key: string): Promise<number> {
|
|
return this.client.del(key);
|
|
}
|
|
|
|
async getTTL(key: string): Promise<number> {
|
|
return this.client.ttl(key);
|
|
}
|
|
|
|
async close(): Promise<void> {
|
|
await this.client.quit();
|
|
}
|
|
}
|