feat: complete tournament API lifecycle
This commit is contained in:
@@ -1,9 +1,19 @@
|
||||
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<string | null>;
|
||||
set(key: string, value: string): Promise<unknown>;
|
||||
set(
|
||||
key: string,
|
||||
value: string,
|
||||
options?: {
|
||||
NX?: boolean;
|
||||
PX?: number;
|
||||
}
|
||||
): Promise<unknown>;
|
||||
del?(key: string): Promise<unknown>;
|
||||
}
|
||||
|
||||
const safeJsonParse = <T>(raw: string | null): T | null => {
|
||||
@@ -18,7 +28,34 @@ const safeJsonParse = <T>(raw: string | null): T | null => {
|
||||
};
|
||||
|
||||
export class TournamentStore {
|
||||
constructor(private readonly redis: RedisClientLike, private readonly keys: TournamentKeys) {}
|
||||
constructor(
|
||||
private readonly redis: RedisClientLike,
|
||||
private readonly keys: TournamentKeys
|
||||
) {}
|
||||
|
||||
async withMutationLock<T>(operation: () => Promise<T>, timeoutMs = 2_000): Promise<T> {
|
||||
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<TournamentState | null> {
|
||||
return safeJsonParse<TournamentState>(await this.redis.get(this.keys.stateKey));
|
||||
@@ -54,7 +91,14 @@ export class TournamentStore {
|
||||
|
||||
async appendBettingEntry(entry: TournamentBetEntry): Promise<TournamentBetEntry[]> {
|
||||
const entries = await this.getBettingEntries();
|
||||
entries.push(entry);
|
||||
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;
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user