SPB Git forge

spb/rareindex

Public
54commits 1branches 0releases
7.1 MBsize
maindefault branch
10 days agolast push
TypeScript 61.9% HTML 37.2% SQL 0.7%
1.7 KB · 37 lines typescript
Raw Blame History
1import 'server-only';2import { sql } from '@/lib/db';3import { db } from '@/lib/db';45/**6 * Fixed-window counter stored in Postgres so limits hold across PM2 instances.7 * Returns whether the call is allowed and the seconds until the window resets.8 */9export async function rateLimit(key: string, limit: number, windowSeconds: number): Promise<{ ok: boolean; remaining: number; retryAfterSeconds: number }> {10  const now = new Date();11  const resetAt = new Date(now.getTime() + windowSeconds * 1000);12  const nowIso = now.toISOString();13  const resetIso = resetAt.toISOString();14  const rows = (await db().execute(sql`15    insert into rate_limits (key, count, reset_at) values (${key}, 1, ${resetIso}::timestamptz)16    on conflict (key) do update set17      count = case when rate_limits.reset_at <= ${nowIso}::timestamptz then 1 else rate_limits.count + 1 end,18      reset_at = case when rate_limits.reset_at <= ${nowIso}::timestamptz then ${resetIso}::timestamptz else rate_limits.reset_at end19    returning count, reset_at20  `)) as unknown as Array<{ count: number; reset_at: Date | string }>;21  const row = rows[0]!;22  const count = Number(row.count);23  const reset = new Date(row.reset_at);24  return { ok: count <= limit, remaining: Math.max(0, limit - count), retryAfterSeconds: Math.max(1, Math.ceil((reset.getTime() - now.getTime()) / 1000)) };25}2627export class RateLimited extends Error {28  constructor(public retryAfterSeconds: number) {29    super(`Too many attempts. Try again in ${retryAfterSeconds}s.`);30  }31}3233export async function enforce(key: string, limit: number, windowSeconds: number): Promise<void> {34  const r = await rateLimit(key, limit, windowSeconds);35  if (!r.ok) throw new RateLimited(r.retryAfterSeconds);36}37