feat: 계정 Web Push 전달 기반과 사건 판정을 추가한다

게임 상태 변경과 같은 트랜잭션에 내구성 outbox를 기록하고 Gateway가 계정 설정과 기기 구독으로 fan-out하도록 한다. 전역 기본값은 비활성으로 유지하며 migration과 회귀 테스트를 함께 추가한다.
This commit is contained in:
2026-08-23 13:16:07 +00:00
parent f858806295
commit 7bc3213b03
34 changed files with 1964 additions and 9 deletions
+18 -3
View File
@@ -126,6 +126,7 @@ const buildContext = (options: {
const logCreate = vi.fn(async () => ({}));
const findMany = vi.fn(async () => (target ? [{ id: target.id, name: target.name }] : []));
const inheritanceLogFindMany = vi.fn(async () => options.inheritanceLogs ?? []);
const webPushOutboxCreateMany = vi.fn(async () => ({ count: 1 }));
const activeWorldState =
options.configConst === undefined
? worldState
@@ -167,9 +168,11 @@ const buildContext = (options: {
general?.userId === where.userId ? general : null
),
findMany,
findUnique: vi.fn(async ({ where }: { where: { id: number } }) =>
target?.id === where.id ? target : null
),
findUnique: vi.fn(async ({ where }: { where: { id: number } }) => {
if (target?.id === where.id) return target;
if (general?.id === where.id) return general;
return null;
}),
},
nation: {
findUnique: vi.fn(async ({ where }: { where: { id: number } }) =>
@@ -193,6 +196,9 @@ const buildContext = (options: {
findUnique: vi.fn(async () => null),
upsert: vi.fn(async () => ({})),
},
webPushOutbox: {
createMany: webPushOutboxCreateMany,
},
};
const accessTokenStore = new RedisAccessTokenStore(
{
@@ -225,6 +231,7 @@ const buildContext = (options: {
findMany,
inheritanceLogFindMany,
messageRows,
webPushOutboxCreateMany,
changeJournal,
};
};
@@ -613,6 +620,14 @@ describe('inherit router actor and permission boundaries', () => {
}),
}),
]);
expect(fixture.webPushOutboxCreateMany).toHaveBeenNthCalledWith(1, {
data: [{ eventId: 'message:101', eventType: 'PRIVATE_MESSAGE_RECEIVED', userIds: ['user-1'] }],
skipDuplicates: true,
});
expect(fixture.webPushOutboxCreateMany).toHaveBeenNthCalledWith(2, {
data: [{ eventId: 'message:102', eventType: 'PRIVATE_MESSAGE_RECEIVED', userIds: ['user-2'] }],
skipDuplicates: true,
});
expect(fixture.changeJournal.snapshot()).toEqual([
{ domain: 'messages.mailbox', entityId: 7 },
{ domain: 'messages.mailbox', entityId: 8 },