TypeScript 97.6%
SQL 1.4%
JavaScript 0.5%
1import Redis from "ioredis";2import { config } from "../config";34let _redis: Redis | null = null;56export function redis(): Redis {7 if (!_redis) {8 _redis = new Redis(config.redisUrl, { maxRetriesPerRequest: 2, enableOfflineQueue: true, lazyConnect: false });9 _redis.on("error", (e) => console.error("[redis]", e.message));10 }11 return _redis;12}1314export async function pingRedis(): Promise<number> {15 const t = Date.now();16 await redis().ping();17 return Date.now() - t;18}1920export async function closeRedis(): Promise<void> {21 if (_redis) {22 await _redis.quit();23 _redis = null;24 }25}2627/**28 * Sliding-window rate limit (fixed window with two buckets). Returns retry-after29 * seconds when exceeded, else 0.30 */31export async function rateLimit(key: string, limit: number, windowSec: number): Promise<number> {32 const r = redis();33 const now = Math.floor(Date.now() / 1000);34 const bucket = Math.floor(now / windowSec);35 const k = `rl:${key}:${bucket}`;36 const prevK = `rl:${key}:${bucket - 1}`;37 const [[, cur], [, prev]] = (await r.multi().incr(k).get(prevK).exec()) as [[null, number], [null, string | null]];38 if (cur === 1) await r.expire(k, windowSec * 2);39 const elapsed = (now % windowSec) / windowSec;40 const weighted = cur + Number(prev ?? 0) * (1 - elapsed);41 if (weighted > limit) return Math.max(1, windowSec - (now % windowSec));42 return 0;43}4445export async function setJson(key: string, value: unknown, ttlSec: number): Promise<void> {46 await redis().set(key, JSON.stringify(value), "EX", ttlSec);47}4849export async function getJson<T>(key: string): Promise<T | null> {50 const v = await redis().get(key);51 return v ? (JSON.parse(v) as T) : null;52}53