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
+1
View File
@@ -44,4 +44,5 @@ export interface DatabaseClient {
vote: GamePrisma.VoteDelegate;
inputEvent: GamePrisma.InputEventDelegate;
turnDaemonLease: GamePrisma.TurnDaemonLeaseDelegate;
readModelOutbox: GamePrisma.ReadModelOutboxDelegate;
}
+4 -2
View File
@@ -6,7 +6,9 @@ import {
type ReadModelRevisionKey,
} from '@sammo-ts/common';
import { GamePrisma } from './gamePrisma.js';
import { GamePrisma, type GamePrismaClient } from './gamePrisma.js';
export type ReadModelJournalDatabase = Pick<GamePrismaClient, '$queryRaw'>;
interface ReadModelJournalWriteRow {
domain: string;
@@ -61,7 +63,7 @@ const assertWriteRows = (
* that owns the domain mutation; this function never opens or commits one.
*/
export const writeReadModelChangeJournal = async (
transaction: GamePrisma.TransactionClient,
transaction: ReadModelJournalDatabase,
candidates: Iterable<ReadModelRevisionKey>
): Promise<ReadModelJournalWriteResult | null> => {
const keys = normalizeReadModelRevisionKeys(candidates);
@@ -2,6 +2,10 @@ import { parseReadModelOutboxPayload, type ReadModelOutboxPayloadV1 } from '@sam
import { GamePrisma, type GamePrismaClient } from './gamePrisma.js';
export interface ReadModelOutboxDatabase extends Pick<GamePrismaClient, '$queryRaw'> {
readModelOutbox: GamePrisma.ReadModelOutboxDelegate;
}
export interface ClaimedReadModelOutbox {
id: bigint;
payload: unknown;
@@ -46,7 +50,7 @@ const retryDelayMs = (attempts: number, baseMs: number, maxMs: number): number =
};
export const claimReadModelOutboxBatch = async (
db: GamePrismaClient,
db: ReadModelOutboxDatabase,
options: Pick<ReadModelOutboxDispatchOptions, 'owner' | 'limit' | 'leaseMs'> & { now?: Date }
): Promise<ClaimedReadModelOutbox[]> => {
if (!options.owner.trim()) {
@@ -82,7 +86,7 @@ export const claimReadModelOutboxBatch = async (
};
export const markReadModelOutboxDelivered = async (
db: GamePrismaClient,
db: ReadModelOutboxDatabase,
input: { id: bigint; owner: string; deliveredAt?: Date }
): Promise<boolean> => {
const result = await db.readModelOutbox.updateMany({
@@ -98,7 +102,7 @@ export const markReadModelOutboxDelivered = async (
};
export const releaseReadModelOutbox = async (
db: GamePrismaClient,
db: ReadModelOutboxDatabase,
input: { id: bigint; owner: string; error: unknown; availableAt: Date }
): Promise<boolean> => {
const result = await db.readModelOutbox.updateMany({
@@ -114,7 +118,7 @@ export const releaseReadModelOutbox = async (
};
export const dispatchReadModelOutboxBatch = async (
db: GamePrismaClient,
db: ReadModelOutboxDatabase,
publish: (payload: ReadModelOutboxPayloadV1, outboxId: bigint) => Promise<void>,
options: ReadModelOutboxDispatchOptions
): Promise<ReadModelOutboxDispatchResult> => {
@@ -156,7 +160,7 @@ export const dispatchReadModelOutboxBatch = async (
};
export const pruneDeliveredReadModelOutbox = async (
db: GamePrismaClient,
db: ReadModelOutboxDatabase,
input: { deliveredBefore: Date; limit?: number }
): Promise<number> => {
const limit = normalizeLimit(input.limit);