86개 API mutation을 분류하고 메시지 mailbox, 베팅, 국가 설정, 예약 명령의 revision/outbox 표식을 소유 transaction에 연결한다. 공개 SSE는 식별자 없는 invalidation만 노출한다.
104 lines
3.8 KiB
TypeScript
104 lines
3.8 KiB
TypeScript
import { describe, expect, it } from 'vitest';
|
|
|
|
import {
|
|
hasRealtimeReadModelChanges,
|
|
parseReadModelOutboxPayload,
|
|
readModelOutboxPayloadToMessageMailboxes,
|
|
readModelOutboxPayloadToChanges,
|
|
resolveRealtimeReadModelInvalidation,
|
|
} from '../src/index.js';
|
|
|
|
describe('read-model outbox payload', () => {
|
|
it('validates the version, domain, entity ID, and bigint revision', () => {
|
|
expect(
|
|
parseReadModelOutboxPayload({
|
|
version: 1,
|
|
changes: [
|
|
['general.content', 7, '12'],
|
|
['map.world', 0, '3'],
|
|
],
|
|
})
|
|
).toEqual({
|
|
version: 1,
|
|
changes: [
|
|
['general.content', 7, '12'],
|
|
['map.world', 0, '3'],
|
|
],
|
|
});
|
|
expect(parseReadModelOutboxPayload({ version: 2, changes: [] })).toBeNull();
|
|
expect(parseReadModelOutboxPayload({ version: 1, changes: [['unknown', 0, '1']] })).toBeNull();
|
|
expect(parseReadModelOutboxPayload({ version: 1, changes: [['map.world', -1, '1']] })).toBeNull();
|
|
expect(parseReadModelOutboxPayload({ version: 1, changes: [['map.world', 0, '-1']] })).toBeNull();
|
|
});
|
|
|
|
it('reconstructs an idempotent internal invalidation without broadening map-only changes', () => {
|
|
const payload = parseReadModelOutboxPayload({
|
|
version: 1,
|
|
changes: [
|
|
['general.content', 7, '2'],
|
|
['map.general', 7, '2'],
|
|
['map.world', 0, '9'],
|
|
['front.general', 7, '3'],
|
|
['front.nation', 4, '5'],
|
|
['records.general', 7, '8'],
|
|
['reserved.general', 7, '4'],
|
|
['lobby.general', 7, '2'],
|
|
['contacts.world', 0, '6'],
|
|
],
|
|
});
|
|
if (!payload) throw new Error('valid payload rejected');
|
|
|
|
const changes = readModelOutboxPayloadToChanges(payload);
|
|
expect(changes).toMatchObject({
|
|
generalIds: [7],
|
|
mapGeneralIds: [7],
|
|
mapChanged: true,
|
|
frontStatusActorIds: [7],
|
|
frontStatusNationIds: [4],
|
|
recordGeneralIds: [7],
|
|
reservedGeneralIds: [7],
|
|
lobbyGeneralIds: [7],
|
|
contactsChanged: true,
|
|
worldChanged: false,
|
|
});
|
|
expect(hasRealtimeReadModelChanges(changes)).toBe(true);
|
|
expect(resolveRealtimeReadModelInvalidation(changes, { generalId: 99, cityId: 2, nationId: 9 })).toMatchObject(
|
|
{
|
|
map: true,
|
|
context: false,
|
|
commands: false,
|
|
lobby: false,
|
|
}
|
|
);
|
|
});
|
|
|
|
it('keeps source-only, access-only, tournament, and betting domains off the dashboard channel', () => {
|
|
const payload = parseReadModelOutboxPayload({
|
|
version: 1,
|
|
changes: [
|
|
['access.general', 7, '1'],
|
|
['dashboard.global', 0, '1'],
|
|
['tournament', 0, '1'],
|
|
['betting', 0, '1'],
|
|
],
|
|
});
|
|
if (!payload) throw new Error('valid payload rejected');
|
|
expect(hasRealtimeReadModelChanges(readModelOutboxPayloadToChanges(payload))).toBe(false);
|
|
});
|
|
|
|
it('keeps durable mailbox revisions separate from dashboard invalidations', () => {
|
|
const payload = parseReadModelOutboxPayload({
|
|
version: 1,
|
|
changes: [
|
|
['messages.mailbox', 9999, '3'],
|
|
['messages.mailbox', 7, '2'],
|
|
['messages.mailbox', 7, '2'],
|
|
],
|
|
});
|
|
if (!payload) throw new Error('valid payload rejected');
|
|
|
|
expect(readModelOutboxPayloadToMessageMailboxes(payload)).toEqual([7, 9999]);
|
|
expect(hasRealtimeReadModelChanges(readModelOutboxPayloadToChanges(payload))).toBe(false);
|
|
});
|
|
});
|