refactor: 턴 경계와 12턴 묶음을 보존하는 2배속 복구 구현

This commit is contained in:
2026-09-06 10:04:33 +00:00
parent 63da0921cb
commit d3443ec7f5
53 changed files with 1467 additions and 160 deletions
@@ -0,0 +1,43 @@
import { randomUUID } from 'node:crypto';
import type { GamePrismaClient } from '@sammo-ts/infra';
import {
readClockDatabaseWall,
reconcileClockSuspension,
startClockSuspension,
type ClockOperationAuthority,
} from './clockReconciliation.js';
/** lease를 획득했지만 clock_ready를 공개하기 전, 중단된 복구 또는 새 정전을 처리한다. */
export const prepareRealtimeRecovery = async (
db: GamePrismaClient,
authority: Extract<ClockOperationAuthority, { kind: 'DAEMON' }>,
options: { paused?: boolean } = {}
): Promise<void> => {
const world = await db.worldState.findFirstOrThrow({ orderBy: { id: 'asc' } });
if (world.clockMode !== 'realtime') return;
if (world.clockPhase === 'SUSPENDED') {
if (options.paused) return;
const pending = await db.clockSuspension.findFirst({
where: { worldStateId: world.id, source: 'RECOVERY', policy: 'RECOVER_TURNS', status: 'SUSPENDED' },
orderBy: { sourceRevision: 'desc' },
});
if (pending) await reconcileClockSuspension({ db, suspensionId: pending.id, authority });
return;
}
if (world.clockPhase !== 'RUNNING' || !world.clockWallAnchor || world.clockTick === null) return;
const now = await readClockDatabaseWall(db);
// 가속 중 정상적인 프로세스 교체는 기존 창을 그대로 재사용한다.
// 한 턴 미만의 장애는 잔여 구간 실행만 필요하므로 새 좌표 세대를 만들지 않는다.
if (!options.paused && now.getTime() - world.clockWallAnchor.getTime() < world.tickSeconds * 1_000) return;
const suspensionId = `recovery-${randomUUID()}`;
await startClockSuspension({
db,
suspensionId,
source: 'RECOVERY',
policy: 'RECOVER_TURNS',
authority,
recoverDurableObservation: true,
});
// 이전 버전의 profile 상태만 PAUSED였던 경우에도 명시적 재개 전에는 실행하지 않는다.
if (!options.paused) await reconcileClockSuspension({ db, suspensionId, authority });
};