Files
core2026/packages/infra/src/webPushOutbox.ts
T
Hide_D 7bc3213b03 feat: 계정 Web Push 전달 기반과 사건 판정을 추가한다
게임 상태 변경과 같은 트랜잭션에 내구성 outbox를 기록하고 Gateway가 계정 설정과 기기 구독으로 fan-out하도록 한다. 전역 기본값은 비활성으로 유지하며 migration과 회귀 테스트를 함께 추가한다.
2026-08-23 13:16:07 +00:00

54 lines
1.7 KiB
TypeScript

import type { WebPushEventType } from '@sammo-ts/common';
import type { GamePrisma } from './gamePrisma.js';
export type WebPushOutboxDatabase = Pick<GamePrisma.TransactionClient, 'general' | 'webPushOutbox'>;
export interface WebPushOutboxEventInput {
eventId: string;
eventType: WebPushEventType;
userIds?: readonly string[];
year?: number;
month?: number;
}
const uniqueUserIds = (values: readonly string[]): string[] => [...new Set(values.filter(Boolean))].sort();
export const enqueueWebPushOutboxEvents = async (
db: WebPushOutboxDatabase,
events: readonly WebPushOutboxEventInput[]
): Promise<number> => {
if (events.length === 0) return 0;
const result = await db.webPushOutbox.createMany({
data: events.map((event) => ({
eventId: event.eventId,
eventType: event.eventType,
userIds: uniqueUserIds(event.userIds ?? []),
...(event.year === undefined ? {} : { year: event.year }),
...(event.month === undefined ? {} : { month: event.month }),
})),
skipDuplicates: true,
});
return result.count;
};
export const enqueuePrivateMessageWebPush = async (
db: WebPushOutboxDatabase,
draft: { msgType: string; mailbox: number; destId: number },
messageId: number
): Promise<void> => {
if (draft.msgType !== 'private' || draft.mailbox !== draft.destId) return;
const recipient = await db.general.findUnique({
where: { id: draft.destId },
select: { userId: true },
});
if (!recipient?.userId) return;
await enqueueWebPushOutboxEvents(db, [
{
eventId: `message:${messageId}`,
eventType: 'PRIVATE_MESSAGE_RECEIVED',
userIds: [recipient.userId],
},
]);
};