fix(engine): 서버 재개 시 게임 시계를 Ref 기준으로 보정한다

짧은 중단은 순서대로 따라잡고 장기 중단은 턴 간격별 Ref 한도를 넘어선 완전 턴을 일괄 이동한다. 장수와 미완료 경매 시각을 같은 트랜잭션에서 저장하고 표시 시각 지연을 복구한다.
This commit is contained in:
2026-08-23 02:00:45 +00:00
parent ade543f936
commit eaf7ef8c22
10 changed files with 596 additions and 47 deletions
@@ -171,7 +171,8 @@ export class TurnDaemonLifecycle {
}
const nowMs = this.clock.nowMs();
const gameClock = await this.stateStore.loadGameClock?.(new Date(nowMs));
const wallNow = new Date(nowMs);
const gameClock = await this.stateStore.loadGameClock?.(wallNow);
if (gameClock?.mode === 'manual') {
// Ref observes all generals due before one monthly boundary in
// a single snapshot. Manual mode advances directly to that
@@ -186,6 +187,13 @@ export class TurnDaemonLifecycle {
await this.runOnce({ reason: 'schedule', targetTime });
continue;
}
if (
gameClock?.mode === 'realtime' &&
(await this.stateStore.shouldRebaseRealtimeBacklog?.(wallNow)) &&
(await this.rebaseRealtimeBacklog(wallNow))
) {
continue;
}
const gameNowMs = gameClock?.now.getTime() ?? nowMs;
const nextTurnMs = nextRunTime.getTime();
if (gameNowMs >= nextTurnMs) {
@@ -419,6 +427,58 @@ export class TurnDaemonLifecycle {
}
}
private async rebaseRealtimeBacklog(wallNow: Date): Promise<boolean> {
if (!this.stateStore.rebaseRealtimeBacklog) {
return false;
}
const startMs = this.clock.nowMs();
let result: TurnRunResult | null;
try {
const rebaseAndFlush = async (): Promise<TurnRunResult | null> => {
const rebased = await this.stateStore.rebaseRealtimeBacklog!(wallNow);
if (!rebased) {
return null;
}
const nextResult: TurnRunResult = {
lastTurnTime: rebased.lastTurnTime,
processedGenerals: 0,
processedTurns: 0,
durationMs: 0,
partial: false,
checkpoint: rebased.checkpoint,
};
this.status.state = 'flushing';
await this.hooks?.flushChanges?.(nextResult);
return nextResult;
};
result = this.stateManager ? await this.stateManager.transaction(rebaseAndFlush) : await rebaseAndFlush();
} catch (error) {
this.status.running = false;
this.status.state = 'paused';
this.status.paused = true;
this.errorPaused = true;
this.status.lastError = error instanceof Error ? error.message : 'Unknown realtime backlog rebase error.';
await this.hooks?.onRunError?.(error);
// The rebase attempt was handled, albeit as a pause. Do not fall
// through and execute overdue turns against a transaction that
// failed to persist its clock/schedule shift.
return true;
}
if (!result) {
return false;
}
await this.applyRunResult(result, startMs);
this.status.state = 'idle';
try {
await this.hooks?.publishEvents?.(result);
} catch (error) {
this.status.lastError =
error instanceof Error ? error.message : 'Unknown backlog rebase event publication error.';
}
return true;
}
private async applyRunResult(result: TurnRunResult, startMs: number): Promise<void> {
this.status.lastRunAt = new Date(startMs).toISOString();
this.status.lastDurationMs = Math.max(0, this.clock.nowMs() - startMs);