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:
@@ -2,6 +2,8 @@ import { EmailService } from './email.service';
|
||||
import { SMSService } from './sms.service';
|
||||
import { PushService } from './push.service';
|
||||
import { TemplateService } from './template.service';
|
||||
import { RedisService } from './redis.service';
|
||||
import { loadNotificationConfig } from '../config/notification.config';
|
||||
import type {
|
||||
Notification,
|
||||
NotificationChannel,
|
||||
@@ -11,11 +13,21 @@ import type {
|
||||
} from '../types/notification.types';
|
||||
import type { TemplateResolutionOptions } from '../types/template.types';
|
||||
|
||||
export interface RateLimitResult {
|
||||
allowed: boolean;
|
||||
currentCount: number;
|
||||
limit: number;
|
||||
remaining: number;
|
||||
resetInSeconds: number;
|
||||
}
|
||||
|
||||
export class NotificationService {
|
||||
private static instance: NotificationService;
|
||||
private emailService: EmailService;
|
||||
private smsService: SMSService;
|
||||
private pushService: PushService;
|
||||
private redisService: RedisService;
|
||||
private config: ReturnType<typeof loadNotificationConfig>;
|
||||
private pendingDeduplication = new Map<string, Set<string>>();
|
||||
private preferenceCache = new Map<string, NotificationPreference>();
|
||||
|
||||
@@ -23,6 +35,8 @@ export class NotificationService {
|
||||
this.emailService = EmailService.getInstance();
|
||||
this.smsService = SMSService.getInstance();
|
||||
this.pushService = PushService.getInstance();
|
||||
this.config = loadNotificationConfig();
|
||||
this.redisService = RedisService.getInstance({ url: this.config.redis.url });
|
||||
}
|
||||
|
||||
static getInstance(): NotificationService {
|
||||
@@ -204,7 +218,65 @@ export class NotificationService {
|
||||
}
|
||||
}
|
||||
|
||||
async checkRateLimit(
|
||||
identifier: string,
|
||||
channel: NotificationChannel,
|
||||
customLimit?: number,
|
||||
customWindowSeconds?: number
|
||||
): Promise<RateLimitResult> {
|
||||
const limit = customLimit || this.getLimitForChannel(channel);
|
||||
const windowSeconds = customWindowSeconds || this.config.rateLimits.windowSeconds;
|
||||
const key = `rl:${channel}:${identifier}`;
|
||||
|
||||
const currentCount = await this.redisService.increment(key, windowSeconds);
|
||||
const ttl = await this.redisService.getTTL(key);
|
||||
|
||||
return {
|
||||
allowed: currentCount <= limit,
|
||||
currentCount,
|
||||
limit,
|
||||
remaining: Math.max(0, limit - currentCount),
|
||||
resetInSeconds: Math.max(1, ttl),
|
||||
};
|
||||
}
|
||||
|
||||
async deduplicateNotification(
|
||||
dedupKey: DeduplicationKey,
|
||||
customWindowSeconds?: number
|
||||
): Promise<boolean> {
|
||||
const dedupId = `dedup:${dedupKey.userId}:${dedupKey.templateId}:${dedupKey.key}`;
|
||||
const windowSeconds = customWindowSeconds || dedupKey.windowSeconds || this.config.redis.dedupWindowSeconds;
|
||||
|
||||
const wasSet = await this.redisService.setIfNotExists(dedupId, '1', windowSeconds);
|
||||
return wasSet;
|
||||
}
|
||||
|
||||
getRateLimitConfig(): {
|
||||
emailPerMinute: number;
|
||||
smsPerMinute: number;
|
||||
pushPerMinute: number;
|
||||
windowSeconds: number;
|
||||
} {
|
||||
return {
|
||||
emailPerMinute: this.config.rateLimits.emailPerMinute,
|
||||
smsPerMinute: this.config.rateLimits.smsPerMinute,
|
||||
pushPerMinute: this.config.rateLimits.pushPerMinute,
|
||||
windowSeconds: this.config.rateLimits.windowSeconds,
|
||||
};
|
||||
}
|
||||
|
||||
getTemplateService(): TemplateService {
|
||||
return TemplateService.getInstance();
|
||||
}
|
||||
|
||||
private getLimitForChannel(channel: NotificationChannel): number {
|
||||
switch (channel) {
|
||||
case 'email':
|
||||
return this.config.rateLimits.emailPerMinute;
|
||||
case 'sms':
|
||||
return this.config.rateLimits.smsPerMinute;
|
||||
case 'push':
|
||||
return this.config.rateLimits.pushPerMinute;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user