feat: 게임 시계 reconciliation 권위 기반 추가

This commit is contained in:
2026-09-03 08:44:43 +00:00
parent b91dcbcaaa
commit ae7d55ef47
29 changed files with 1265 additions and 81 deletions
+140 -2
View File
@@ -2,6 +2,31 @@ export const GAME_TICKS_PER_TURN = 36_000_000;
export const MAX_SAFE_GAME_TICK = Number.MAX_SAFE_INTEGER;
export type GameClockMode = 'realtime' | 'manual';
export type GameClockPhase = 'PREOPEN' | 'RUNNING' | 'SUSPENDED' | 'RECONCILING' | 'MANUAL' | 'COMPLETED';
export type ClockAlignmentPolicy = 'EXACT' | 'LEGACY_COMPLETE_TURNS' | 'CATCH_UP';
declare const gameTickBrand: unique symbol;
declare const observedGameInstantBrand: unique symbol;
declare const scheduleInstantBrand: unique symbol;
declare const clockRevisionBrand: unique symbol;
declare const wallInstantBrand: 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 WallInstant = Date & { readonly [wallInstantBrand]: 'WallInstant' };
export interface ExactClockAlignmentPlan {
policy: 'EXACT';
sourceRevision: ClockRevision;
targetRevision: ClockRevision;
cutTick: GameTick;
gapTicks: GameTick;
catchUpTicks: GameTick;
shiftTicks: GameTick;
alignedTick: GameTick;
}
export interface GameClockState {
baseTime: Date;
@@ -9,6 +34,8 @@ export interface GameClockState {
mode: GameClockMode;
wallAnchor: Date;
turnSeconds: number;
phase?: GameClockPhase;
revision?: number;
}
const requireSafeTick = (tick: number): number => {
@@ -18,6 +45,108 @@ const requireSafeTick = (tick: number): number => {
return tick;
};
export const asGameTick = (tick: number): GameTick => requireSafeTick(tick) as GameTick;
export const asObservedGameInstant = (tick: number): ObservedGameInstant =>
requireSafeTick(tick) as ObservedGameInstant;
export const asScheduleInstant = (tick: number): ScheduleInstant => requireSafeTick(tick) as ScheduleInstant;
export const asClockRevision = (revision: number): ClockRevision => {
if (!Number.isSafeInteger(revision) || revision < 1) {
throw new Error(`Clock revision must be a positive safe integer: ${revision}`);
}
return revision as ClockRevision;
};
export const asWallInstant = (instant: Date): WallInstant => {
if (Number.isNaN(instant.getTime())) {
throw new Error('Wall instant must be a valid date.');
}
return new Date(instant.getTime()) as WallInstant;
};
export const inferClockPhase = (mode: GameClockMode): GameClockPhase => (mode === 'manual' ? 'MANUAL' : 'RUNNING');
const GAME_CLOCK_PHASES: readonly GameClockPhase[] = [
'PREOPEN',
'RUNNING',
'SUSPENDED',
'RECONCILING',
'MANUAL',
'COMPLETED',
];
export const parseGameClockPhase = (value: string): GameClockPhase => {
if ((GAME_CLOCK_PHASES as readonly string[]).includes(value)) {
return value as GameClockPhase;
}
throw new Error(`Unknown game clock phase: ${value}`);
};
export const scheduleNotBefore = (instant: ObservedGameInstant, phase: GameClockPhase): ScheduleInstant => {
if (phase === 'PREOPEN') {
return asScheduleInstant(Math.max(0, instant));
}
return asScheduleInstant(instant);
};
export const createDeadline = (
instant: ObservedGameInstant,
durationTicks: GameTick,
phase: GameClockPhase
): ScheduleInstant => scheduleNotBefore(asObservedGameInstant(requireSafeTick(instant + durationTicks)), phase);
export const assertGameplayCommitAllowed = (phase: GameClockPhase): void => {
if (phase !== 'RUNNING' && phase !== 'MANUAL') {
throw new Error(`Gameplay commit is forbidden while the game clock phase is ${phase}.`);
}
};
export const buildExactClockAlignmentPlan = (input: {
sourceRevision: number;
cutTick: number;
cutWall: Date;
resumeWall: Date;
ticksPerSecond: number;
catchUpTicks?: number;
}): ExactClockAlignmentPlan => {
const sourceRevision = asClockRevision(input.sourceRevision);
const cutTick = asGameTick(input.cutTick);
const cutWall = asWallInstant(input.cutWall);
const resumeWall = asWallInstant(input.resumeWall);
if (!Number.isSafeInteger(input.ticksPerSecond) || input.ticksPerSecond <= 0) {
throw new Error(`ticksPerSecond must be a positive safe integer: ${input.ticksPerSecond}`);
}
const elapsedMilliseconds = Math.max(0, resumeWall.getTime() - cutWall.getTime());
if (!Number.isSafeInteger(elapsedMilliseconds)) {
throw new Error('Clock suspension wall gap is outside the safe integer range.');
}
const wholeSeconds = Math.trunc(elapsedMilliseconds / 1_000);
const remainingMilliseconds = elapsedMilliseconds - wholeSeconds * 1_000;
const gapTicks = asGameTick(
requireSafeTick(
wholeSeconds * input.ticksPerSecond +
Math.trunc((remainingMilliseconds * input.ticksPerSecond) / 1_000)
)
);
const catchUpTicks = asGameTick(input.catchUpTicks ?? 0);
if (catchUpTicks < 0 || catchUpTicks > gapTicks) {
throw new Error(`catchUpTicks must be between 0 and the wall gap (${gapTicks}): ${catchUpTicks}`);
}
const shiftTicks = asGameTick(gapTicks - catchUpTicks);
return {
policy: 'EXACT',
sourceRevision,
targetRevision: asClockRevision(sourceRevision + 1),
cutTick,
gapTicks,
catchUpTicks,
shiftTicks,
alignedTick: asGameTick(cutTick + gapTicks),
};
};
const tickOffsetMilliseconds = (tick: number, ticksPerSecond: number): number => {
const wholeSeconds = Math.floor(tick / ticksPerSecond);
const remainingTicks = tick - wholeSeconds * ticksPerSecond;
@@ -35,6 +164,8 @@ export class GameClock {
readonly wallAnchor: Date;
readonly turnSeconds: number;
readonly ticksPerSecond: number;
readonly phase: GameClockPhase;
readonly revision: ClockRevision;
constructor(state: GameClockState) {
if (!Number.isInteger(state.turnSeconds) || state.turnSeconds <= 0) {
@@ -55,6 +186,8 @@ export class GameClock {
this.wallAnchor = new Date(state.wallAnchor.getTime());
this.turnSeconds = state.turnSeconds;
this.ticksPerSecond = GAME_TICKS_PER_TURN / state.turnSeconds;
this.phase = state.phase ?? inferClockPhase(state.mode);
this.revision = asClockRevision(state.revision ?? 1);
}
static baseTimeForProjection(projectedTime: Date, tick: number, turnSeconds: number): Date {
@@ -76,14 +209,19 @@ export class GameClock {
}
nowTick(wallNow: Date): number {
if (this.mode === 'manual') {
if (this.mode === 'manual' || this.phase === 'MANUAL' || this.phase === 'COMPLETED') {
return this.tick;
}
const elapsedTicks = this.ticksBetween(this.wallAnchor, wallNow);
// A future realtime anchor represents the formal opening at anchor tick.
// Before that instant Ref exposes the elapsed offset as a negative tick,
// which lets PREOPEN-only actions keep their logical cooldowns moving.
return this.addTicks(this.tick, elapsedTicks);
const projectedTick = this.addTicks(this.tick, elapsedTicks);
// PREOPEN is the only phase where the opening anchor may project a
// signed negative coordinate. Once a realtime game is running, an NTP
// rewind must never make the observed coordinate decrease below the
// last durable clock snapshot.
return this.phase === 'PREOPEN' ? projectedTick : Math.max(this.tick, projectedTick);
}
now(wallNow: Date): Date {
+66 -1
View File
@@ -1,6 +1,15 @@
import { describe, expect, it } from 'vitest';
import { GAME_TICKS_PER_TURN, GameClock, MAX_SAFE_GAME_TICK } from '../src/time/GameClock.js';
import {
GAME_TICKS_PER_TURN,
GameClock,
MAX_SAFE_GAME_TICK,
asGameTick,
asObservedGameInstant,
buildExactClockAlignmentPlan,
createDeadline,
scheduleNotBefore,
} from '../src/time/GameClock.js';
describe('GameClock', () => {
const baseTime = new Date('2042-01-01T00:00:00.000Z');
@@ -42,12 +51,68 @@ describe('GameClock', () => {
mode: 'realtime',
wallAnchor: new Date('2026-01-01T01:00:00.000Z'),
turnSeconds: 3_600,
phase: 'PREOPEN',
});
expect(clock.nowTick(new Date('2026-01-01T00:30:00.000Z'))).toBe(-GAME_TICKS_PER_TURN / 2);
expect(clock.nowTick(new Date('2026-01-01T01:00:00.000Z'))).toBe(0);
});
it('does not rewind a RUNNING realtime tick when wall time moves backward', () => {
const clock = new GameClock({
baseTime,
tick: GAME_TICKS_PER_TURN,
mode: 'realtime',
wallAnchor: new Date('2026-01-01T01:00:00.000Z'),
turnSeconds: 3_600,
phase: 'RUNNING',
revision: 7,
});
expect(clock.nowTick(new Date('2026-01-01T00:59:55.000Z'))).toBe(GAME_TICKS_PER_TURN);
expect(clock.revision).toBe(7);
});
it('floors PREOPEN executable schedules at opening tick zero', () => {
const observed = asObservedGameInstant(-GAME_TICKS_PER_TURN / 2);
expect(scheduleNotBefore(observed, 'PREOPEN')).toBe(0);
expect(createDeadline(observed, asGameTick(GAME_TICKS_PER_TURN / 4), 'PREOPEN')).toBe(0);
expect(scheduleNotBefore(observed, 'RUNNING')).toBe(observed);
});
it('preserves a 65 minute 17.250 second sub-turn suspension remainder exactly', () => {
const plan = buildExactClockAlignmentPlan({
sourceRevision: 11,
cutTick: 123_456,
cutWall: new Date('2026-01-01T00:00:00.000Z'),
resumeWall: new Date('2026-01-01T01:05:17.250Z'),
ticksPerSecond: 10_000,
});
expect(plan).toMatchObject({
sourceRevision: 11,
targetRevision: 12,
gapTicks: 39_172_500,
shiftTicks: 39_172_500,
alignedTick: 39_295_956,
});
expect(plan.shiftTicks % GAME_TICKS_PER_TURN).toBe(3_172_500);
});
it('aligns a 24 hour exact maintenance without catch-up', () => {
const plan = buildExactClockAlignmentPlan({
sourceRevision: 1,
cutTick: 2 * GAME_TICKS_PER_TURN,
cutWall: new Date('2026-01-01T00:00:00.000Z'),
resumeWall: new Date('2026-01-02T00:00:00.000Z'),
ticksPerSecond: 10_000,
});
expect(plan.gapTicks).toBe(24 * GAME_TICKS_PER_TURN);
expect(plan.alignedTick).toBe(26 * GAME_TICKS_PER_TURN);
});
it('projects near the safe tick boundary without unsafe intermediate multiplication', () => {
const clock = new GameClock({
baseTime: new Date(0),
+107 -29
View File
@@ -63,21 +63,23 @@ enum InputEventTarget {
}
model InputEvent {
sequence BigInt @id @default(autoincrement())
requestId String @unique @map("request_id")
target InputEventTarget
eventType String @map("event_type")
payload Json @default(dbgenerated("'{}'::jsonb"))
actorUserId String? @map("actor_user_id")
status InputEventStatus @default(PENDING)
result Json?
error String?
attempts Int @default(0)
lockedBy String? @map("locked_by")
leaseUntil DateTime? @map("lease_until")
createdAt DateTime @default(now()) @map("created_at")
processingAt DateTime? @map("processing_at")
completedAt DateTime? @map("completed_at")
sequence BigInt @id @default(autoincrement())
requestId String @unique @map("request_id")
target InputEventTarget
eventType String @map("event_type")
payload Json @default(dbgenerated("'{}'::jsonb"))
actorUserId String? @map("actor_user_id")
acceptedGameTick BigInt? @map("accepted_game_tick")
acceptedClockRevision BigInt? @map("accepted_clock_revision")
status InputEventStatus @default(PENDING)
result Json?
error String?
attempts Int @default(0)
lockedBy String? @map("locked_by")
leaseUntil DateTime? @map("lease_until")
createdAt DateTime @default(now()) @map("created_at")
processingAt DateTime? @map("processing_at")
completedAt DateTime? @map("completed_at")
@@index([target, status, sequence])
@@map("input_event")
@@ -145,25 +147,101 @@ model TurnDaemonLease {
}
model WorldState {
id Int @id @default(autoincrement())
scenarioCode String @map("scenario_code")
currentYear Int @map("current_year")
currentMonth Int @map("current_month")
tickSeconds Int @map("tick_seconds")
clockBaseTime DateTime? @map("clock_base_time")
clockTick BigInt? @map("clock_tick")
clockMode String @default("realtime") @map("clock_mode")
clockWallAnchor DateTime? @map("clock_wall_anchor")
lastTurnTick BigInt? @map("last_turn_tick")
config Json @default(dbgenerated("'{}'::jsonb"))
meta Json @default(dbgenerated("'{}'::jsonb"))
updatedAt DateTime @updatedAt @map("updated_at")
id Int @id @default(autoincrement())
scenarioCode String @map("scenario_code")
currentYear Int @map("current_year")
currentMonth Int @map("current_month")
tickSeconds Int @map("tick_seconds")
clockBaseTime DateTime? @map("clock_base_time")
clockTick BigInt? @map("clock_tick")
clockMode String @default("realtime") @map("clock_mode")
clockWallAnchor DateTime? @map("clock_wall_anchor")
lastTurnTick BigInt? @map("last_turn_tick")
clockPhase String @default("RUNNING") @map("clock_phase")
clockRevision BigInt @default(1) @map("clock_revision")
deadlineGeneration BigInt @default(1) @map("deadline_generation")
config Json @default(dbgenerated("'{}'::jsonb"))
meta Json @default(dbgenerated("'{}'::jsonb"))
updatedAt DateTime @updatedAt @map("updated_at")
trafficPeriods TrafficPeriod[]
trafficPeriods TrafficPeriod[]
clockSuspensions ClockSuspension[]
clockProjectionOutbox ClockProjectionOutbox[]
@@map("world_state")
}
model ClockSuspension {
id String @id @db.VarChar(64)
worldStateId Int @map("world_state_id")
source String
policy String
status String @default("SUSPENDED")
sourceRevision BigInt @map("source_revision")
targetRevision BigInt @map("target_revision")
cutTick BigInt @map("cut_tick")
cutWallAt DateTime @map("cut_wall_at") @db.Timestamp(3)
resumeWallAt DateTime? @map("resume_wall_at") @db.Timestamp(3)
rateTicksPerSecond Int @map("rate_ticks_per_second")
catchUpTicks BigInt @default(0) @map("catch_up_ticks")
gapTicks BigInt? @map("gap_ticks")
shiftTicks BigInt? @map("shift_ticks")
alignedTick BigInt? @map("aligned_tick")
participantChecksumBefore String? @map("participant_checksum_before")
participantChecksumAfter String? @map("participant_checksum_after")
detail Json @default(dbgenerated("'{}'::jsonb"))
createdAt DateTime @default(dbgenerated("(CURRENT_TIMESTAMP AT TIME ZONE 'UTC')")) @map("created_at") @db.Timestamp(3)
updatedAt DateTime @default(dbgenerated("(CURRENT_TIMESTAMP AT TIME ZONE 'UTC')")) @updatedAt @map("updated_at") @db.Timestamp(3)
worldState WorldState @relation(fields: [worldStateId], references: [id], onDelete: Cascade)
participants ClockReconciliationParticipant[]
projectionOutbox ClockProjectionOutbox[]
@@unique([worldStateId, targetRevision])
@@index([status, createdAt])
@@map("clock_suspension")
}
model ClockReconciliationParticipant {
suspensionId String @map("suspension_id") @db.VarChar(64)
participantKey String @map("participant_key") @db.VarChar(96)
policy String
beforeChecksum String @map("before_checksum") @db.VarChar(128)
afterChecksum String @map("after_checksum") @db.VarChar(128)
affectedCount Int @default(0) @map("affected_count")
detail Json @default(dbgenerated("'{}'::jsonb"))
suspension ClockSuspension @relation(fields: [suspensionId], references: [id], onDelete: Cascade)
@@id([suspensionId, participantKey])
@@map("clock_reconciliation_participant")
}
model ClockProjectionOutbox {
id BigInt @id @default(autoincrement())
worldStateId Int @map("world_state_id")
suspensionId String? @map("suspension_id") @db.VarChar(64)
targetRevision BigInt @map("target_revision")
status String @default("PENDING")
payload Json @default(dbgenerated("'{}'::jsonb"))
checksum String @db.VarChar(128)
attempts Int @default(0)
availableAt DateTime @default(dbgenerated("(CURRENT_TIMESTAMP AT TIME ZONE 'UTC')")) @map("available_at") @db.Timestamp(3)
lockedAt DateTime? @map("locked_at") @db.Timestamp(3)
lockedBy String? @map("locked_by") @db.VarChar(128)
appliedAt DateTime? @map("applied_at") @db.Timestamp(3)
lastError String? @map("last_error")
createdAt DateTime @default(dbgenerated("(CURRENT_TIMESTAMP AT TIME ZONE 'UTC')")) @map("created_at") @db.Timestamp(3)
updatedAt DateTime @default(dbgenerated("(CURRENT_TIMESTAMP AT TIME ZONE 'UTC')")) @updatedAt @map("updated_at") @db.Timestamp(3)
worldState WorldState @relation(fields: [worldStateId], references: [id], onDelete: Cascade)
suspension ClockSuspension? @relation(fields: [suspensionId], references: [id], onDelete: Cascade)
@@unique([worldStateId, targetRevision])
@@index([status, availableAt, id])
@@map("clock_projection_outbox")
}
model Nation {
id Int @id
name String
@@ -0,0 +1,81 @@
ALTER TABLE world_state
ADD COLUMN clock_phase TEXT NOT NULL DEFAULT 'RUNNING',
ADD COLUMN clock_revision BIGINT NOT NULL DEFAULT 1,
ADD COLUMN deadline_generation BIGINT NOT NULL DEFAULT 1;
UPDATE world_state
SET clock_phase = CASE
WHEN clock_mode = 'manual' THEN 'MANUAL'
WHEN clock_wall_anchor IS NOT NULL AND clock_wall_anchor > CURRENT_TIMESTAMP THEN 'PREOPEN'
ELSE 'RUNNING'
END;
ALTER TABLE input_event
ADD COLUMN accepted_game_tick BIGINT,
ADD COLUMN accepted_clock_revision BIGINT;
CREATE TABLE clock_suspension (
id VARCHAR(64) PRIMARY KEY,
world_state_id INTEGER NOT NULL REFERENCES world_state(id) ON DELETE CASCADE,
source TEXT NOT NULL,
policy TEXT NOT NULL,
status TEXT NOT NULL DEFAULT 'SUSPENDED',
source_revision BIGINT NOT NULL,
target_revision BIGINT NOT NULL,
cut_tick BIGINT NOT NULL,
cut_wall_at TIMESTAMP(3) NOT NULL,
resume_wall_at TIMESTAMP(3),
rate_ticks_per_second INTEGER NOT NULL,
catch_up_ticks BIGINT NOT NULL DEFAULT 0,
gap_ticks BIGINT,
shift_ticks BIGINT,
aligned_tick BIGINT,
participant_checksum_before VARCHAR(128),
participant_checksum_after VARCHAR(128),
detail JSONB NOT NULL DEFAULT '{}'::jsonb,
created_at TIMESTAMP(3) NOT NULL DEFAULT (CURRENT_TIMESTAMP AT TIME ZONE 'UTC'),
updated_at TIMESTAMP(3) NOT NULL DEFAULT (CURRENT_TIMESTAMP AT TIME ZONE 'UTC'),
CONSTRAINT clock_suspension_world_revision_key UNIQUE (world_state_id, target_revision),
CONSTRAINT clock_suspension_revision_step CHECK (target_revision = source_revision + 1),
CONSTRAINT clock_suspension_nonnegative_catchup CHECK (catch_up_ticks >= 0),
CONSTRAINT clock_suspension_positive_rate CHECK (rate_ticks_per_second > 0)
);
CREATE INDEX clock_suspension_status_created_at_idx ON clock_suspension(status, created_at);
CREATE TABLE clock_reconciliation_participant (
suspension_id VARCHAR(64) NOT NULL REFERENCES clock_suspension(id) ON DELETE CASCADE,
participant_key VARCHAR(96) NOT NULL,
policy TEXT NOT NULL,
before_checksum VARCHAR(128) NOT NULL,
after_checksum VARCHAR(128) NOT NULL,
affected_count INTEGER NOT NULL DEFAULT 0,
detail JSONB NOT NULL DEFAULT '{}'::jsonb,
PRIMARY KEY (suspension_id, participant_key),
CONSTRAINT clock_reconciliation_participant_policy_check CHECK (policy IN ('SHIFT', 'KEEP', 'REBUILD', 'FORBID')),
CONSTRAINT clock_reconciliation_participant_count_check CHECK (affected_count >= 0)
);
CREATE TABLE clock_projection_outbox (
id BIGSERIAL PRIMARY KEY,
world_state_id INTEGER NOT NULL REFERENCES world_state(id) ON DELETE CASCADE,
suspension_id VARCHAR(64) REFERENCES clock_suspension(id) ON DELETE CASCADE,
target_revision BIGINT NOT NULL,
status TEXT NOT NULL DEFAULT 'PENDING',
payload JSONB NOT NULL DEFAULT '{}'::jsonb,
checksum VARCHAR(128) NOT NULL,
attempts INTEGER NOT NULL DEFAULT 0,
available_at TIMESTAMP(3) NOT NULL DEFAULT (CURRENT_TIMESTAMP AT TIME ZONE 'UTC'),
locked_at TIMESTAMP(3),
locked_by VARCHAR(128),
applied_at TIMESTAMP(3),
last_error TEXT,
created_at TIMESTAMP(3) NOT NULL DEFAULT (CURRENT_TIMESTAMP AT TIME ZONE 'UTC'),
updated_at TIMESTAMP(3) NOT NULL DEFAULT (CURRENT_TIMESTAMP AT TIME ZONE 'UTC'),
CONSTRAINT clock_projection_outbox_world_revision_key UNIQUE (world_state_id, target_revision),
CONSTRAINT clock_projection_outbox_status_check CHECK (status IN ('PENDING', 'APPLYING', 'APPLIED', 'FAILED')),
CONSTRAINT clock_projection_outbox_attempts_check CHECK (attempts >= 0)
);
CREATE INDEX clock_projection_outbox_status_available_at_id_idx
ON clock_projection_outbox(status, available_at, id);
@@ -8,6 +8,8 @@ interface TryLockRow {
/** Serializes score/traffic writers whose table lock order otherwise differs by entry point. */
export const GENERAL_ACCESS_PERSISTENCE_LOCK = 'general-access:persistence';
/** Serializes phase/revision changes with every gameplay flush in one game schema. */
export const CLOCK_OPERATION_PERSISTENCE_LOCK = 'game-clock:operation';
const lockKeySql = (logicalKey: string): GamePrisma.Sql =>
GamePrisma.sql`hashtextextended(current_schema() || chr(31) || ${logicalKey}, 0)`;
+9
View File
@@ -16,6 +16,9 @@ export interface TurnEngineWorldStateRow {
clockMode: string;
clockWallAnchor: Date | null;
lastTurnTick: bigint | null;
clockPhase: string;
clockRevision: bigint;
deadlineGeneration: bigint;
config: JsonValue;
meta: JsonValue;
updatedAt?: Date | null;
@@ -181,6 +184,9 @@ export interface TurnEngineWorldStateUpdateInput {
clockMode: string;
clockWallAnchor: Date;
lastTurnTick: bigint;
clockPhase: string;
clockRevision: bigint;
deadlineGeneration: bigint;
config: InputJsonValue;
meta: InputJsonValue;
}
@@ -195,6 +201,9 @@ export interface TurnEngineWorldStateCreateInput {
clockMode: string;
clockWallAnchor: Date;
lastTurnTick: bigint;
clockPhase: string;
clockRevision: bigint;
deadlineGeneration: bigint;
config: InputJsonValue;
meta: InputJsonValue;
}