File size: 1,666 Bytes
57a889c
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
import { Injectable } from '@nestjs/common';

interface Attempt { count: number; first: number }

/**
 * In-memory per-IP rate limiter, ported 1:1 from the legacy auth route's
 * `rateLimiter`. Each named bucket keeps its own attempt map; `check` returns
 * false once a key exceeds `max` within `windowMs` (the caller answers 429).
 *
 * The legacy route also ran a setInterval to garbage-collect expired records;
 * that was pure memory housekeeping (the window check below already treats an
 * expired record as fresh), so it is intentionally omitted — the limit
 * behaviour is identical and there's no dangling timer to leak in tests.
 */
@Injectable()
export class RateLimitService {
  private readonly buckets = new Map<string, Map<string, Attempt>>();

  private store(bucket: string): Map<string, Attempt> {
    let s = this.buckets.get(bucket);
    if (!s) { s = new Map(); this.buckets.set(bucket, s); }
    return s;
  }

  /** Returns true when the request is allowed, false when it should be rejected (429). */
  check(bucket: string, key: string, max: number, windowMs: number, now: number): boolean {
    const store = this.store(bucket);
    const record = store.get(key);
    if (record && record.count >= max && now - record.first < windowMs) {
      return false;
    }
    if (!record || now - record.first >= windowMs) {
      store.set(key, { count: 1, first: now });
    } else {
      record.count++;
    }
    return true;
  }

  /** Test helper: clear a bucket (mirrors the legacy exported maps used for resets). */
  reset(bucket?: string): void {
    if (bucket) this.buckets.get(bucket)?.clear();
    else this.buckets.clear();
  }
}