fix(game-api): 장수 소유자 확인 메시지를 복구

Ref와 같이 확인자와 확인 대상에게 시스템 개인 메시지를 저장하고, 두 mailbox 변경을 durable journal에 기록한다. 실패 경계와 실제 PostgreSQL 저장 회귀 테스트를 포함한다.
This commit is contained in:
2026-08-21 05:45:51 +00:00
parent fb65400906
commit e241af0ba3
5 changed files with 393 additions and 14 deletions
@@ -33,9 +33,9 @@ writer reconciliation을 포함한다. rolling deployment가 끝난 뒤에만
| 분류 | 수 | route |
| --- | ---: | --- |
| durable journal | 22 | `betting.bet`; `messages.delete`, `messages.respond`, `messages.send`; `nation.setBill`, `nation.setBlockScout`, `nation.setBlockWar`, `nation.setNotice`, `nation.setRate`, `nation.setScoutMsg`, `nation.setSecretLimit`; `npc.setGeneralPriority`, `npc.setNationPolicy`, `npc.setNationPriority`; `turns.repeatGeneral`, `turns.setGeneral`, `turns.setGeneralBulk`, `turns.shiftGeneral`; `vote.closePoll`, `vote.createPoll`, `vote.submitVote`, `vote.updatePoll` |
| durable journal | 23 | `betting.bet`; `inherit.checkOwner`; `messages.delete`, `messages.respond`, `messages.send`; `nation.setBill`, `nation.setBlockScout`, `nation.setBlockWar`, `nation.setNotice`, `nation.setRate`, `nation.setScoutMsg`, `nation.setSecretLimit`; `npc.setGeneralPriority`, `npc.setNationPolicy`, `npc.setNationPriority`; `turns.repeatGeneral`, `turns.setGeneral`, `turns.setGeneralBulk`, `turns.shiftGeneral`; `vote.closePoll`, `vote.createPoll`, `vote.submitVote`, `vote.updatePoll` |
| separate access journal | 1 | `public.recordAccess` |
| explicit no realtime consumer | 15 | `board.writeArticle`, `board.writeComment`; `diplomacy.destroyLetter`, `diplomacy.respondLetter`, `diplomacy.rollbackLetter`, `diplomacy.sendLetter`; `inherit.checkOwner`; `join.getSelectionPool`, `join.listPossessCandidates`; `messages.readLatest`; `turns.repeatNation`, `turns.setNation`, `turns.setNationBulk`, `turns.shiftNation`; `vote.addComment` |
| explicit no realtime consumer | 14 | `board.writeArticle`, `board.writeComment`; `diplomacy.destroyLetter`, `diplomacy.respondLetter`, `diplomacy.rollbackLetter`, `diplomacy.sendLetter`; `join.getSelectionPool`, `join.listPossessCandidates`; `messages.readLatest`; `turns.repeatNation`, `turns.setNation`, `turns.setNationBulk`, `turns.shiftNation`; `vote.addComment` |
| engine owned | 27 | `auction.bidBuyRice`, `auction.bidSellRice`, `auction.bidUnique`, `auction.openBuyRice`, `auction.openSellRice`, `auction.openUnique`; `general.adjustIcon`, `general.buildNationCandidate`, `general.dieOnPrestart`, `general.dropItem`, `general.ensureDieOnPrestartStatus`, `general.instantRetreat`, `general.setMySetting`, `general.vacation`; `inherit.openUniqueAuction`; `join.createGeneral`, `join.possessGeneral`, `join.reselectPoolGeneral`, `join.selectPoolGeneral`; `nation.appoint`, `nation.changePermission`, `nation.kick`; `troop.create`, `troop.exit`, `troop.join`, `troop.kick`, `troop.rename` |
| mixed saga | 9 | `inherit.buyHiddenBuff`, `inherit.buyRandomUnique`, `inherit.resetSpecialWar`, `inherit.resetStat`, `inherit.resetTurnTime`, `inherit.setNextSpecialWar`; `tournament.cancel`, `tournament.join`, `tournament.placeBet` |
| Redis projection | 6 | `tournament.patchState`, `tournament.seedParticipants`, `tournament.setBettingEntries`, `tournament.setMatches`, `tournament.setParticipants`, `tournament.setState` |
@@ -51,6 +51,7 @@ writer reconciliation을 포함한다. rolling deployment가 끝난 뒤에만
| writer | durable key | public wake-up | 근거와 경계 |
| --- | --- | --- | --- |
| `betting.bet` | `general.content:<actor>`, `betting:0` | 없음 | 본인 베팅/유산 지출과 베팅 aggregate source가 바뀐다. `betting`은 현재 별도 화면 source이고 main dashboard fan-out을 만들지 않는다. |
| `inherit.checkOwner` | 확인자·확인 대상의 `messages.mailbox:<general>` | 두 장수 mailbox viewer에게 ID 없는 `messagesInvalidated` | Ref처럼 확인 결과와 피확인 알림을 시스템 개인 메시지로 저장하며 포인트 차감·유산 로그·두 메시지·journal을 한 API input-event transaction에서 commit한다. |
| `messages.send` | 생성된 수신/송신 복사본의 `messages.mailbox:<mailbox>` | 해당 mailbox viewer에게 ID 없는 `messagesInvalidated` | 기존 pre-commit Redis `messageCreated`를 제거했다. outbox publish 뒤에도 browser에는 mailbox/message/sender/time/revision이 노출되지 않는다. |
| `messages.delete` | 실제로 만료한 송신/수신 mailbox | 동일 | sender copy만 지우는 수동 외교 메시지는 그 mailbox만 표시한다. |
| `messages.respond` | 영향 mailbox, `records.general`, 실제 외교 변경 국가의 `nation.content`, front-state patch 도시의 `city.content`, 필요 시 `map.world`, transitive aggregate용 `dashboard.global` | mailbox boolean 및 해당 dashboard slice | 실패 로그도 commit되면 actor 개인 기록을 표시한다. 외교 수락이 실제 diplomacy/city/nation dependency를 바꿀 때만 broad source key를 표시한다. |
@@ -72,8 +73,8 @@ public dashboard event로 내보내지 않는다. browser wake-up은 정밀 enti
전쟁/불가침 상태를 실제 변경하는 `messages.respond`와 구분한다.
- `messages.readLatest`는 본인의 읽음 cursor다. 요청한 tab이 이미 최신 cursor를 알고
있으므로 자기 자신에게 다시 wake-up을 보내지 않는다.
- nation reserved turn, selection-pool reservation, possession 후보, inheritance owner
확인은 각각 전용 화면/request response가 최신 상태를 소유한다.
- nation reserved turn, selection-pool reservation possession 후보는 각각 전용
화면/request response가 최신 상태를 소유한다.
- image upload는 외부 content store write이며 game PostgreSQL read model이 아니다.
- battle simulation 준비와 서버 fallback은 호환상 mutation transport를 쓰지만
read-only 계산이며 input event transaction을 열지 않는다.