fix: 토너먼트 단계의 메인 실시간 갱신을 연결

토너먼트 state의 stage 전이만 공용 실시간 채널에 발행하고 공개 invalidation으로 변환한다.
메인 dashboard store가 단계 값을 소유해 상단 문구와 메뉴 강조를 단일/다중 탭에서 함께 갱신한다.
Redis 통합 테스트와 production Chromium 회귀 검증을 추가한다.
This commit is contained in:
2026-08-17 01:41:37 +00:00
parent 1a4cd1a53f
commit 50068224c8
17 changed files with 392 additions and 31 deletions
@@ -1,7 +1,12 @@
import type { GamePrisma, GamePrismaClient } from '@sammo-ts/infra';
import { randomUUID } from 'node:crypto';
import { writeTournamentProjection, type TurnDaemonCommand, type TurnDaemonCommandResult } from '@sammo-ts/common';
import {
buildGameEventChannel,
writeTournamentProjection,
type TurnDaemonCommand,
type TurnDaemonCommandResult,
} from '@sammo-ts/common';
import type { GatewayAdminActionRecord, GatewayAdminActionResult } from './gatewayAdminActions.js';
@@ -74,8 +79,10 @@ const shiftTournamentClock = async (
): Promise<boolean> => {
const stateKey = `sammo:${profileName}:tournament:state`;
const sourceKeys = {
stateKey,
sourceRevisionKey: `sammo:${profileName}:tournament:source-revision`,
sourceRevisionChannel: `sammo:${profileName}:tournament:source-changed`,
realtimeEventChannel: buildGameEventChannel(profileName),
};
const lockKey = `${stateKey}:mutation-lock`;
const token = randomUUID();
@@ -1,4 +1,10 @@
import { asRecord, LiteHashDRBG, RandUtil, writeTournamentProjection } from '@sammo-ts/common';
import {
asRecord,
buildGameEventChannel,
LiteHashDRBG,
RandUtil,
writeTournamentProjection,
} from '@sammo-ts/common';
import type { RedisConnector } from '@sammo-ts/infra';
import { LogCategory, LogFormat, LogScope } from '@sammo-ts/logic';
import { simpleSerialize } from '@sammo-ts/logic/war/utils.js';
@@ -69,12 +75,13 @@ export const createTournamentAutoStartHandler = (options: {
now?: () => Date;
}): TurnCalendarHandler => {
const keys = {
state: `sammo:${options.profileName}:tournament:state`,
stateKey: `sammo:${options.profileName}:tournament:state`,
participants: `sammo:${options.profileName}:tournament:participants`,
matches: `sammo:${options.profileName}:tournament:matches`,
betting: `sammo:${options.profileName}:tournament:betting`,
sourceRevisionKey: `sammo:${options.profileName}:tournament:source-revision`,
sourceRevisionChannel: `sammo:${options.profileName}:tournament:source-changed`,
realtimeEventChannel: buildGameEventChannel(options.profileName),
};
return {
onMonthChanged: async (context) => {
@@ -85,7 +92,7 @@ export const createTournamentAutoStartHandler = (options: {
if (!world || !redis || config.tournamentTrig !== true) {
return;
}
const previousState = safeJsonParse<TournamentState>(await redis.get(keys.state));
const previousState = safeJsonParse<TournamentState>(await redis.get(keys.stateKey));
if (previousState && previousState.stage > 0) {
return;
}
@@ -145,7 +152,7 @@ export const createTournamentAutoStartHandler = (options: {
{ key: keys.participants, value: [] },
{ key: keys.matches, value: [] },
{ key: keys.betting, value: [] },
{ key: keys.state, value: nextState },
{ key: keys.stateKey, value: nextState },
]);
const [typeText, generalTypeText] = TOURNAMENT_TEXT[type] ?? TOURNAMENT_TEXT[0];