import Redis from "ioredis"; import { config } from "../config"; let _redis: Redis | null = null; export function redis(): Redis { if (!_redis) { _redis = new Redis(config.redisUrl, { maxRetriesPerRequest: 2, enableOfflineQueue: true, lazyConnect: false }); _redis.on("error", (e) => console.error("[redis]", e.message)); } return _redis; } export async function pingRedis(): Promise { const t = Date.now(); await redis().ping(); return Date.now() - t; } export async function closeRedis(): Promise { if (_redis) { await _redis.quit(); _redis = null; } } /** * Sliding-window rate limit (fixed window with two buckets). Returns retry-after * seconds when exceeded, else 0. */ export async function rateLimit(key: string, limit: number, windowSec: number): Promise { const r = redis(); const now = Math.floor(Date.now() / 1000); const bucket = Math.floor(now / windowSec); const k = `rl:${key}:${bucket}`; const prevK = `rl:${key}:${bucket - 1}`; const [[, cur], [, prev]] = (await r.multi().incr(k).get(prevK).exec()) as [[null, number], [null, string | null]]; if (cur === 1) await r.expire(k, windowSec * 2); const elapsed = (now % windowSec) / windowSec; const weighted = cur + Number(prev ?? 0) * (1 - elapsed); if (weighted > limit) return Math.max(1, windowSec - (now % windowSec)); return 0; } export async function setJson(key: string, value: unknown, ttlSec: number): Promise { await redis().set(key, JSON.stringify(value), "EX", ttlSec); } export async function getJson(key: string): Promise { const v = await redis().get(key); return v ? (JSON.parse(v) as T) : null; }