feat: 직접 writer를 내구성 변화 저널에 연결

86개 API mutation을 분류하고 메시지 mailbox, 베팅, 국가 설정, 예약 명령의 revision/outbox 표식을 소유 transaction에 연결한다. 공개 SSE는 식별자 없는 invalidation만 노출한다.
This commit is contained in:
2026-08-16 18:38:08 +00:00
parent e4ac2a60b1
commit dd4645e4d0
22 changed files with 485 additions and 55 deletions
@@ -2,6 +2,7 @@ export const READ_MODEL_DOMAINS = [
'general.content',
'city.content',
'nation.content',
'dashboard.global',
'world.content',
'map.world',
'map.general',
@@ -16,6 +17,7 @@ export const READ_MODEL_DOMAINS = [
'lobby.general',
'contacts.world',
'reserved.general',
'messages.mailbox',
'tournament',
'betting',
] as const;
@@ -8,6 +8,13 @@ import { createEmptyRealtimeReadModelChanges, type RealtimeReadModelChanges } fr
const uniqueSortedIds = (values: Iterable<number>): number[] =>
[...new Set(values)].sort((left, right) => left - right);
export const readModelOutboxPayloadToMessageMailboxes = (
payload: ReadModelOutboxPayloadV1
): readonly number[] =>
uniqueSortedIds(
payload.changes.flatMap(([domain, entityId]) => (domain === 'messages.mailbox' ? [entityId] : []))
);
export const parseReadModelOutboxPayload = (value: unknown): ReadModelOutboxPayloadV1 | null => {
if (!value || typeof value !== 'object' || Array.isArray(value)) {
return null;
@@ -73,6 +80,8 @@ export const readModelOutboxPayloadToChanges = (
case 'nation.content':
nationIds.push(entityId);
break;
case 'dashboard.global':
break;
case 'world.content':
changes.worldChanged = true;
break;
@@ -113,6 +122,7 @@ export const readModelOutboxPayloadToChanges = (
reservedGeneralIds.push(entityId);
break;
case 'access.general':
case 'messages.mailbox':
case 'tournament':
case 'betting':
// These domains have no browser-wide dashboard invalidation.
+7 -1
View File
@@ -206,6 +206,12 @@ export interface MessageCreatedEvent {
senderId: number;
}
/** Durable mailbox wake-up derived from committed read-model outbox rows. */
export interface MessagesChangedEvent {
type: 'messagesChanged';
mailboxes: number[];
}
export interface ReadModelInvalidatedEvent {
type: 'readModelInvalidated';
invalidation: RealtimeReadModelInvalidation;
@@ -218,4 +224,4 @@ export interface MessagesInvalidatedEvent {
/** Events safe to expose to an authenticated browser over SSE. */
export type PublicRealtimeEvent = ReadModelInvalidatedEvent | MessagesInvalidatedEvent;
export type RealtimeEvent = TurnCompletedEvent | ReadModelChangedEvent | MessageCreatedEvent;
export type RealtimeEvent = TurnCompletedEvent | ReadModelChangedEvent | MessageCreatedEvent | MessagesChangedEvent;