44 getDocsFromServer ,
55 increment ,
66 setDoc ,
7+ Timestamp ,
78} from 'firebase/firestore' ;
89import { getDb } from './db' ;
910
@@ -15,6 +16,7 @@ interface RateLimitOptions {
1516interface RateLimitDocument {
1617 count : number ;
1718 windowStart : number ;
19+ expiresAt ?: Timestamp ;
1820}
1921
2022export interface RateLimitState {
@@ -27,6 +29,9 @@ export interface RateLimitState {
2729const DEFAULT_WINDOW_MS = 60_000 ;
2830const DEFAULT_MAX_REQUESTS = 30 ;
2931const SHARD_COUNT = 20 ;
32+ // Documents are cleaned up via Firestore TTL configured on the `expiresAt` field.
33+ // Keeping a modest retention allows slow TTL sweeps without impacting active windows.
34+ const RETENTION_WINDOWS = 24 ; // number of windows to keep for TTL cleanup
3035
3136export async function enforceRateLimit (
3237 key : string ,
@@ -38,10 +43,11 @@ export async function enforceRateLimit(
3843 const windowRef = collection ( db , 'rateLimits' , `${ key } :${ windowStart } ` , 'shards' ) ;
3944 const shardId = Math . floor ( Math . random ( ) * SHARD_COUNT ) . toString ( ) ;
4045 const shardRef = doc ( windowRef , shardId ) ;
46+ const expiresAt = Timestamp . fromMillis ( windowStart + windowMs * RETENTION_WINDOWS ) ;
4147
4248 await setDoc (
4349 shardRef ,
44- { count : increment ( 1 ) , windowStart } ,
50+ { count : increment ( 1 ) , windowStart, expiresAt } ,
4551 { merge : true } ,
4652 ) ;
4753
0 commit comments