feat: 시계 reconciliation 워커와 명령 경계 완성

This commit is contained in:
2026-09-03 09:40:48 +00:00
parent ae7d55ef47
commit a3e2bf90ae
46 changed files with 3011 additions and 219 deletions
+46 -5
View File
@@ -17,8 +17,8 @@ export type ScheduleInstant = GameTick & { readonly [scheduleInstantBrand]: 'Sch
export type ClockRevision = number & { readonly [clockRevisionBrand]: 'ClockRevision' };
export type WallInstant = Date & { readonly [wallInstantBrand]: 'WallInstant' };
export interface ExactClockAlignmentPlan {
policy: 'EXACT';
export interface ClockAlignmentPlan {
policy: ClockAlignmentPolicy;
sourceRevision: ClockRevision;
targetRevision: ClockRevision;
cutTick: GameTick;
@@ -84,6 +84,15 @@ export const parseGameClockPhase = (value: string): GameClockPhase => {
throw new Error(`Unknown game clock phase: ${value}`);
};
const CLOCK_ALIGNMENT_POLICIES: readonly ClockAlignmentPolicy[] = ['EXACT', 'LEGACY_COMPLETE_TURNS', 'CATCH_UP'];
export const parseClockAlignmentPolicy = (value: string): ClockAlignmentPolicy => {
if ((CLOCK_ALIGNMENT_POLICIES as readonly string[]).includes(value)) {
return value as ClockAlignmentPolicy;
}
throw new Error(`Unknown clock alignment policy: ${value}`);
};
export const scheduleNotBefore = (instant: ObservedGameInstant, phase: GameClockPhase): ScheduleInstant => {
if (phase === 'PREOPEN') {
return asScheduleInstant(Math.max(0, instant));
@@ -103,14 +112,15 @@ export const assertGameplayCommitAllowed = (phase: GameClockPhase): void => {
}
};
export const buildExactClockAlignmentPlan = (input: {
const buildAlignmentPlan = (input: {
policy: ClockAlignmentPolicy;
sourceRevision: number;
cutTick: number;
cutWall: Date;
resumeWall: Date;
ticksPerSecond: number;
catchUpTicks?: number;
}): ExactClockAlignmentPlan => {
}): ClockAlignmentPlan => {
const sourceRevision = asClockRevision(input.sourceRevision);
const cutTick = asGameTick(input.cutTick);
const cutWall = asWallInstant(input.cutWall);
@@ -136,7 +146,7 @@ export const buildExactClockAlignmentPlan = (input: {
}
const shiftTicks = asGameTick(gapTicks - catchUpTicks);
return {
policy: 'EXACT',
policy: input.policy,
sourceRevision,
targetRevision: asClockRevision(sourceRevision + 1),
cutTick,
@@ -147,6 +157,37 @@ export const buildExactClockAlignmentPlan = (input: {
};
};
export const buildExactClockAlignmentPlan = (
input: Omit<Parameters<typeof buildAlignmentPlan>[0], 'policy'>
): ClockAlignmentPlan => buildAlignmentPlan({ ...input, policy: 'EXACT' });
export const buildClockAlignmentPlan = (input: {
policy: ClockAlignmentPolicy;
sourceRevision: number;
cutTick: number;
cutWall: Date;
resumeWall: Date;
ticksPerSecond: number;
catchUpTicks?: number;
}): ClockAlignmentPlan => {
if (input.policy === 'EXACT') {
if ((input.catchUpTicks ?? 0) !== 0) {
throw new Error('EXACT alignment does not allow catch-up ticks.');
}
return buildAlignmentPlan({ ...input, policy: 'EXACT', catchUpTicks: 0 });
}
if (input.policy === 'CATCH_UP') {
return buildAlignmentPlan({ ...input, policy: 'CATCH_UP' });
}
const exact = buildAlignmentPlan({ ...input, policy: 'LEGACY_COMPLETE_TURNS', catchUpTicks: 0 });
const shiftTicks = asGameTick(Math.floor(exact.gapTicks / GAME_TICKS_PER_TURN) * GAME_TICKS_PER_TURN);
return {
...exact,
catchUpTicks: asGameTick(exact.gapTicks - shiftTicks),
shiftTicks,
};
};
const tickOffsetMilliseconds = (tick: number, ticksPerSecond: number): number => {
const wholeSeconds = Math.floor(tick / ticksPerSecond);
const remainingTicks = tick - wholeSeconds * ticksPerSecond;