시간 도메인과 정지 중 메시지·베팅 경계 정리
This commit is contained in:
@@ -16,13 +16,23 @@
|
||||
"world_state.deadline_generation",
|
||||
"general.turn_tick",
|
||||
"general.recent_war_tick",
|
||||
"general.meta.next_change_tick",
|
||||
"select_pool.reserved_until_tick",
|
||||
"select_npc_token.valid_until_tick",
|
||||
"select_npc_token.pick_more_from_tick",
|
||||
"message.time_tick",
|
||||
"message.valid_until_tick",
|
||||
"message.occurred_game_tick",
|
||||
"message_action.created_game_tick",
|
||||
"message_action.expires_game_tick",
|
||||
"message_action.resolved_game_tick",
|
||||
"message_action.clock_revision",
|
||||
"message_action.deadline_generation",
|
||||
"inheritance_ledger.applied_clock_revision",
|
||||
"inheritance_ledger.applied_deadline_generation",
|
||||
"auction.open_tick",
|
||||
"auction.close_tick",
|
||||
"auction_bid.occurred_game_tick",
|
||||
"vote_poll.start_tick",
|
||||
"vote_poll.end_tick",
|
||||
"clock_suspension.source_revision",
|
||||
@@ -34,6 +44,43 @@
|
||||
"clock_suspension.aligned_tick",
|
||||
"clock_projection_outbox.target_revision"
|
||||
],
|
||||
"wallTimeFields": [
|
||||
"input_event.created_at",
|
||||
"input_event.processing_at",
|
||||
"input_event.completed_at",
|
||||
"input_event.lease_until",
|
||||
"read_model_outbox.available_at",
|
||||
"read_model_outbox.locked_at",
|
||||
"read_model_outbox.delivered_at",
|
||||
"web_push_outbox.available_at",
|
||||
"web_push_outbox.locked_at",
|
||||
"web_push_outbox.delivered_at",
|
||||
"turn_daemon_lease.lease_until",
|
||||
"turn_daemon_lease.heartbeat_at",
|
||||
"message.created_at_wall",
|
||||
"message.delete_until_wall",
|
||||
"message.tombstoned_at_wall",
|
||||
"message_read_state.updated_at",
|
||||
"diplomacy_letter.date",
|
||||
"auction_bid.requested_at_wall",
|
||||
"auction_bid.created_at",
|
||||
"auction.finalizing_at",
|
||||
"auction.finished_at",
|
||||
"inheritance_ledger.requested_at_wall",
|
||||
"inheritance_ledger.consumed_at_wall",
|
||||
"inheritance_ledger.created_at_wall",
|
||||
"clock_suspension.cut_wall_at",
|
||||
"clock_suspension.resume_wall_at"
|
||||
],
|
||||
"excludedFromReconciliation": [
|
||||
"all WALL_TIME created_at and updated_at audit fields",
|
||||
"normal message envelope and five-minute deletion lifecycle",
|
||||
"account and inheritance receipt timestamps",
|
||||
"traffic and general-access periods",
|
||||
"notification and outbox delivery/retry timestamps",
|
||||
"daemon, worker, editor, gateway, and release leases",
|
||||
"board, authentication, account, audit, and operator timestamps"
|
||||
],
|
||||
"participants": [
|
||||
{
|
||||
"key": "world-clock",
|
||||
@@ -56,6 +103,13 @@
|
||||
"projectionFields": ["general.turn_time"],
|
||||
"owner": "game-engine/turn-daemon"
|
||||
},
|
||||
{
|
||||
"key": "selection-reselection-deadline",
|
||||
"policy": "SHIFT",
|
||||
"authorityFields": ["general.meta.next_change_tick"],
|
||||
"projectionFields": ["general.meta.next_change", "general.meta.nextChangeAt"],
|
||||
"owner": "game-engine/select-pool"
|
||||
},
|
||||
{
|
||||
"key": "general-recent-war-occurrence",
|
||||
"policy": "KEEP",
|
||||
@@ -67,8 +121,15 @@
|
||||
"key": "auction-open-occurrence",
|
||||
"policy": "KEEP",
|
||||
"authorityFields": ["auction.open_tick"],
|
||||
"projectionFields": ["auction.created_at"],
|
||||
"owner": "game-api/auction"
|
||||
"projectionFields": [],
|
||||
"owner": "game-engine/auction"
|
||||
},
|
||||
{
|
||||
"key": "auction-bid-occurrence",
|
||||
"policy": "KEEP",
|
||||
"authorityFields": ["auction_bid.occurred_game_tick"],
|
||||
"projectionFields": ["auction_bid.event_at"],
|
||||
"owner": "game-engine/auction"
|
||||
},
|
||||
{
|
||||
"key": "auction-deadline",
|
||||
@@ -85,18 +146,37 @@
|
||||
"owner": "game-api/auction-worker"
|
||||
},
|
||||
{
|
||||
"key": "message-occurrence",
|
||||
"key": "message-action-occurrence",
|
||||
"policy": "KEEP",
|
||||
"authorityFields": ["message.time_tick"],
|
||||
"projectionFields": ["message.time"],
|
||||
"owner": "game-engine/message"
|
||||
"authorityFields": ["message_action.created_game_tick"],
|
||||
"projectionFields": ["message.time", "message.time_tick"],
|
||||
"rowScope": "messages with a message_action row only",
|
||||
"owner": "game-engine/message-action"
|
||||
},
|
||||
{
|
||||
"key": "message-expiry",
|
||||
"key": "message-action-expiry",
|
||||
"policy": "SHIFT",
|
||||
"authorityFields": ["message.valid_until_tick"],
|
||||
"projectionFields": ["message.valid_until"],
|
||||
"owner": "game-engine/message"
|
||||
"authorityFields": ["message_action.expires_game_tick"],
|
||||
"projectionFields": ["message.valid_until", "message.valid_until_tick"],
|
||||
"rowScope": "messages with a message_action row only",
|
||||
"owner": "game-engine/message-action"
|
||||
},
|
||||
{
|
||||
"key": "message-action-clock-coordinate",
|
||||
"policy": "REBUILD",
|
||||
"authorityFields": ["message_action.clock_revision", "message_action.deadline_generation"],
|
||||
"projectionFields": [],
|
||||
"owner": "game-engine/message-action"
|
||||
},
|
||||
{
|
||||
"key": "inheritance-effect-coordinate",
|
||||
"policy": "KEEP",
|
||||
"authorityFields": [
|
||||
"inheritance_ledger.applied_clock_revision",
|
||||
"inheritance_ledger.applied_deadline_generation"
|
||||
],
|
||||
"projectionFields": [],
|
||||
"owner": "game-engine/inheritance"
|
||||
},
|
||||
{
|
||||
"key": "vote-start-occurrence",
|
||||
@@ -127,7 +207,7 @@
|
||||
"owner": "game-engine/npc-selection"
|
||||
},
|
||||
{
|
||||
"key": "accepted-command-coordinate",
|
||||
"key": "daemon-command-coordinate",
|
||||
"policy": "KEEP",
|
||||
"authorityFields": [
|
||||
"input_event.accepted_game_tick",
|
||||
@@ -139,7 +219,7 @@
|
||||
"input_event.processing_clock_revision",
|
||||
"input_event.processing_deadline_generation"
|
||||
],
|
||||
"owner": "game-api/input-event"
|
||||
"owner": "game-engine/input-event-claim"
|
||||
},
|
||||
{
|
||||
"key": "tournament-deadlines",
|
||||
@@ -220,6 +300,17 @@
|
||||
"clock_projection_outbox.available_at",
|
||||
"clock_projection_outbox.locked_at",
|
||||
"clock_projection_outbox.applied_at",
|
||||
"message.created_at_wall",
|
||||
"message.delete_until_wall",
|
||||
"message.tombstoned_at_wall",
|
||||
"message_action.created_at_wall",
|
||||
"message_action.updated_at_wall",
|
||||
"auction.created_at",
|
||||
"auction.updated_at",
|
||||
"auction_bid.requested_at_wall",
|
||||
"auction_bid.created_at",
|
||||
"inheritance_ledger.requested_at_wall",
|
||||
"inheritance_ledger.consumed_at_wall",
|
||||
"*.created_at",
|
||||
"*.updated_at"
|
||||
]
|
||||
|
||||
@@ -3,11 +3,13 @@
|
||||
## Product contract
|
||||
|
||||
Gameplay time is an integer `GameTick`; one turn is permanently `36,000,000`
|
||||
ticks. Wall time is an observation and operational-control input, never the
|
||||
authority for gameplay ordering. A long suspension advances the observed game
|
||||
coordinate to the resume wall instant without replaying skipped turns, monthly
|
||||
events, RNG, auctions, or tournaments. Every movable future schedule is shifted
|
||||
by the same exact tick delta, including the sub-turn remainder.
|
||||
ticks. Wall time is separately authoritative for account, community, audit,
|
||||
lease, retry, notification, and operational rules. It is never projected into a
|
||||
game deadline. A long suspension advances the observed game coordinate to the
|
||||
resume wall instant without replaying skipped turns, monthly events, RNG,
|
||||
auctions, or tournaments. Every movable future GAME schedule is shifted by the
|
||||
same exact tick delta, including the sub-turn remainder. WALL occurrences and
|
||||
deadlines are outside that operation.
|
||||
|
||||
The clock state is stored in `world_state`:
|
||||
|
||||
@@ -50,6 +52,16 @@ The authoritative registry is
|
||||
architecture gate rejects a new tick/revision field that is absent from that
|
||||
inventory.
|
||||
|
||||
The participant set contains only GAME authority or its projections: world and
|
||||
turn cursors, general turns/recent-war occurrences/reselection deadlines,
|
||||
auction occurrences/deadlines, actionable-message occurrences/deadlines,
|
||||
vote deadlines, selection/NPC windows, input-event game coordinates,
|
||||
tournament Redis deadlines, and clock-operation metadata. A normal message's
|
||||
`created_at_wall`/`delete_until_wall`, inheritance receipts, notification and
|
||||
outbox retry timestamps, leases, and audit columns are explicitly excluded.
|
||||
The former broad `message-expiry` meaning is split into
|
||||
`message-action-expiry`; an envelope has no GAME lifetime.
|
||||
|
||||
## Unification wait
|
||||
|
||||
A unification month with an invader choice changes `RUNNING -> SUSPENDED` and
|
||||
@@ -109,9 +121,11 @@ turn-daemon fencing row
|
||||
-> Redis outbox projection
|
||||
```
|
||||
|
||||
The ordinary turn flush already validates phase, revision, and deadline
|
||||
generation after taking this lock prefix. Clock operation participants will be
|
||||
added without changing that prefix.
|
||||
The ordinary turn flush and daemon command claim validate phase, revision, and
|
||||
deadline generation after taking this lock prefix. WALL-only message/account
|
||||
operations do not take this lock and remain available while suspended. Hybrid
|
||||
operations commit their GAME effect only behind this fence; inheritance debit,
|
||||
receipt, effect, and command success are one transaction.
|
||||
|
||||
## Opening invariant
|
||||
|
||||
@@ -137,8 +151,17 @@ all present. Before that boundary, the loader and ordinary turn-flush fence both
|
||||
treat the row as legacy `MANUAL`; the first fenced flush installs the complete
|
||||
snapshot atomically instead of trusting the new column's `RUNNING` database
|
||||
default. Input-event acceptance does not use that compatibility fallback: an
|
||||
API or worker may enqueue gameplay only after the authoritative clock is fully
|
||||
initialized.
|
||||
API or worker records only a DB-wall receipt, then the daemon establishes the
|
||||
GAME coordinate while claiming under the authoritative fence. Rolling-upgrade
|
||||
payload coordinates may be parsed and ignored, but never become rule authority.
|
||||
|
||||
Migration `20260903140000_split_message_wall_and_game_time` separates message
|
||||
envelopes from actions and adds explicit auction-bid occurrence/request facts,
|
||||
inheritance receipts, and selection cooldown tick authority. Legacy projection
|
||||
columns remain temporarily for old readers. A missing GAME tick fails closed;
|
||||
it never changes the rule to WALL_TIME. A WALL rule likewise never derives an
|
||||
authority tick. See [`time-domains.md`](./time-domains.md) for the complete
|
||||
inventory and migration policy.
|
||||
|
||||
No active participant remains `FORBID`. Tournament writes carry
|
||||
tick/revision/generation coordinates and are revision-fenced in Redis.
|
||||
|
||||
@@ -1,17 +1,20 @@
|
||||
# 게임 시계
|
||||
|
||||
게임 진행 시각은 `world_state.clock_tick`이 기준입니다. 벽시계는 daemon lease,
|
||||
요청 timeout, 처리 budget과 같은 운영 제어에만 사용합니다. 장수 턴, 메시지
|
||||
유효기간, 투표, 경매와 대회 마감은 game tick 또는 그 tick에서 투영한 시각을
|
||||
사용합니다.
|
||||
시간 규칙은 `GAME_TIME`, `WALL_TIME`, `MONOTONIC_ELAPSED_TIME`로
|
||||
나뉩니다. 게임 진행의 권위는 `world_state.clock_tick`, 영속 wall
|
||||
판정의 권위는 PostgreSQL UTC 시계, 프로세스 내부 경과시간의 권위는
|
||||
monotonic clock입니다. 장수 턴·외교 효력·게임 경매·투표·대회는 GAME,
|
||||
일반 메시지·계정·감사·lease·retry는 WALL입니다. 전체 필드별 계약은
|
||||
[`time-domains.md`](./time-domains.md)를 따릅니다.
|
||||
|
||||
한 턴은 항상 `36,000,000` tick입니다. `tick_seconds`가 바뀌면 현재 표시
|
||||
한 턴은 항상 `36,000,000` tick입니다. `tick_seconds`가 바뀌면 현재 GAME 표시
|
||||
시각이 유지되도록 `clock_base_time`을 다시 계산하므로, 기존 장수 턴 순서와
|
||||
남은 턴 수가 보존됩니다. DateTime 필드는 이전 데이터와 화면을 위한 투영값이며
|
||||
tick 필드가 존재하면 tick이 우선합니다.
|
||||
남은 턴 수가 보존됩니다. GAME 규칙의 DateTime은 화면/레거시 투영일 뿐이며
|
||||
tick이 반드시 authority입니다. WALL 규칙은 tick이 없어도 정상이며
|
||||
DateTime을 tick으로 변환해 판정하지 않습니다.
|
||||
|
||||
운영 중 턴 간격 변경은 Gateway의 내구성 런타임 작업으로만 수행합니다. 같은
|
||||
transaction에서 `world_state`, 장수·경매·메시지·설문 투영값과 checkpoint를
|
||||
transaction에서 `world_state`, 장수·경매·actionable message·설문 투영값과 checkpoint를
|
||||
갱신하며 기존 역사/행동 로그의 `created_at`은 다시 쓰지 않습니다. 토너먼트의
|
||||
Redis 투영은 DB commit 뒤 action ID로 멱등 적용됩니다.
|
||||
|
||||
@@ -29,6 +32,18 @@ Redis 투영은 DB commit 뒤 action ID로 멱등 적용됩니다.
|
||||
프로필 설치 시 선택한 모드는 DB에 저장됩니다. daemon의 환경변수는 로드한
|
||||
모드를 명시적으로 덮어쓸 때만 사용해 주세요.
|
||||
|
||||
`SUSPENDED`와 `RECONCILING`에서는 `GameClock.nowTick()`이 wall anchor 이후의
|
||||
현실 경과시간을 더하지 않고 저장된 `clock_tick`을 그대로 반환합니다. 따라서
|
||||
24시간 동안 정지해도 actionable message, 토너먼트와 국가 베팅의 GAME deadline은
|
||||
줄지 않습니다. 일반 메시지 envelope와 5분 삭제 기한은 별도의 DB WALL_TIME이라
|
||||
같은 기간 계속 흐릅니다. 정지 전에 도착한 등용장도 envelope로 계속 수신·열람할
|
||||
수 있지만, 등용 수락 효과는 daemon GAME fence가 다시 열릴 때까지 적용되지 않습니다.
|
||||
|
||||
이미 열린 토너먼트·국가 베팅에는 `SUSPENDED` 중에도 새 베팅을 제출할 수 있습니다.
|
||||
이때 베팅 가능 여부는 frozen GAME coordinate로 판정하고, 재화 mutation은 현재
|
||||
phase/revision/generation을 다시 잠가 검증합니다. 단계 전환·마감·정산은 실행하지
|
||||
않으며, 원자적 reconciliation이 진행되는 `RECONCILING`에서는 새 베팅도 받지 않습니다.
|
||||
|
||||
## 중단 후 재개
|
||||
|
||||
realtime daemon은 재개할 때 Ref `checkDelay()`와 같은 한도를 적용합니다.
|
||||
@@ -45,21 +60,24 @@ realtime daemon은 재개할 때 Ref `checkDelay()`와 같은 한도를 적용
|
||||
장수 턴 tick은 바꾸지 않습니다. 동시에 `clock_wall_anchor`를 작업 실행
|
||||
시각으로 다시 고정합니다.
|
||||
|
||||
DB migration은 기존 DateTime 값에서 tick을 채웁니다. 새 설치와 migration
|
||||
DB migration은 GAME 규칙의 기존 DateTime 투영에서 tick을 채웁니다. 새 설치와 migration
|
||||
재실행은 `prisma:migrate:deploy:game`으로 수행합니다. 메시지의 연도 9999 같은
|
||||
무기한 호환값은 안전한 정수 범위를 넘을 수 있으므로 tick을 `NULL`로 두고
|
||||
DateTime fallback을 사용합니다.
|
||||
무기한 호환값은 일반 메시지의 투영일 뿐입니다. actionable deadline은
|
||||
`expires_game_tick`, 일반 삭제 deadline은 `delete_until_wall`만이 authority이며
|
||||
NULL에 따라 다른 시계로 fallback하지 않습니다.
|
||||
|
||||
## 비동기 작업의 시계 경계
|
||||
|
||||
게임 규칙의 수락·입찰·예약 시각은 logical game time을 사용하지만 daemon
|
||||
queue의 `InputEvent.createdAt`, worker history retention과 timeout은 운영
|
||||
벽시계를 사용합니다. NPC 빙의 enqueue는 현재 logical game time을 event
|
||||
payload의 `acceptedGameAt`에 고정합니다. queue에 들어갈 때 유효했던 token은
|
||||
처리 전 game tick이 진행해도 이 저장된 논리 수락 시각으로 다시 검증합니다.
|
||||
외부 요청은 `InputEvent.createdAt` DB WALL_TIME으로 접수합니다. API payload가
|
||||
game tick을 미리 고정하지 않으며, daemon이 clock lock/fence 아래서 claim할 때
|
||||
`accepted_game_tick`/세대와 `processing_game_tick`/세대를 확정합니다. NPC,
|
||||
선택, 투표, 경매, 유산 효과는 처리 tick으로 검증하며 stale revision은
|
||||
적용하지 않습니다. worker history retention·lease·retry는 DB WALL_TIME,
|
||||
프로세스 대기 budget은 monotonic time입니다.
|
||||
|
||||
경매 입찰은 같은 logical tick에서 여러 번 일어날 수 있습니다. bid 표시
|
||||
시각은 같은 game time을 보존하고, optimistic 경합 판정은 임의 UUID의
|
||||
경매 입찰은 `requested_at_wall`로 현실 요청을, `occurred_game_tick`으로 GAME
|
||||
사건을 별도 기록합니다. bid 표시 투영은 같은 game time을 보존하고,
|
||||
optimistic 경합 판정은 임의 UUID의
|
||||
사전순이 아니라 읽은 `latest_event_id`를 버전 토큰으로 사용합니다. worker
|
||||
재시작 시 `OPEN`은 `close_tick` deadline에, 이미 마감 판정이 끝난
|
||||
`FINALIZING`은 현재 tick에 seed하여 durable finalization event 복구를 즉시
|
||||
|
||||
@@ -0,0 +1,166 @@
|
||||
# Time-domain inventory
|
||||
|
||||
This document is the authoritative classification of persistent timestamps,
|
||||
deadlines, cooldowns, and process-local elapsed-time rules. Classification is
|
||||
per rule, not per table. A feature may record both a wall occurrence and a game
|
||||
effect; those are two facts, never one fallback clock.
|
||||
|
||||
## Domain contract
|
||||
|
||||
| Domain | Authority | Advances while suspended/reconciling | Reconciliation |
|
||||
| ------------------------ | -------------------------------------------------------------- | ------------------------------------ | ----------------------------- |
|
||||
| `GAME_TIME` | `world_state.clock_tick` under phase/revision/generation fence | no | `SHIFT`, `KEEP`, or `REBUILD` |
|
||||
| `WALL_TIME` | PostgreSQL UTC `CURRENT_TIMESTAMP` for persistent decisions | yes | never |
|
||||
| `MONOTONIC_ELAPSED_TIME` | `performance.now()` / monotonic process clock | process-local only | never persisted |
|
||||
|
||||
`GameTick`, `ClockRevision`, `DeadlineGeneration`, `WallInstant`, and
|
||||
`MonotonicDuration` name these meanings in new/refactored APIs. Existing
|
||||
`createdAt`/`updatedAt` fields remain wall audit timestamps unless this inventory
|
||||
explicitly calls them game projections.
|
||||
|
||||
## Game database inventory
|
||||
|
||||
`Pause` means whether the rule continues to age during `SUSPENDED` or
|
||||
`RECONCILING`. `Projection` means a non-authoritative compatibility/display
|
||||
representation.
|
||||
|
||||
| Table / rule / field(s) | Current meaning | Domain and authority | Pause | Reconcile / projection | Decision and reason |
|
||||
| -------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | ---------------------------------------------------------------- | ------------------------------------ | -------------------------------------------------------------------- | ----------------------------------------------------------------------------------- | -------------------------------------------------------------------------------------------------------------------- |
|
||||
| `world_state.clock_tick` | observed world coordinate | GAME, self-authoritative | stop | REBUILD | root of all game-time decisions |
|
||||
| `world_state.last_turn_tick` | executed turn cursor | GAME | stop | SHIFT | execution order must preserve skipped-turn policy |
|
||||
| `world_state.clock_revision`, `deadline_generation` | clock/future-deadline generations | GAME metadata | stop | REBUILD | stale commands/workers must fail their fence |
|
||||
| `world_state.clock_base_time`, `clock_wall_anchor` | tick-to-date mapping and wall observation anchor | GAME projection metadata | n/a | REBUILD | not business wall deadlines |
|
||||
| `world_state.updated_at` | row audit | WALL, DB UTC | advance | excluded | operational history is not shifted |
|
||||
| `clock_suspension.source_revision`, `target_revision`, `cut_tick`, `catch_up_ticks`, `gap_ticks`, `shift_ticks`, `aligned_tick` | reconciliation plan/audit | GAME metadata | stop | KEEP | immutable clock operation facts |
|
||||
| `clock_suspension.cut_wall_at`, `resume_wall_at`, `created_at`, `updated_at` | operator/runtime occurrence audit | WALL, DB UTC | advance | excluded | records when the real operation occurred |
|
||||
| `clock_projection_outbox.target_revision` | target GAME generation | GAME metadata | stop | KEEP | projection fence |
|
||||
| `clock_projection_outbox.available_at`, `locked_at`, `applied_at`, `created_at`, `updated_at` | retry/lease/audit | WALL, DB UTC | advance | excluded | worker control cannot pause with game time |
|
||||
| `clock_reconciliation_participant` checksum/count/policy | immutable operation evidence | GAME operation metadata | n/a | KEEP | evidence, not a deadline |
|
||||
| `input_event.accepted_game_tick`, `accepted_clock_revision`, `accepted_deadline_generation` | daemon-claim boundary | GAME metadata | stop | KEEP across matching revision; rebase pending legacy rows only | API does not pre-stamp these; daemon owns acceptance |
|
||||
| `input_event.processing_game_tick`, `processing_clock_revision`, `processing_deadline_generation` | actual mutation boundary | GAME metadata | stop | KEEP | effect validation/RNG uses this coordinate |
|
||||
| `input_event.created_at`, `processing_at`, `completed_at`, `lease_until` | request receipt, processing audit, lease | WALL, DB UTC | advance | excluded | external occurrence and worker lease |
|
||||
| `read_model_outbox.*_at`, `web_push_outbox.*_at` | availability, claim, delivery, audit | WALL, DB UTC | advance | excluded | retry and notification delivery are operational |
|
||||
| `turn_daemon_lease.lease_until`, `heartbeat_at` | daemon liveness | WALL, DB UTC | advance | excluded | a paused game must still lose a dead daemon lease |
|
||||
| `general.turn_tick` | next general turn | GAME | stop | SHIFT; `turn_time` projection | determines engine order |
|
||||
| `general.recent_war_tick` | past battle occurrence | GAME | stop | KEEP; `recent_war_time` projection | historical event does not move |
|
||||
| `general.meta.next_change_tick` | N-turn reselection cooldown | GAME | stop | SHIFT; `next_change`/`nextChangeAt` projections | expressed in turns; missing tick fails closed |
|
||||
| `general.created_at`, `updated_at` | entity audit | WALL, DB UTC | advance | excluded | no gameplay deadline meaning |
|
||||
| `select_pool.reserved_until_tick` | selection reservation deadline | GAME | stop | SHIFT; `reserved_until` projection | reservation is measured in game turns |
|
||||
| `select_npc_token.valid_until_tick`, `pick_more_from_tick` | NPC selection windows | GAME | stop | SHIFT; DateTime projections | token is a game selection schedule; missing ticks fail closed |
|
||||
| `general_access_log.last_refresh`, `last_action_at`; `general_access_batch.created_at`; `traffic_period.started_at`, `last_refresh`; `traffic_period_general.last_refresh` | traffic/access accounting | WALL, DB UTC | advance | excluded | community/operations usage, not world progression |
|
||||
| `message.created_at_wall`, `delete_until_wall`, `tombstoned_at_wall` | envelope send/delete lifecycle | WALL, DB UTC | advance | excluded | normal messages work while the game is paused; deletion is real five minutes |
|
||||
| `message.occurred_game_tick` | optional game context | GAME occurrence | stop | KEEP | context only, never deletion authority |
|
||||
| `message.time`, `time_tick`, `valid_until`, `valid_until_tick` | rolling compatibility projections | projection only | n/a | recompute only for `message_action`; general-envelope values never decide lifecycle | old columns are not fallback authority |
|
||||
| `message_action.created_game_tick`, `resolved_game_tick` | action occurrence/resolution | GAME occurrence | stop | KEEP | actionable message lifecycle is separate from envelope |
|
||||
| `message_action.expires_game_tick` | proposal response deadline | GAME | stop | SHIFT | remaining game duration survives pause |
|
||||
| `message_action.clock_revision`, `deadline_generation` | response fence | GAME metadata | stop | REBUILD | stale responses are rejected |
|
||||
| `message_action.created_at_wall`, `updated_at_wall`; `message_read_state.updated_at` | audit/read occurrence | WALL, DB UTC | advance | excluded | community UX state |
|
||||
| `diplomacy_letter.date` | document authored/sent time | WALL, DB UTC | advance | excluded | game effect dates live in diplomacy/action state, not the document timestamp |
|
||||
| diplomacy war/nonaggression start/end month data | diplomatic effect schedule | GAME calendar | stop | handled by engine schedule | affects world turns and war validity |
|
||||
| `inheritance_point.updated_at`, `inheritance_log.created_at`, `inheritance_result.created_at`, baseline/user-state audit fields | account ledger/result audit | WALL, DB UTC | advance | excluded | account/external-currency history |
|
||||
| `inheritance_ledger.requested_at_wall`, `consumed_at_wall`, `created_at_wall` | direct purchase receipt | WALL, DB UTC | advance | excluded | real request/debit receipt |
|
||||
| `inheritance_ledger.applied_clock_revision`, `applied_deadline_generation` | game-effect fence metadata | GAME metadata | stop | KEEP | no `applied_game_tick`: current direct effects are timeless immediate state changes |
|
||||
| inheritance command `input_event` | durable effect state/idempotency | WALL receipt + GAME processing fence | mixed, separated | only GAME coordinate participates | one transaction commits debit, receipt, effect, and command success; failure leaves durable input event and no debit |
|
||||
| `auction.open_tick`, `auction_bid.occurred_game_tick` | open/bid game occurrence | GAME | stop | KEEP; bid `event_at` is projection | event order/RNG/replay context |
|
||||
| `auction.close_tick` | in-world close deadline | GAME | stop | SHIFT; `close_at` projection | authoritative worker/finalizer deadline; missing tick fails closed |
|
||||
| `auction_bid.requested_at_wall`, `created_at`; `auction.finalizing_at`, `finished_at`, `created_at`, `updated_at` | request/processing/audit | WALL, DB UTC | advance | excluded | real action and recovery history |
|
||||
| `auction.latest_event_at` | optimistic compatibility projection of latest game event | GAME projection | stop | follows authoritative event tick | never used as wall deadline |
|
||||
| `vote_poll.start_tick`, `end_tick` | poll occurrence/deadline | GAME | stop | KEEP/SHIFT; `start_at`/`end_at` projections | poll is an in-world survey; missing deadline tick fails closed |
|
||||
| `vote_poll.closed_at`, `created_at`, `updated_at`; `vote.created_at`; `vote_comment.created_at` | closure/user/audit occurrence | WALL, DB UTC | advance | excluded | closure receipt and community content history |
|
||||
| tournament `nextTick`, `bettingCloseTick` in Redis | stage/betting deadlines | GAME | stop | REBUILD; `nextAt`/`bettingCloseAt` projections | stages advance with the world; legacy date-only state fails closed |
|
||||
| nation betting open/close year-month and tournament phase | in-world availability | GAME calendar/tick | stop | engine/Redis participant | tied to tournament turns |
|
||||
| tournament/nation bet submission | user WALL request + effect at the current frozen GAME coordinate | WALL + GAME, separated | submission is allowed during `SUSPENDED`; GAME deadline does not age | receipt excluded; GAME availability/fence retained | pausing stage progress must not close an already-open betting window |
|
||||
| `nation_betting.*_at`, `nation_bet.*_at` | user/audit occurrence | WALL, DB UTC | advance | excluded | receipts, not close authority |
|
||||
| `game_history.date`, old-general `turntime`, archived projected dates | archived game-calendar projection | GAME historical display | stop | KEEP, never shifted | immutable archive/replay record |
|
||||
| archive/entity `created_at`, cancellation `opened_at`/`cancelled_at`, unification `completed_at` | operation/archive audit | WALL, DB UTC | advance | excluded | real creation/completion facts |
|
||||
| `general_turn_revision.lease_expires_at`, `nation_turn_revision.lease_expires_at` and audit timestamps | edit lease/revision audit | WALL, DB UTC | advance | excluded | editor concurrency timeout |
|
||||
| board post/comment, log/error/event, legacy migration timestamps | content/audit/migration history | WALL, DB UTC | advance | excluded | community and operational evidence |
|
||||
|
||||
The year-9999 message sentinel remains a legacy projection only.
|
||||
`MAX_SAFE_GAME_TICK` is the separate GAME-domain infinite sentinel. Neither is
|
||||
converted into or used as the other domain's ordinary deadline.
|
||||
|
||||
## Gateway database inventory
|
||||
|
||||
Gateway has no gameplay clock authority. Every Gateway `DateTime` is WALL_TIME:
|
||||
|
||||
- `app_user`: identity/session/icon/terms/privacy/Kakao/grace/deletion/login and
|
||||
`created_at`/`updated_at` fields.
|
||||
- access grants, retired identities, admin audits, user icons, legacy member
|
||||
logs, and migration timestamps.
|
||||
- profile lifecycle `preopen_at`, `open_at`, `scheduled_start_at`, build request,
|
||||
start/completion/last-used, and row audit timestamps. These are real control
|
||||
plane schedules; they do not replace a profile's `world_state.clock_tick`.
|
||||
- subscriptions/preferences/receipts/notifications and web-push delivery
|
||||
`available_at`, `locked_at`, `delivered_at`, expiration and audit fields.
|
||||
- runtime actions, operations, releases, and bulk releases: schedule, start,
|
||||
completion, retry, lease, heartbeat, successful and audit timestamps.
|
||||
|
||||
Competitive Gateway operation/release claim and lease renewal decisions read
|
||||
PostgreSQL `CURRENT_TIMESTAMP` inside the persistence transaction; the caller's
|
||||
poll timestamp is not authoritative. A Gateway PREOPEN wall schedule is an operational request; once
|
||||
the game exists, gameplay schedules use the game database tick.
|
||||
|
||||
## Process-local monotonic inventory
|
||||
|
||||
The following are `MONOTONIC_ELAPSED_TIME` and are never persisted: daemon/RPC
|
||||
wait budgets, turn processing budgets, worker poll/resync intervals, lock
|
||||
acquisition waits, readiness loops, short-lived cache TTLs, latency metrics, and
|
||||
test wait loops. Production implementations use `performance.now()` where an
|
||||
elapsed duration is measured. `Date.now()`/`new Date()` remains valid only when
|
||||
creating or formatting a WALL occurrence, calculating a non-competitive auth
|
||||
TTL for an external protocol, or providing an explicit test clock.
|
||||
|
||||
## API phase policy
|
||||
|
||||
| Operation | During `SUSPENDED` / `RECONCILING` | Fence |
|
||||
| -------------------------------------------------------------------------------------------------------------------------- | --------------------------------------------------------------------- | ----------------------------------------------------------------------- |
|
||||
| normal public/private/nation message send/read/delete, including receiving/reading an existing recruitment letter envelope | allowed | WALL DB transaction only; actionable deadline remains frozen GAME state |
|
||||
| notification/account/inheritance history/audit reads | allowed | WALL |
|
||||
| actionable message response | rejected/queued except the explicitly authorized unification response | daemon clock phase/revision/generation |
|
||||
| tournament/nation bet submission while its GAME window is open | allowed in `SUSPENDED`; rejected in `RECONCILING` | frozen GAME deadline + phase/revision/generation fence |
|
||||
| tournament stage transition/close/settlement and nation-bet close/settlement | not applied | daemon GAME fence |
|
||||
| turn, reservation, war/diplomacy effect, auction, vote, other tournament mutation | not applied | daemon GAME fence |
|
||||
| direct inheritance state mutation | rejected if the daemon GAME fence cannot commit | atomic input-event + revision/generation fence |
|
||||
|
||||
There is currently no account-only inheritance purchase endpoint. Direct
|
||||
inheritance commands use policy 1 (no debit if game mutation cannot commit), not
|
||||
an ambiguous partially-applied state. Their `InputEvent.requestId` is the durable
|
||||
idempotency/effect state; `InheritanceLedger.requestId` proves the one successful
|
||||
receipt.
|
||||
|
||||
## `loadCurrentGameTime` call-site audit
|
||||
|
||||
All production call sites were reviewed. The remaining uses are GAME-only:
|
||||
|
||||
- `messages/store`: create/read/invalidate the separate `message_action`; normal
|
||||
envelope creation, display, read state, and deletion do not load game time.
|
||||
- `messages/diplomaticResponse`: game-effect/log calendar context after an
|
||||
actionable response fence; the letter's authored date is DB WALL_TIME.
|
||||
- auction `open`, `scheduler`, `worker`, and router: open/close tick projection,
|
||||
due evaluation, and fence context; bid receipt time is separate WALL_TIME.
|
||||
- vote router, tournament router/worker: GAME poll/stage deadlines and Redis
|
||||
projection fences.
|
||||
- troop/general/join selection routers: current world turn/schedule context;
|
||||
the NPC reservation mutation holds the clock advisory lock and `world_state`
|
||||
row fence before reading it.
|
||||
- lobby: display-only projected server game time and phase.
|
||||
|
||||
No inheritance receipt, ordinary message timestamp/delete rule, audit log,
|
||||
lease, outbox retry, API timeout, or worker budget calls this helper.
|
||||
|
||||
Ordinary message send/read-state/delete mutations retain their durable API
|
||||
`InputEvent` transaction and read-model journal, but use the WALL-only input
|
||||
boundary. That boundary deliberately does not acquire the game clock advisory
|
||||
lock, so a reconciliation transaction cannot unnecessarily serialize community
|
||||
messaging. Actionable responses continue to use the GAME-fenced boundary.
|
||||
|
||||
## Migration boundary
|
||||
|
||||
Migration `20260903140000_split_message_wall_and_game_time` adds and backfills
|
||||
the explicit message, action, auction-bid, inheritance receipt, and selection
|
||||
cooldown authorities. It never extends an already-expired message delete window.
|
||||
Old projection columns remain during rolling deployment, but new code never
|
||||
chooses a clock by NULL fallback: GAME rules require their tick; WALL rules use
|
||||
their wall column. The disposable migration verifier covers populated upgrade,
|
||||
indexes/constraints, replay safety, and a second no-op deploy.
|
||||
Reference in New Issue
Block a user