SPB Git forge

spb/spinza

Public
8commits 1branches 0releases
1.6 MBsize
maindefault branch
16 days agolast push
TypeScript 97.6% SQL 1.4% JavaScript 0.5%
1.7 KB · 53 lines typescript
Raw Blame History
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