diff --git a/src/analytics/services/batch-size-guard.service.ts b/src/analytics/services/batch-size-guard.service.ts new file mode 100644 index 00000000..92214d6a --- /dev/null +++ b/src/analytics/services/batch-size-guard.service.ts @@ -0,0 +1,23 @@ +import { Injectable } from '@nestjs/common'; + +@Injectable() +export class BatchSizeGuardService { + private readonly maxSize: number = 10000; + private droppedCount: number = 0; + + canAdd(currentSize: number): boolean { + if (currentSize >= this.maxSize) { + this.droppedCount++; + return false; + } + return true; + } + + getDroppedCount(): number { + return this.droppedCount; + } + + resetDroppedCount(): void { + this.droppedCount = 0; + } +} diff --git a/src/common/interceptors/request-dedup.interceptor.ts b/src/common/interceptors/request-dedup.interceptor.ts new file mode 100644 index 00000000..99ccb7c1 --- /dev/null +++ b/src/common/interceptors/request-dedup.interceptor.ts @@ -0,0 +1,33 @@ +import { Injectable, NestInterceptor, ExecutionContext, CallHandler } from '@nestjs/common'; +import { Observable, of } from 'rxjs'; +import { tap } from 'rxjs/operators'; + +@Injectable() +export class RequestDedupInterceptor implements NestInterceptor { + private cache = new Map(); + private readonly ttlMs = 60000; + + intercept(context: ExecutionContext, next: CallHandler): Observable { + const req = context.switchToHttp().getRequest(); + if (req.method !== 'POST') return next.handle(); + + const key = this.fingerprint(req); + const cached = this.cache.get(key); + if (cached && Date.now() - cached.timestamp < this.ttlMs) { + const res = context.switchToHttp().getResponse(); + res.setHeader('X-Duplicate-Request', 'true'); + return of(cached.response); + } + + return next.handle().pipe( + tap((response) => { + this.cache.set(key, { response, timestamp: Date.now() }); + }), + ); + } + + private fingerprint(req: any): string { + const id = req.user ? req.user.id : 'anon'; + return `${req.method}:${req.path}:${JSON.stringify(req.body)}:${id}`; + } +}