feat: API 변화 저널과 outbox worker를 연결

입력 이벤트의 업무 변경, 성공 원장, revision 및 outbox를 한 transaction에서 커밋한다. 설문의 commit 전 Redis 발행을 제거하고 접속 점수용 private revision과 재시도·retention을 갖춘 dispatcher lifecycle을 추가한다.
This commit is contained in:
2026-08-16 18:22:57 +00:00
parent b41b161e9c
commit f858ca43f4
15 changed files with 606 additions and 52 deletions
+11 -1
View File
@@ -1,6 +1,8 @@
import { randomUUID } from 'node:crypto';
import { initTRPC, TRPCError } from '@trpc/server';
import { ChangeJournal } from '@sammo-ts/common';
import { isGameAccessBlocked } from '@sammo-ts/common/auth/sanctions';
import { writeReadModelChangeJournal } from '@sammo-ts/infra';
import type { GameApiContext } from './context.js';
import { IdempotentTurnDaemonTransport } from './daemon/idempotentTransport.js';
@@ -45,8 +47,10 @@ const inputEventMiddleware = t.middleware(async ({ ctx, type, path, next }) => {
}
const requestId = `${ctx.requestId ?? randomUUID()}:${path}`;
const changeJournal = new ChangeJournal();
let journalPersisted = false;
try {
return await executeInputEvent({
const result = await executeInputEvent({
db: ctx.db,
requestId,
eventType: path,
@@ -56,15 +60,21 @@ const inputEventMiddleware = t.middleware(async ({ ctx, type, path, next }) => {
ctx: {
...ctx,
db: transaction,
changeJournal,
turnDaemon: new IdempotentTurnDaemonTransport(ctx.turnDaemon, requestId),
},
});
if (!result.ok) {
throw result.error;
}
journalPersisted = Boolean(await writeReadModelChangeJournal(transaction, changeJournal.snapshot()));
return result;
},
});
if (journalPersisted) {
ctx.readModelOutbox?.wake();
}
return result;
} catch (error) {
if (error instanceof DuplicateInputEventError) {
throw new TRPCError({