feat: 직접 writer를 내구성 변화 저널에 연결

86개 API mutation을 분류하고 메시지 mailbox, 베팅, 국가 설정, 예약 명령의 revision/outbox 표식을 소유 transaction에 연결한다. 공개 SSE는 식별자 없는 invalidation만 노출한다.
This commit is contained in:
2026-08-16 18:38:08 +00:00
parent e4ac2a60b1
commit dd4645e4d0
22 changed files with 485 additions and 55 deletions
@@ -5,9 +5,11 @@ import type { ReadModelOutboxDatabase } from '@sammo-ts/infra';
import { ReadModelOutboxWorker } from '../src/realtime/outboxWorker.js';
const payload = (domain: 'front.general' | 'access.general' | 'tournament' | 'betting') => ({
const payload = (
domain: 'front.general' | 'access.general' | 'dashboard.global' | 'messages.mailbox' | 'tournament' | 'betting'
) => ({
version: 1,
changes: [[domain, domain === 'front.general' || domain === 'access.general' ? 7 : 0, '1']],
changes: [[domain, domain === 'front.general' || domain === 'access.general' ? 7 : domain === 'messages.mailbox' ? 9999 : 0, '1']],
});
const createFixture = (rows: readonly object[]) => {
@@ -47,7 +49,7 @@ describe('ReadModelOutboxWorker', () => {
);
});
it.each(['access.general', 'tournament', 'betting'] as const)(
it.each(['access.general', 'dashboard.global', 'tournament', 'betting'] as const)(
'marks a %s-only envelope delivered without dashboard Redis publish',
async (domain) => {
const fixture = createFixture([{ id: 12n, payload: payload(domain), attempts: 1 }]);
@@ -65,6 +67,24 @@ describe('ReadModelOutboxWorker', () => {
}
);
it('publishes a durable mailbox wake-up without the legacy dashboard revision', async () => {
const fixture = createFixture([{ id: 14n, payload: payload('messages.mailbox'), attempts: 1 }]);
const worker = new ReadModelOutboxWorker(fixture.db, fixture.redis, 'che:default', {
owner: 'worker-test',
intervalMs: 60_000,
});
worker.start();
await vi.waitFor(() => expect(fixture.updateMany).toHaveBeenCalledTimes(1));
await worker.stop();
expect(fixture.incr).not.toHaveBeenCalled();
expect(JSON.parse(String(fixture.publish.mock.calls[0]?.[1]))).toEqual({
type: 'messagesChanged',
mailboxes: [9999],
});
});
it('coalesces repeated wakeups into one trailing batch and waits for it on shutdown', async () => {
let releaseFirst: (() => void) | undefined;
const first = new Promise<readonly object[]>((resolve) => {