감사 수집과 저장 장애를 게임 진행에서 분리한다

This commit is contained in:
2026-09-26 16:26:02 +00:00
parent daf7a87799
commit 6b65633324
22 changed files with 1108 additions and 550 deletions
+31 -17
View File
@@ -1,5 +1,10 @@
import { TRPCError } from '@trpc/server';
import { persistAuditDiplomacyEvents, type AuditDiplomacyEventDraft, type GamePrisma } from '@sammo-ts/infra';
import {
withPlayAuditSavepoint,
persistAuditDiplomacyEvents,
type AuditDiplomacyEventDraft,
type GamePrisma,
} from '@sammo-ts/infra';
import type { ApiInputExecutionContext } from '../inputEventBoundary.js';
import { asRecord, JosaUtil } from '@sammo-ts/common';
@@ -104,6 +109,7 @@ const persistEffects = async (
}
): Promise<void> => {
const changes: AuditDiplomacyEventDraft[] = [];
let collectionFailed = false;
const states = new Map(audit?.before.map((row) => [`${row.srcNationId}:${row.destNationId}`, row]));
const project = (row: GamePrisma.DiplomacyGetPayload<Record<string, never>>) => ({
state: row.stateCode,
@@ -129,22 +135,26 @@ const persistEffects = async (
},
});
if (audit) {
const key = `${effect.srcNationId}:${effect.destNationId}`;
const previous = states.get(key);
if (!previous) throw new Error('Missing locked diplomacy audit state');
const before = project(previous);
const after = project(updated);
if (JSON.stringify(before) !== JSON.stringify(after)) {
changes.push({
...audit.base,
srcNationId: effect.srcNationId,
destNationId: effect.destNationId,
ordinal: changes.length + 1,
before,
after,
});
try {
const key = `${effect.srcNationId}:${effect.destNationId}`;
const previous = states.get(key);
if (!previous) throw new Error('Missing locked diplomacy audit state');
const before = project(previous);
const after = project(updated);
if (JSON.stringify(before) !== JSON.stringify(after)) {
changes.push({
...audit.base,
srcNationId: effect.srcNationId,
destNationId: effect.destNationId,
ordinal: changes.length + 1,
before,
after,
});
}
states.set(key, updated);
} catch {
collectionFailed = true;
}
states.set(key, updated);
}
} else if (effect.type === 'nation:patch' && effect.targetId !== undefined) {
const patch = effect.patch;
@@ -159,7 +169,11 @@ const persistEffects = async (
}
}
await persistLogs(db, orderLegacyActionLoggerFlush(logs), year, month, at, serverId);
await persistAuditDiplomacyEvents(db, changes);
if (changes.length || collectionFailed)
await withPlayAuditSavepoint(db, async (auditDb) => {
if (collectionFailed) throw new Error('Audit relation collection failed');
await persistAuditDiplomacyEvents(auditDb, changes);
});
};
const refreshFrontStates = async (db: DatabaseClient, mapName: string, nationIds: number[]): Promise<number[]> => {
@@ -117,6 +117,12 @@ export const readAuditWorld = async (tx: GamePrisma.TransactionClient) => {
startYear,
startMonth,
collectionStart,
historyGap: (() => {
const gap = asRecord(meta.playAuditGap);
return gap.serverId === serverId && Number.isInteger(gap.firstYear) && Number.isInteger(gap.firstMonth)
? { firstYear: Number(gap.firstYear), firstMonth: Number(gap.firstMonth) }
: null;
})(),
tick: world.lastTurnTick?.toString() ?? null,
asOf: new Date().toISOString(),
};
+45 -36
View File
@@ -3,6 +3,7 @@ import {
GamePrisma,
hashAuditDiplomacyDocument,
persistAuditDiplomacyEvents,
withPlayAuditSavepoint,
projectAuditDocumentState,
readTurnRuntimeReady,
type AuditDiplomacyEventDraft,
@@ -83,6 +84,7 @@ const readCoordinate = async (ctx: GameApiContext) => {
};
export const createDiplomacyDocumentAudit = (ctx: GameApiContext, actor: GeneralRow, permission: number) => {
let collectionFailed = false;
const changes: {
letter: Letter;
before: Record<string, unknown> | null;
@@ -92,48 +94,55 @@ export const createDiplomacyDocumentAudit = (ctx: GameApiContext, actor: General
return {
record: (letter: Letter, before: Record<string, unknown> | null, eventType: DocumentAction) => {
if (!ctx.auditInput) return;
changes.push({ letter, before, after: projectAuditDocumentState(letter), eventType });
try {
changes.push({ letter, before, after: projectAuditDocumentState(letter), eventType });
} catch {
collectionFailed = true;
}
},
flush: async (): Promise<void> => {
// 무transaction legacy unit fixture에는 입력 원장 identity를 꾸며 넣지 않는다.
const input = ctx.auditInput;
if (!input || !changes.length) return;
if (!input || (!changes.length && !collectionFailed)) return;
if (input.actorUserId !== actor.userId || input.actorUserId !== ctx.auth?.user.id)
throw new Error('Play audit diplomacy actor mismatch');
const coordinate = await readCoordinate(ctx);
if (!coordinate) return;
const events: AuditDiplomacyEventDraft[] = changes.map((change, index) => ({
schemaVersion: 1,
serverId: coordinate.serverId,
srcNationId: change.letter.srcNationId,
destNationId: change.letter.destNationId,
category: 'DOCUMENT',
source: 'API',
eventType: change.eventType,
documentId: change.letter.id,
documentHash: hashAuditDiplomacyDocument(change.letter),
previousDocumentId: change.letter.prevId,
year: coordinate.year,
month: coordinate.month,
tick: coordinate.tick,
clockRevision: coordinate.clockRevision,
executionId: `api:${input.requestId}`,
ordinal: index + 1,
requestId: input.requestId,
inputSequence: input.sequence,
actor: {
userId: actor.userId,
generalId: actor.id,
name: actor.name,
nationId: actor.nationId,
officerLevel: actor.officerLevel,
npcState: actor.npcState,
permission,
},
before: change.before,
after: change.after,
}));
await persistAuditDiplomacyEvents(ctx.db, events);
await withPlayAuditSavepoint(ctx.db, async (auditDb) => {
if (collectionFailed) throw new Error('Audit document collection failed');
const coordinate = await readCoordinate({ ...ctx, db: auditDb });
if (!coordinate) return;
const events: AuditDiplomacyEventDraft[] = changes.map((change, index) => ({
schemaVersion: 1,
serverId: coordinate.serverId,
srcNationId: change.letter.srcNationId,
destNationId: change.letter.destNationId,
category: 'DOCUMENT',
source: 'API',
eventType: change.eventType,
documentId: change.letter.id,
documentHash: hashAuditDiplomacyDocument(change.letter),
previousDocumentId: change.letter.prevId,
year: coordinate.year,
month: coordinate.month,
tick: coordinate.tick,
clockRevision: coordinate.clockRevision,
executionId: `api:${input.requestId}`,
ordinal: index + 1,
requestId: input.requestId,
inputSequence: input.sequence,
actor: {
userId: actor.userId,
generalId: actor.id,
name: actor.name,
nationId: actor.nationId,
officerLevel: actor.officerLevel,
npcState: actor.npcState,
permission,
},
before: change.before,
after: change.after,
}));
await persistAuditDiplomacyEvents(auditDb, events);
});
},
};
};