Files
joke-app/apps/server/src/ws/rate-limit.ts
EJClaw 6ee8da18f1 M1 기반: 모노레포, 공통 엔진, 공용 프로토콜, 오목 엔진, 서버 골격
- packages/engine: 시드 RNG(sfc32), GameDefinition 타입, 무작위 대국·정보 유출 테스트 도구
- packages/shared: WS 프로토콜 스키마, 닉네임·방 코드 규칙, 게임 카탈로그
- packages/games/omok: 한국식·렌주·자유룰, 재귀 금수 판정, 무르기·무승부·시계 (테스트 27개)
- apps/server: SQLite 스키마, 게스트·디스코드 로그인, 세션, 방 관리(저장·복구·타이머·재접속), WS 게이트웨이, HTTP API

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-04 04:18:29 +09:00

46 lines
1.1 KiB
TypeScript

/** Token bucket (docs/03 §8). */
export class TokenBucket {
private tokens: number;
private last: number;
constructor(
private ratePerSec: number,
private burst: number,
now = Date.now(),
) {
this.tokens = burst;
this.last = now;
}
take(now = Date.now()): boolean {
this.tokens = Math.min(this.burst, this.tokens + ((now - this.last) / 1000) * this.ratePerSec);
this.last = now;
if (this.tokens < 1) return false;
this.tokens -= 1;
return true;
}
}
/** Fixed-window counter per key (HTTP endpoints). */
export class WindowLimiter {
private hits = new Map<string, { start: number; n: number }>();
constructor(
private limit: number,
private windowMs: number,
) {}
allow(key: string, now = Date.now()): boolean {
const h = this.hits.get(key);
if (!h || now - h.start >= this.windowMs) {
this.hits.set(key, { start: now, n: 1 });
if (this.hits.size > 50_000) this.prune(now);
return true;
}
h.n++;
return h.n <= this.limit;
}
private prune(now: number): void {
for (const [k, v] of this.hits) if (now - v.start >= this.windowMs) this.hits.delete(k);
}
}