feat: 엔진 변화 저널을 상태 커밋과 원자화
최종 read model projection과 저장된 로그를 소유 transaction 안에서 판정하고 revision 및 outbox와 함께 커밋한다. clock 전용 변화와 rollback은 revision을 남기지 않으며 커밋 receipt를 유실 없이 큐잉한다.
This commit is contained in:
@@ -4,7 +4,9 @@ import { LogCategory, LogFormat, LogScope } from '@sammo-ts/logic';
|
||||
|
||||
import {
|
||||
applyRealtimeReadModelBaseline,
|
||||
createReadModelChangeJournal,
|
||||
createRealtimeReadModelBaseline,
|
||||
createWorldReadModelSignature,
|
||||
mergePersistedVisibleLogChanges,
|
||||
summarizeRealtimeReadModelChanges,
|
||||
} from '../src/turn/databaseHooks.js';
|
||||
@@ -12,6 +14,98 @@ import type { InMemoryTurnWorld } from '../src/turn/inMemoryWorld.js';
|
||||
import type { TurnWorldChanges } from '../src/turn/inMemoryWorld.js';
|
||||
import type { ReservedTurnChanges } from '../src/turn/reservedTurnStore.js';
|
||||
|
||||
describe('durable read-model change journal mapping', () => {
|
||||
it('maps every final engine invalidation to its precise durable domain', () => {
|
||||
const changes = {
|
||||
...createEmptyRealtimeReadModelChanges(),
|
||||
generalIds: [7],
|
||||
cityIds: [3],
|
||||
nationIds: [2],
|
||||
mapGeneralIds: [8],
|
||||
mapCityIds: [4],
|
||||
mapNationIds: [5],
|
||||
frontStatusGeneralIds: [9],
|
||||
frontStatusNationIds: [2],
|
||||
frontStatusActorIds: [7],
|
||||
lobbyGeneralIds: [7],
|
||||
reservedGeneralIds: [7],
|
||||
recordGeneralIds: [7],
|
||||
worldChanged: true,
|
||||
globalRecordsChanged: true,
|
||||
worldHistoryChanged: true,
|
||||
contactsChanged: true,
|
||||
frontStatusChanged: true,
|
||||
mapChanged: true,
|
||||
lobbyChanged: true,
|
||||
};
|
||||
|
||||
expect(createReadModelChangeJournal(changes).snapshot()).toEqual([
|
||||
{ domain: 'city.content', entityId: 3 },
|
||||
{ domain: 'contacts.world', entityId: 0 },
|
||||
{ domain: 'front.general', entityId: 7 },
|
||||
{ domain: 'front.global', entityId: 0 },
|
||||
{ domain: 'front.nation', entityId: 2 },
|
||||
{ domain: 'general.content', entityId: 7 },
|
||||
{ domain: 'lobby.general', entityId: 7 },
|
||||
{ domain: 'lobby.world', entityId: 0 },
|
||||
{ domain: 'map.general', entityId: 8 },
|
||||
{ domain: 'map.world', entityId: 0 },
|
||||
{ domain: 'nation.content', entityId: 2 },
|
||||
{ domain: 'records.general', entityId: 7 },
|
||||
{ domain: 'records.global', entityId: 0 },
|
||||
{ domain: 'records.history', entityId: 0 },
|
||||
{ domain: 'reserved.general', entityId: 7 },
|
||||
{ domain: 'world.content', entityId: 0 },
|
||||
]);
|
||||
});
|
||||
|
||||
it('ignores moving clock and lease metadata but detects month, turn-term, config, and gameplay meta', () => {
|
||||
const state = {
|
||||
currentYear: 190,
|
||||
currentMonth: 1,
|
||||
tickSeconds: 600,
|
||||
lastTurnTime: new Date('2026-08-16T00:00:00.000Z'),
|
||||
clockBaseTime: new Date('2026-08-16T00:00:00.000Z'),
|
||||
clockTick: 1,
|
||||
clockMode: 'realtime' as const,
|
||||
clockWallAnchor: new Date('2026-08-16T00:00:00.000Z'),
|
||||
lastTurnTick: 1,
|
||||
meta: {
|
||||
lastTurnTime: '2026-08-16T00:00:00.000Z',
|
||||
heartbeatAt: '2026-08-16T00:00:00.000Z',
|
||||
leaseUntil: '2026-08-16T00:01:00.000Z',
|
||||
killturn: 24,
|
||||
},
|
||||
};
|
||||
const config = { const: { feature: 'old' } };
|
||||
const world = {
|
||||
getState: () => ({ ...state, meta: { ...state.meta } }),
|
||||
getScenarioConfig: () => config,
|
||||
} as unknown as InMemoryTurnWorld;
|
||||
const baseline = createWorldReadModelSignature(world);
|
||||
|
||||
state.lastTurnTime = new Date('2026-08-16T00:05:00.000Z');
|
||||
state.clockTick = 2;
|
||||
state.lastTurnTick = 2;
|
||||
state.meta.lastTurnTime = '2026-08-16T00:05:00.000Z';
|
||||
state.meta.heartbeatAt = '2026-08-16T00:00:10.000Z';
|
||||
state.meta.leaseUntil = '2026-08-16T00:01:10.000Z';
|
||||
expect(createWorldReadModelSignature(world)).toBe(baseline);
|
||||
|
||||
state.currentMonth = 2;
|
||||
expect(createWorldReadModelSignature(world)).not.toBe(baseline);
|
||||
state.currentMonth = 1;
|
||||
state.tickSeconds = 300;
|
||||
expect(createWorldReadModelSignature(world)).not.toBe(baseline);
|
||||
state.tickSeconds = 600;
|
||||
config.const.feature = 'new';
|
||||
expect(createWorldReadModelSignature(world)).not.toBe(baseline);
|
||||
config.const.feature = 'old';
|
||||
state.meta.killturn = 25;
|
||||
expect(createWorldReadModelSignature(world)).not.toBe(baseline);
|
||||
});
|
||||
});
|
||||
|
||||
describe('summarizeRealtimeReadModelChanges', () => {
|
||||
it('classifies the committed log rows even when they bypass in-memory log drafts', () => {
|
||||
expect(
|
||||
|
||||
Reference in New Issue
Block a user