import { randomUUID } from 'node:crypto'; import type { TournamentKeys } from './keys.js'; import type { TournamentBetEntry, TournamentMatchEntry, TournamentParticipantEntry, TournamentState } from './types.js'; interface RedisClientLike { get(key: string): Promise; set( key: string, value: string, options?: { NX?: boolean; PX?: number; } ): Promise; del?(key: string): Promise; } const safeJsonParse = (raw: string | null): T | null => { if (!raw) { return null; } try { return JSON.parse(raw) as T; } catch { return null; } }; export class TournamentStore { constructor( private readonly redis: RedisClientLike, private readonly keys: TournamentKeys ) {} async withMutationLock(operation: () => Promise, timeoutMs = 2_000): Promise { if (!this.redis.del) { return operation(); } const lockKey = `${this.keys.stateKey}:mutation-lock`; const token = randomUUID(); const deadline = Date.now() + timeoutMs; while (Date.now() < deadline) { const acquired = await this.redis.set(lockKey, token, { NX: true, PX: 30_000 }); if (acquired) { try { return await operation(); } finally { if ((await this.redis.get(lockKey)) === token) { await this.redis.del(lockKey); } } } await new Promise((resolve) => setTimeout(resolve, 10)); } throw new Error('토너먼트 요청이 처리 중입니다. 잠시 후 다시 시도해주세요.'); } async getState(): Promise { return safeJsonParse(await this.redis.get(this.keys.stateKey)); } async setState(state: TournamentState): Promise { await this.redis.set(this.keys.stateKey, JSON.stringify(state)); } async getParticipants(): Promise { return safeJsonParse(await this.redis.get(this.keys.participantsKey)) ?? []; } async setParticipants(participants: TournamentParticipantEntry[]): Promise { await this.redis.set(this.keys.participantsKey, JSON.stringify(participants)); } async getMatches(): Promise { return safeJsonParse(await this.redis.get(this.keys.matchesKey)) ?? []; } async setMatches(matches: TournamentMatchEntry[]): Promise { await this.redis.set(this.keys.matchesKey, JSON.stringify(matches)); } async getBettingEntries(): Promise { return safeJsonParse(await this.redis.get(this.keys.bettingKey)) ?? []; } async setBettingEntries(entries: TournamentBetEntry[]): Promise { await this.redis.set(this.keys.bettingKey, JSON.stringify(entries)); } async appendBettingEntry(entry: TournamentBetEntry): Promise { const entries = await this.getBettingEntries(); const existing = entries.find( (candidate) => candidate.generalId === entry.generalId && candidate.targetId === entry.targetId ); if (existing) { existing.amount += entry.amount; } else { entries.push(entry); } await this.setBettingEntries(entries); return entries; } }