fix: API 입력 이벤트의 원자적 재실행을 보장한다

This commit is contained in:
2026-08-24 16:30:38 +00:00
parent 604e8fdaf2
commit cca2b3d925
9 changed files with 1141 additions and 119 deletions
+20 -5
View File
@@ -1,5 +1,6 @@
import { randomUUID } from 'node:crypto';
import { initTRPC, TRPCError } from '@trpc/server';
import { middlewareMarker } from '@trpc/server/unstable-core-do-not-import';
import { ChangeJournal } from '@sammo-ts/common';
import { isGameAccessBlocked } from '@sammo-ts/common/auth/sanctions';
import { writeReadModelChangeJournal } from '@sammo-ts/infra';
@@ -58,19 +59,25 @@ const generalActivityMiddleware = t.middleware(async ({ ctx, type, next }) => {
return result;
});
const inputEventMiddleware = t.middleware(async ({ ctx, type, path, next }) => {
export const scopeApiInputEventRequestId = (baseRequestId: string, path: string, batchIndex: number): string =>
`${baseRequestId}:${path}${batchIndex === 0 ? '' : `:batch:${batchIndex}`}`;
const inputEventMiddleware = t.middleware(async ({ ctx, type, path, batchIndex, getRawInput, next }) => {
if (type !== 'mutation' || !ctx.db.$transaction) {
return next();
}
const requestId = `${ctx.requestId ?? randomUUID()}:${path}`;
const requestId = scopeApiInputEventRequestId(ctx.requestId ?? randomUUID(), path, batchIndex);
const payload = await getRawInput();
const changeJournal = new ChangeJournal();
let journalPersisted = false;
let executedResult: Awaited<ReturnType<typeof next>> | undefined;
try {
const result = await executeInputEvent({
const response = await executeInputEvent({
db: ctx.db,
requestId,
eventType: path,
payload,
actorUserId: ctx.auth?.user.id,
execute: async (transaction) => {
const result = await next({
@@ -85,13 +92,21 @@ const inputEventMiddleware = t.middleware(async ({ ctx, type, path, next }) => {
throw result.error;
}
journalPersisted = Boolean(await writeReadModelChangeJournal(transaction, changeJournal.snapshot()));
return result;
executedResult = result;
return result.data;
},
});
if (journalPersisted) {
ctx.readModelOutbox?.wake();
}
return result;
if (executedResult) {
return executedResult;
}
return {
marker: middlewareMarker,
ok: true,
data: response,
};
} catch (error) {
if (error instanceof DuplicateInputEventError) {
throw new TRPCError({