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
+10 -16
View File
@@ -1,5 +1,6 @@
import { describe, expect, it, vi } from 'vitest';
import { ChangeJournal } from '@sammo-ts/common';
import type { GameSessionTokenPayload } from '@sammo-ts/common/auth/gameToken';
import type { GamePrisma, RedisConnector } from '@sammo-ts/infra';
@@ -106,6 +107,7 @@ const buildContext = (options: {
}));
const redisIncr = vi.fn(async (_key: string) => 41);
const redisPublish = vi.fn(async (_channel: string, _message: string) => 1);
const changeJournal = new ChangeJournal();
const queryRaw = vi.fn(async (query: GamePrisma.Sql) => {
const text = sqlText(query);
if (text.includes('FROM vote_poll') && text.includes('LIMIT 1')) {
@@ -193,8 +195,9 @@ const buildContext = (options: {
accessTokenStore,
flushStore: new InMemoryFlushStore(),
gameTokenSecret: 'test-secret',
changeJournal,
};
return { context, requestCommand, queryRaw, db, redisIncr, redisPublish };
return { context, requestCommand, queryRaw, db, redisIncr, redisPublish, changeJournal };
};
describe('vote router actor and permission boundaries', () => {
@@ -221,16 +224,9 @@ describe('vote router actor and permission boundaries', () => {
goldReward: 90,
})
);
expect(fixture.redisIncr).toHaveBeenCalledWith('sammo:che:default:read-model:revision');
const published = JSON.parse(String(fixture.redisPublish.mock.calls[0]?.[1]));
expect(published).toMatchObject({
type: 'readModelChanged',
revision: 41,
changes: {
frontStatusActorIds: [7],
frontStatusChanged: false,
},
});
expect(fixture.changeJournal.snapshot()).toEqual([{ domain: 'front.general', entityId: 7 }]);
expect(fixture.redisIncr).not.toHaveBeenCalled();
expect(fixture.redisPublish).not.toHaveBeenCalled();
});
it('publishes a global front-status projection after creating a survey', async () => {
@@ -244,11 +240,9 @@ describe('vote router actor and permission boundaries', () => {
})
).resolves.toEqual({ ok: true });
const published = JSON.parse(String(fixture.redisPublish.mock.calls[0]?.[1]));
expect(published).toMatchObject({
type: 'readModelChanged',
changes: { frontStatusChanged: true },
});
expect(fixture.changeJournal.snapshot()).toEqual([{ domain: 'front.global', entityId: 0 }]);
expect(fixture.redisIncr).not.toHaveBeenCalled();
expect(fixture.redisPublish).not.toHaveBeenCalled();
});
it('uses the current world develcost for the legacy five-times survey reward', async () => {