feat: implement input event system with durable command handling
- Introduced InputEvent model with status tracking (PENDING, PROCESSING, SUCCEEDED, FAILED) and unique request IDs. - Added DatabaseTurnDaemonTransport for sending commands and handling idempotency. - Implemented executeInputEvent function to manage input event lifecycle and error handling. - Created DatabaseTurnDaemonCommandQueue for managing command processing and lease recovery. - Enhanced turn daemon lifecycle to support atomic command execution and error recovery. - Added tests for input event atomicity, command queuing, and error handling scenarios.
This commit is contained in:
@@ -1,12 +1,15 @@
|
||||
import { JosaUtil, asRecord } from '@sammo-ts/common';
|
||||
import { createGamePostgresConnector } from '@sammo-ts/infra';
|
||||
import { createGamePostgresConnector, type GamePrisma } from '@sammo-ts/infra';
|
||||
import { ActionLogger, LogFormat, type TournamentType, type TriggerValue } from '@sammo-ts/logic';
|
||||
|
||||
import type { TurnDaemonCommand, TurnDaemonCommandResult, TurnDaemonHooks } from '../lifecycle/types.js';
|
||||
import type { TurnDaemonCommand, TurnDaemonCommandResult } from '../lifecycle/types.js';
|
||||
import type { InMemoryTurnWorld } from '../turn/inMemoryWorld.js';
|
||||
|
||||
export interface TournamentRewardFinalizer {
|
||||
finalize(command: Extract<TurnDaemonCommand, { type: 'tournamentReward' }>): Promise<TurnDaemonCommandResult>;
|
||||
finalize(
|
||||
command: Extract<TurnDaemonCommand, { type: 'tournamentReward' }>,
|
||||
db?: GamePrisma.TransactionClient
|
||||
): Promise<TurnDaemonCommandResult>;
|
||||
close(): Promise<void>;
|
||||
}
|
||||
|
||||
@@ -57,39 +60,22 @@ const pushLogs = (world: InMemoryTurnWorld, logs: ReturnType<ActionLogger['flush
|
||||
}
|
||||
};
|
||||
|
||||
const flushWorld = async (world: InMemoryTurnWorld, hooks?: TurnDaemonHooks): Promise<void> => {
|
||||
if (!hooks?.flushChanges) {
|
||||
return;
|
||||
}
|
||||
const state = world.getState();
|
||||
await hooks.flushChanges({
|
||||
lastTurnTime: state.lastTurnTime.toISOString(),
|
||||
processedGenerals: 0,
|
||||
processedTurns: 0,
|
||||
durationMs: 0,
|
||||
partial: false,
|
||||
checkpoint: world.getCheckpoint(),
|
||||
});
|
||||
};
|
||||
|
||||
export const createTournamentRewardFinalizer = async (options: {
|
||||
databaseUrl: string;
|
||||
world: InMemoryTurnWorld;
|
||||
hooks?: TurnDaemonHooks;
|
||||
}): Promise<TournamentRewardFinalizer> => {
|
||||
const connector = createGamePostgresConnector({ url: options.databaseUrl });
|
||||
await connector.connect();
|
||||
const prisma = connector.prisma;
|
||||
|
||||
const finalize = async (
|
||||
command: Extract<TurnDaemonCommand, { type: 'tournamentReward' }>
|
||||
command: Extract<TurnDaemonCommand, { type: 'tournamentReward' }>,
|
||||
commandDb?: GamePrisma.TransactionClient
|
||||
): Promise<TurnDaemonCommandResult> => {
|
||||
const { world, hooks } = options;
|
||||
const { world } = options;
|
||||
const db = commandDb ?? prisma;
|
||||
const { winnerId, runnerUpId } = command;
|
||||
const rewardMap = new Map<
|
||||
number,
|
||||
{ gold: number; exp: number; label: string; inheritPoint: number }
|
||||
>();
|
||||
const rewardMap = new Map<number, { gold: number; exp: number; label: string; inheritPoint: number }>();
|
||||
|
||||
const applyTier = (
|
||||
ids: number[],
|
||||
@@ -126,7 +112,7 @@ export const createTournamentRewardFinalizer = async (options: {
|
||||
}
|
||||
|
||||
const nameMap = new Map<number, string>();
|
||||
const generals = await prisma.general.findMany({
|
||||
const generals = await db.general.findMany({
|
||||
where: { id: { in: Array.from(rewardMap.keys()) } },
|
||||
select: { id: true, userId: true, name: true },
|
||||
});
|
||||
@@ -240,7 +226,7 @@ export const createTournamentRewardFinalizer = async (options: {
|
||||
.filter((entry) => !!entry.userId);
|
||||
|
||||
for (const entry of pointUpdates) {
|
||||
await prisma.inheritancePoint.upsert({
|
||||
await db.inheritancePoint.upsert({
|
||||
where: {
|
||||
userId_key: { userId: entry.userId!, key: 'tournament' },
|
||||
},
|
||||
@@ -249,8 +235,6 @@ export const createTournamentRewardFinalizer = async (options: {
|
||||
});
|
||||
}
|
||||
|
||||
await flushWorld(world, hooks);
|
||||
|
||||
return {
|
||||
type: 'tournamentReward',
|
||||
ok: true,
|
||||
@@ -269,4 +253,4 @@ export const createTournamentRewardFinalizer = async (options: {
|
||||
await connector.disconnect();
|
||||
},
|
||||
};
|
||||
};
|
||||
};
|
||||
|
||||
Reference in New Issue
Block a user