FRE-4521 Implement Redis integration for rate limiting and deduplication
- 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
This commit is contained in:
73
packages/shared-notifications/src/services/redis.service.ts
Normal file
73
packages/shared-notifications/src/services/redis.service.ts
Normal file
@@ -0,0 +1,73 @@
|
||||
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();
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user