refactor(game): refresh committed read models selectively

This commit is contained in:
2026-08-09 15:46:01 +00:00
parent 00b9fcdb93
commit a2683df80f
14 changed files with 807 additions and 48 deletions
+10
View File
@@ -7,3 +7,13 @@ export const buildGameEventChannel = (profileName: string): string => {
const normalized = normalizeProfileName(profileName);
return `sammo:${normalized}:realtime:events`;
};
export const buildGameReadModelRevisionKey = (profileName: string): string => {
const normalized = normalizeProfileName(profileName);
return `sammo:${normalized}:read-model:revision`;
};
export const buildGameReadModelDomainRevisionKey = (profileName: string): string => {
const normalized = normalizeProfileName(profileName);
return `sammo:${normalized}:read-model:domain-revisions`;
};
+69
View File
@@ -1,9 +1,77 @@
export type MessageTypeKey = 'public' | 'private' | 'national' | 'diplomacy';
/**
* Durable mutations summarized after the database transaction commits.
* Entity IDs let each authenticated client decide whether its own read model
* is affected without exposing entity payloads over the shared Redis channel.
*/
export interface RealtimeReadModelChanges {
generalIds: number[];
cityIds: number[];
nationIds: number[];
reservedGeneralIds: number[];
recordGeneralIds: number[];
worldChanged: boolean;
globalRecordsChanged: boolean;
worldHistoryChanged: boolean;
contactsChanged: boolean;
}
export const createEmptyRealtimeReadModelChanges = (): RealtimeReadModelChanges => ({
generalIds: [],
cityIds: [],
nationIds: [],
reservedGeneralIds: [],
recordGeneralIds: [],
worldChanged: false,
globalRecordsChanged: false,
worldHistoryChanged: false,
contactsChanged: false,
});
const mergeIds = (left: readonly number[], right: readonly number[]): number[] =>
[...new Set([...left, ...right])].sort((a, b) => a - b);
export const mergeRealtimeReadModelChanges = (
left: RealtimeReadModelChanges,
right: RealtimeReadModelChanges
): RealtimeReadModelChanges => ({
generalIds: mergeIds(left.generalIds, right.generalIds),
cityIds: mergeIds(left.cityIds, right.cityIds),
nationIds: mergeIds(left.nationIds, right.nationIds),
reservedGeneralIds: mergeIds(left.reservedGeneralIds, right.reservedGeneralIds),
recordGeneralIds: mergeIds(left.recordGeneralIds, right.recordGeneralIds),
worldChanged: left.worldChanged || right.worldChanged,
globalRecordsChanged: left.globalRecordsChanged || right.globalRecordsChanged,
worldHistoryChanged: left.worldHistoryChanged || right.worldHistoryChanged,
contactsChanged: left.contactsChanged || right.contactsChanged,
});
export const hasRealtimeReadModelChanges = (changes: RealtimeReadModelChanges): boolean =>
changes.generalIds.length > 0 ||
changes.cityIds.length > 0 ||
changes.nationIds.length > 0 ||
changes.reservedGeneralIds.length > 0 ||
changes.recordGeneralIds.length > 0 ||
changes.worldChanged ||
changes.globalRecordsChanged ||
changes.worldHistoryChanged ||
changes.contactsChanged;
export interface TurnCompletedEvent {
type: 'turnCompleted';
at: string;
lastTurnTime: string;
/** Absent only for a rolling-deploy event produced by an older daemon. */
changes?: RealtimeReadModelChanges;
revision?: number;
}
export interface ReadModelChangedEvent {
type: 'readModelChanged';
at: string;
changes: RealtimeReadModelChanges;
revision: number;
}
export interface MessageCreatedEvent {
@@ -17,4 +85,5 @@ export interface MessageCreatedEvent {
export type RealtimeEvent =
| TurnCompletedEvent
| ReadModelChangedEvent
| MessageCreatedEvent;