시간 도메인과 정지 중 메시지·베팅 경계 정리

This commit is contained in:
2026-09-03 16:01:19 +00:00
parent 10cddbb565
commit abceee8315
108 changed files with 3504 additions and 1473 deletions
+27 -3
View File
@@ -9,13 +9,18 @@ declare const gameTickBrand: unique symbol;
declare const observedGameInstantBrand: unique symbol;
declare const scheduleInstantBrand: unique symbol;
declare const clockRevisionBrand: unique symbol;
declare const deadlineGenerationBrand: unique symbol;
declare const wallInstantBrand: unique symbol;
declare const monotonicDurationBrand: unique symbol;
export type GameTick = number & { readonly [gameTickBrand]: 'GameTick' };
export type ObservedGameInstant = GameTick & { readonly [observedGameInstantBrand]: 'ObservedGameInstant' };
export type ScheduleInstant = GameTick & { readonly [scheduleInstantBrand]: 'ScheduleInstant' };
export type ClockRevision = number & { readonly [clockRevisionBrand]: 'ClockRevision' };
export type DeadlineGeneration = number & { readonly [deadlineGenerationBrand]: 'DeadlineGeneration' };
export type WallInstant = Date & { readonly [wallInstantBrand]: 'WallInstant' };
/** Process-local elapsed milliseconds; never persist this value as a business timestamp. */
export type MonotonicDuration = number & { readonly [monotonicDurationBrand]: 'MonotonicDuration' };
export interface ClockAlignmentPlan {
policy: ClockAlignmentPolicy;
@@ -59,6 +64,13 @@ export const asClockRevision = (revision: number): ClockRevision => {
return revision as ClockRevision;
};
export const asDeadlineGeneration = (generation: number): DeadlineGeneration => {
if (!Number.isSafeInteger(generation) || generation < 1) {
throw new Error(`Deadline generation must be a positive safe integer: ${generation}`);
}
return generation as DeadlineGeneration;
};
export const asWallInstant = (instant: Date): WallInstant => {
if (Number.isNaN(instant.getTime())) {
throw new Error('Wall instant must be a valid date.');
@@ -66,6 +78,13 @@ export const asWallInstant = (instant: Date): WallInstant => {
return new Date(instant.getTime()) as WallInstant;
};
export const asMonotonicDuration = (milliseconds: number): MonotonicDuration => {
if (!Number.isFinite(milliseconds) || milliseconds < 0) {
throw new Error(`Monotonic duration must be a non-negative finite number: ${milliseconds}`);
}
return milliseconds as MonotonicDuration;
};
export const inferClockPhase = (mode: GameClockMode): GameClockPhase => (mode === 'manual' ? 'MANUAL' : 'RUNNING');
const GAME_CLOCK_PHASES: readonly GameClockPhase[] = [
@@ -136,8 +155,7 @@ const buildAlignmentPlan = (input: {
const remainingMilliseconds = elapsedMilliseconds - wholeSeconds * 1_000;
const gapTicks = asGameTick(
requireSafeTick(
wholeSeconds * input.ticksPerSecond +
Math.trunc((remainingMilliseconds * input.ticksPerSecond) / 1_000)
wholeSeconds * input.ticksPerSecond + Math.trunc((remainingMilliseconds * input.ticksPerSecond) / 1_000)
)
);
const catchUpTicks = asGameTick(input.catchUpTicks ?? 0);
@@ -250,7 +268,13 @@ export class GameClock {
}
nowTick(wallNow: Date): number {
if (this.mode === 'manual' || this.phase === 'MANUAL' || this.phase === 'COMPLETED') {
if (
this.mode === 'manual' ||
this.phase === 'MANUAL' ||
this.phase === 'SUSPENDED' ||
this.phase === 'RECONCILING' ||
this.phase === 'COMPLETED'
) {
return this.tick;
}
const elapsedTicks = this.ticksBetween(this.wallAnchor, wallNow);
@@ -24,7 +24,7 @@ export interface TournamentClockFence {
phaseKey: string;
revision: number;
deadlineGeneration: number;
phase: 'RUNNING';
phase: 'RUNNING' | 'MANUAL' | 'SUSPENDED';
}
const WRITE_TOURNAMENT_PROJECTION_SCRIPT = `
+3 -10
View File
@@ -190,7 +190,7 @@ export type TurnDaemonCommand =
requestId?: string;
auctionId: number;
expectedCloseAt?: string;
expectedCloseTick?: number;
expectedCloseTick: number;
}
| {
type: 'auctionOpen';
@@ -260,7 +260,6 @@ export type TurnDaemonCommand =
voteId: number;
generalId: number;
selection: number[];
acceptedGameTick?: number;
}
| {
type: 'setNationSetting';
@@ -386,15 +385,12 @@ export type TurnDaemonCommand =
ownerLegacyPenalty?: Record<string, unknown>;
generalId: number;
tokenNonce: number;
acceptedGameAt?: string;
}
| {
type: 'selectPoolReserve';
requestId?: string;
userId: string;
seedOwnerIdentity: string | number;
acceptedGameAt: string;
acceptedGameTick?: number;
}
| {
type: 'selectPoolCreate';
@@ -407,8 +403,6 @@ export type TurnDaemonCommand =
ownerPicture?: string;
ownerImageServer?: number;
ownerIconRevision?: string;
acceptedGameAt?: string;
acceptedGameTick?: number;
}
| {
type: 'selectPoolReselect';
@@ -416,8 +410,6 @@ export type TurnDaemonCommand =
userId: string;
ownerDisplayName: string;
uniqueName: string;
acceptedGameAt?: string;
acceptedGameTick?: number;
}
| {
type: 'auctionBid';
@@ -426,7 +418,6 @@ export type TurnDaemonCommand =
auctionId: number;
generalId: number;
amount: number;
acceptedGameTick?: number;
tryExtendCloseDate?: boolean;
};
@@ -505,6 +496,7 @@ export type TurnDaemonCommandResult =
ok: true;
auctionId: number;
closeAt: string;
closeTick: number;
}
| {
type: 'auctionOpen';
@@ -835,6 +827,7 @@ export type TurnDaemonCommandResult =
ok: true;
auctionId: number;
closeAt: string;
closeTick: number;
}
| {
type: 'auctionBid';