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,10 +1,6 @@
|
||||
import { z } from 'zod';
|
||||
|
||||
import type {
|
||||
TurnDaemonCommand,
|
||||
TurnDaemonCommandType,
|
||||
TurnDaemonCommandByType,
|
||||
} from '@sammo-ts/common';
|
||||
import type { TurnDaemonCommand, TurnDaemonCommandType, TurnDaemonCommandByType } from '@sammo-ts/common';
|
||||
|
||||
export type TurnDaemonCommandEnvelope = {
|
||||
requestId: string;
|
||||
@@ -161,22 +157,21 @@ const zSetNationMeta = z.object({
|
||||
expectedUpdatedAt: z.string().optional(),
|
||||
});
|
||||
|
||||
const zAdjustGeneralResources = z
|
||||
.object({
|
||||
type: z.literal('adjustGeneralResources'),
|
||||
reason: z.string().optional(),
|
||||
adjustments: z
|
||||
.array(
|
||||
z
|
||||
.object({
|
||||
generalId: zFiniteNumber,
|
||||
goldDelta: zFiniteNumber.optional(),
|
||||
riceDelta: zFiniteNumber.optional(),
|
||||
})
|
||||
.refine((value) => value.goldDelta !== undefined || value.riceDelta !== undefined)
|
||||
)
|
||||
.min(1),
|
||||
});
|
||||
const zAdjustGeneralResources = z.object({
|
||||
type: z.literal('adjustGeneralResources'),
|
||||
reason: z.string().optional(),
|
||||
adjustments: z
|
||||
.array(
|
||||
z
|
||||
.object({
|
||||
generalId: zFiniteNumber,
|
||||
goldDelta: zFiniteNumber.optional(),
|
||||
riceDelta: zFiniteNumber.optional(),
|
||||
})
|
||||
.refine((value) => value.goldDelta !== undefined || value.riceDelta !== undefined)
|
||||
)
|
||||
.min(1),
|
||||
});
|
||||
|
||||
const zAdjustGeneralMeta = z.object({
|
||||
type: z.literal('adjustGeneralMeta'),
|
||||
@@ -430,10 +425,22 @@ const normalizeGetStatus: CommandNormalizer<'getStatus'> = (envelope) => {
|
||||
};
|
||||
};
|
||||
|
||||
const normalizeRun: CommandNormalizer<'run'> = (envelope) => parseWith(zRun, envelope.command);
|
||||
const normalizePause: CommandNormalizer<'pause'> = (envelope) => parseWith(zPause, envelope.command);
|
||||
const normalizeResume: CommandNormalizer<'resume'> = (envelope) => parseWith(zResume, envelope.command);
|
||||
const normalizeShutdown: CommandNormalizer<'shutdown'> = (envelope) => parseWith(zShutdown, envelope.command);
|
||||
const normalizeRun: CommandNormalizer<'run'> = (envelope) => {
|
||||
const command = parseWith(zRun, envelope.command);
|
||||
return command ? { ...command, requestId: envelope.requestId } : null;
|
||||
};
|
||||
const normalizePause: CommandNormalizer<'pause'> = (envelope) => {
|
||||
const command = parseWith(zPause, envelope.command);
|
||||
return command ? { ...command, requestId: envelope.requestId } : null;
|
||||
};
|
||||
const normalizeResume: CommandNormalizer<'resume'> = (envelope) => {
|
||||
const command = parseWith(zResume, envelope.command);
|
||||
return command ? { ...command, requestId: envelope.requestId } : null;
|
||||
};
|
||||
const normalizeShutdown: CommandNormalizer<'shutdown'> = (envelope) => {
|
||||
const command = parseWith(zShutdown, envelope.command);
|
||||
return command ? { ...command, requestId: envelope.requestId } : null;
|
||||
};
|
||||
|
||||
const normalizers: CommandNormalizerMap = {
|
||||
auctionFinalize: normalizeAuctionFinalize,
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
import {
|
||||
createGamePostgresConnector,
|
||||
type GamePrisma,
|
||||
type InputJsonValue,
|
||||
type TurnEngineCityUpdateInput,
|
||||
type TurnEngineDiplomacyCreateManyInput,
|
||||
@@ -15,7 +16,7 @@ import {
|
||||
import { finalizeLogEntry, LogCategory, LogScope, type LogEntryDraft } from '@sammo-ts/logic';
|
||||
import { asRecord, type RankDataType } from '@sammo-ts/common';
|
||||
|
||||
import type { TurnDaemonHooks } from '../lifecycle/types.js';
|
||||
import type { TurnDaemonCommandResult, TurnDaemonHooks } from '../lifecycle/types.js';
|
||||
import type { InMemoryTurnWorld } from './inMemoryWorld.js';
|
||||
import type { InMemoryReservedTurnStore } from './reservedTurnStore.js';
|
||||
import { buildDiplomacyMeta } from '@sammo-ts/logic';
|
||||
@@ -305,32 +306,37 @@ export const createDatabaseTurnHooks = async (
|
||||
await connector.connect();
|
||||
const prisma = connector.prisma;
|
||||
|
||||
const hooks: TurnDaemonHooks = {
|
||||
flushChanges: async () => {
|
||||
const state = world.getState();
|
||||
const {
|
||||
generals,
|
||||
cities,
|
||||
nations,
|
||||
troops,
|
||||
deletedTroops,
|
||||
deletedGenerals,
|
||||
deletedNations,
|
||||
deletedNationSnapshots,
|
||||
diplomacy,
|
||||
logs,
|
||||
createdGenerals,
|
||||
createdNations,
|
||||
createdTroops,
|
||||
createdDiplomacy,
|
||||
} = world.consumeDirtyState();
|
||||
const persistChanges = async (
|
||||
transaction?: GamePrisma.TransactionClient,
|
||||
commandCompletion?: { requestId: string; result: TurnDaemonCommandResult }
|
||||
): Promise<() => void> => {
|
||||
const state = world.getState();
|
||||
const changes = world.peekDirtyState();
|
||||
const {
|
||||
generals,
|
||||
cities,
|
||||
nations,
|
||||
troops,
|
||||
deletedTroops,
|
||||
deletedGenerals,
|
||||
deletedNations,
|
||||
deletedNationSnapshots,
|
||||
diplomacy,
|
||||
logs,
|
||||
createdGenerals,
|
||||
createdNations,
|
||||
createdTroops,
|
||||
createdDiplomacy,
|
||||
} = changes;
|
||||
const reservedTurnChanges = options?.reservedTurns?.peekDirtyState();
|
||||
|
||||
const worldStateUpdate: TurnEngineWorldStateUpdateInput = {
|
||||
currentYear: state.currentYear,
|
||||
currentMonth: state.currentMonth,
|
||||
tickSeconds: state.tickSeconds,
|
||||
meta: asJson(state.meta),
|
||||
};
|
||||
const worldStateUpdate: TurnEngineWorldStateUpdateInput = {
|
||||
currentYear: state.currentYear,
|
||||
currentMonth: state.currentMonth,
|
||||
tickSeconds: state.tickSeconds,
|
||||
meta: asJson(state.meta),
|
||||
};
|
||||
const persist = async (prisma: GamePrisma.TransactionClient): Promise<void> => {
|
||||
await prisma.worldState.update({
|
||||
where: { id: state.id },
|
||||
data: worldStateUpdate,
|
||||
@@ -462,10 +468,7 @@ export const createDatabaseTurnHooks = async (
|
||||
if (deletedNations.length > 0) {
|
||||
await prisma.diplomacy.deleteMany({
|
||||
where: {
|
||||
OR: [
|
||||
{ srcNationId: { in: deletedNations } },
|
||||
{ destNationId: { in: deletedNations } },
|
||||
],
|
||||
OR: [{ srcNationId: { in: deletedNations } }, { destNationId: { in: deletedNations } }],
|
||||
},
|
||||
});
|
||||
await prisma.nationTurn.deleteMany({
|
||||
@@ -563,9 +566,52 @@ export const createDatabaseTurnHooks = async (
|
||||
});
|
||||
}
|
||||
}
|
||||
if (options?.reservedTurns) {
|
||||
await options.reservedTurns.flushChanges();
|
||||
if (options?.reservedTurns && reservedTurnChanges) {
|
||||
await options.reservedTurns.persistChanges(prisma, reservedTurnChanges);
|
||||
}
|
||||
if (commandCompletion) {
|
||||
await prisma.inputEvent.update({
|
||||
where: { requestId: commandCompletion.requestId },
|
||||
data: {
|
||||
status: 'SUCCEEDED',
|
||||
result: asJson(commandCompletion.result),
|
||||
completedAt: new Date(),
|
||||
error: null,
|
||||
},
|
||||
});
|
||||
}
|
||||
};
|
||||
if (transaction) {
|
||||
await persist(transaction);
|
||||
} else {
|
||||
await prisma.$transaction(persist);
|
||||
}
|
||||
|
||||
return () => {
|
||||
world.acknowledgeDirtyState(changes);
|
||||
if (options?.reservedTurns && reservedTurnChanges) {
|
||||
options.reservedTurns.acknowledgeDirtyState(reservedTurnChanges);
|
||||
}
|
||||
};
|
||||
};
|
||||
|
||||
const hooks: TurnDaemonHooks = {
|
||||
flushChanges: async () => {
|
||||
const acknowledge = await persistChanges();
|
||||
acknowledge();
|
||||
},
|
||||
commitCommand: async (requestId, result) => {
|
||||
const acknowledge = await persistChanges(undefined, { requestId, result });
|
||||
acknowledge();
|
||||
},
|
||||
executeCommand: async (requestId, execute) => {
|
||||
const committed = await prisma.$transaction(async (transaction) => {
|
||||
const result = await execute({ db: transaction });
|
||||
const acknowledge = await persistChanges(transaction, { requestId, result });
|
||||
return { result, acknowledge };
|
||||
});
|
||||
committed.acknowledge();
|
||||
return committed.result;
|
||||
},
|
||||
};
|
||||
|
||||
|
||||
@@ -68,6 +68,23 @@ export interface InMemoryTurnWorldOptions {
|
||||
calendarHandler?: TurnCalendarHandler;
|
||||
}
|
||||
|
||||
export interface TurnWorldChanges {
|
||||
generals: TurnGeneral[];
|
||||
cities: City[];
|
||||
nations: Nation[];
|
||||
troops: Troop[];
|
||||
deletedTroops: number[];
|
||||
deletedGenerals: number[];
|
||||
deletedNations: number[];
|
||||
deletedNationSnapshots: Array<{ nation: Nation; generalIds: number[]; removedAt: Date }>;
|
||||
diplomacy: TurnDiplomacy[];
|
||||
logs: LogEntryDraft[];
|
||||
createdGenerals: TurnGeneral[];
|
||||
createdNations: Nation[];
|
||||
createdTroops: Troop[];
|
||||
createdDiplomacy: TurnDiplomacy[];
|
||||
}
|
||||
|
||||
const compareTurnOrder = (left: TurnGeneral, right: TurnGeneral): number => {
|
||||
const timeDiff = left.turnTime.getTime() - right.turnTime.getTime();
|
||||
if (timeDiff !== 0) {
|
||||
@@ -694,22 +711,7 @@ export class InMemoryTurnWorld {
|
||||
}
|
||||
}
|
||||
|
||||
consumeDirtyState(): {
|
||||
generals: TurnGeneral[];
|
||||
cities: City[];
|
||||
nations: Nation[];
|
||||
troops: Troop[];
|
||||
deletedTroops: number[];
|
||||
deletedGenerals: number[];
|
||||
deletedNations: number[];
|
||||
deletedNationSnapshots: Array<{ nation: Nation; generalIds: number[]; removedAt: Date }>;
|
||||
diplomacy: TurnDiplomacy[];
|
||||
logs: LogEntryDraft[];
|
||||
createdGenerals: TurnGeneral[];
|
||||
createdNations: Nation[];
|
||||
createdTroops: Troop[];
|
||||
createdDiplomacy: TurnDiplomacy[];
|
||||
} {
|
||||
peekDirtyState(): TurnWorldChanges {
|
||||
const generals = Array.from(this.dirtyGeneralIds)
|
||||
.map((id) => this.generals.get(id))
|
||||
.filter((general): general is TurnGeneral => Boolean(general));
|
||||
@@ -740,21 +742,8 @@ export class InMemoryTurnWorld {
|
||||
const deletedTroops = Array.from(this.deletedTroopIds);
|
||||
const deletedGenerals = Array.from(this.deletedGeneralIds);
|
||||
const deletedNations = Array.from(this.deletedNationIds);
|
||||
const deletedNationSnapshots = this.deletedNationSnapshots.splice(0, this.deletedNationSnapshots.length);
|
||||
const logs = this.logs.splice(0, this.logs.length);
|
||||
|
||||
this.dirtyGeneralIds.clear();
|
||||
this.dirtyCityIds.clear();
|
||||
this.dirtyNationIds.clear();
|
||||
this.dirtyTroopIds.clear();
|
||||
this.dirtyDiplomacyKeys.clear();
|
||||
this.createdGeneralIds.clear();
|
||||
this.createdNationIds.clear();
|
||||
this.createdTroopIds.clear();
|
||||
this.createdDiplomacyKeys.clear();
|
||||
this.deletedTroopIds.clear();
|
||||
this.deletedGeneralIds.clear();
|
||||
this.deletedNationIds.clear();
|
||||
const deletedNationSnapshots = this.deletedNationSnapshots.slice();
|
||||
const logs = this.logs.slice();
|
||||
|
||||
return {
|
||||
generals,
|
||||
@@ -774,6 +763,33 @@ export class InMemoryTurnWorld {
|
||||
};
|
||||
}
|
||||
|
||||
acknowledgeDirtyState(changes: TurnWorldChanges): void {
|
||||
for (const general of changes.generals) this.dirtyGeneralIds.delete(general.id);
|
||||
for (const city of changes.cities) this.dirtyCityIds.delete(city.id);
|
||||
for (const nation of changes.nations) this.dirtyNationIds.delete(nation.id);
|
||||
for (const troop of changes.troops) this.dirtyTroopIds.delete(troop.id);
|
||||
for (const entry of changes.diplomacy) {
|
||||
this.dirtyDiplomacyKeys.delete(buildDiplomacyKey(entry.fromNationId, entry.toNationId));
|
||||
}
|
||||
for (const general of changes.createdGenerals) this.createdGeneralIds.delete(general.id);
|
||||
for (const nation of changes.createdNations) this.createdNationIds.delete(nation.id);
|
||||
for (const troop of changes.createdTroops) this.createdTroopIds.delete(troop.id);
|
||||
for (const entry of changes.createdDiplomacy) {
|
||||
this.createdDiplomacyKeys.delete(buildDiplomacyKey(entry.fromNationId, entry.toNationId));
|
||||
}
|
||||
for (const id of changes.deletedTroops) this.deletedTroopIds.delete(id);
|
||||
for (const id of changes.deletedGenerals) this.deletedGeneralIds.delete(id);
|
||||
for (const id of changes.deletedNations) this.deletedNationIds.delete(id);
|
||||
this.deletedNationSnapshots.splice(0, changes.deletedNationSnapshots.length);
|
||||
this.logs.splice(0, changes.logs.length);
|
||||
}
|
||||
|
||||
consumeDirtyState(): TurnWorldChanges {
|
||||
const changes = this.peekDirtyState();
|
||||
this.acknowledgeDirtyState(changes);
|
||||
return changes;
|
||||
}
|
||||
|
||||
private removeCollapsedNations(): void {
|
||||
const collapsedNationIds: number[] = [];
|
||||
for (const nation of this.nations.values()) {
|
||||
|
||||
@@ -72,6 +72,11 @@ const buildNationKey = (nationId: number, officerLevel: number): string => `${na
|
||||
|
||||
type ReservedTurnDatabaseClient = Pick<TurnEngineDatabaseClient, 'generalTurn' | 'nationTurn'>;
|
||||
|
||||
export interface ReservedTurnChanges {
|
||||
generalIds: number[];
|
||||
nationKeys: string[];
|
||||
}
|
||||
|
||||
export class InMemoryReservedTurnStore {
|
||||
private readonly generalTurns = new Map<number, ReservedTurnEntry[]>();
|
||||
private readonly nationTurns = new Map<string, ReservedTurnEntry[]>();
|
||||
@@ -213,12 +218,27 @@ export class InMemoryReservedTurnStore {
|
||||
this.dirtyNationKeys.add(key);
|
||||
}
|
||||
|
||||
async flushChanges(): Promise<void> {
|
||||
const generalIds = Array.from(this.dirtyGeneralIds);
|
||||
for (const generalId of generalIds) {
|
||||
peekDirtyState(): ReservedTurnChanges {
|
||||
return {
|
||||
generalIds: Array.from(this.dirtyGeneralIds),
|
||||
nationKeys: Array.from(this.dirtyNationKeys),
|
||||
};
|
||||
}
|
||||
|
||||
acknowledgeDirtyState(changes: ReservedTurnChanges): void {
|
||||
for (const generalId of changes.generalIds) {
|
||||
this.dirtyGeneralIds.delete(generalId);
|
||||
}
|
||||
for (const key of changes.nationKeys) {
|
||||
this.dirtyNationKeys.delete(key);
|
||||
}
|
||||
}
|
||||
|
||||
async persistChanges(prisma: ReservedTurnDatabaseClient, changes: ReservedTurnChanges): Promise<void> {
|
||||
for (const generalId of changes.generalIds) {
|
||||
const turns = this.getGeneralTurns(generalId);
|
||||
await this.prisma.generalTurn.deleteMany({ where: { generalId } });
|
||||
await this.prisma.generalTurn.createMany({
|
||||
await prisma.generalTurn.deleteMany({ where: { generalId } });
|
||||
await prisma.generalTurn.createMany({
|
||||
data: turns.map((entry, turnIdx) => ({
|
||||
generalId,
|
||||
turnIdx,
|
||||
@@ -228,16 +248,15 @@ export class InMemoryReservedTurnStore {
|
||||
});
|
||||
}
|
||||
|
||||
const nationKeys = Array.from(this.dirtyNationKeys);
|
||||
for (const key of nationKeys) {
|
||||
for (const key of changes.nationKeys) {
|
||||
const [nationIdRaw, officerLevelRaw] = key.split(':');
|
||||
const nationId = Number(nationIdRaw);
|
||||
const officerLevel = Number(officerLevelRaw);
|
||||
const turns = this.getNationTurns(nationId, officerLevel);
|
||||
await this.prisma.nationTurn.deleteMany({
|
||||
await prisma.nationTurn.deleteMany({
|
||||
where: { nationId, officerLevel },
|
||||
});
|
||||
await this.prisma.nationTurn.createMany({
|
||||
await prisma.nationTurn.createMany({
|
||||
data: turns.map((entry, turnIdx) => ({
|
||||
nationId,
|
||||
officerLevel,
|
||||
@@ -247,9 +266,12 @@ export class InMemoryReservedTurnStore {
|
||||
})),
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
this.dirtyGeneralIds.clear();
|
||||
this.dirtyNationKeys.clear();
|
||||
async flushChanges(): Promise<void> {
|
||||
const changes = this.peekDirtyState();
|
||||
await this.persistChanges(this.prisma, changes);
|
||||
this.acknowledgeDirtyState(changes);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
import type { TurnCommandProfile, TurnSchedule } from '@sammo-ts/logic';
|
||||
import { buildGameEventChannel, type RealtimeEvent } from '@sammo-ts/common';
|
||||
import { createRedisConnector, resolveRedisConfigFromEnv } from '@sammo-ts/infra';
|
||||
import { createGamePostgresConnector, createRedisConnector, resolveRedisConfigFromEnv } from '@sammo-ts/infra';
|
||||
import { NATION_TRAIT_KEYS, NationTraitLoader, loadNationTraitModules } from '@sammo-ts/logic';
|
||||
|
||||
import { SystemClock } from '../lifecycle/clock.js';
|
||||
@@ -8,7 +8,7 @@ import { getNextTickTime } from '../lifecycle/getNextTickTime.js';
|
||||
import { InMemoryControlQueue } from '../lifecycle/inMemoryControlQueue.js';
|
||||
import type { Clock, TurnDaemonControlQueue, TurnDaemonHooks, TurnRunBudget } from '../lifecycle/types.js';
|
||||
import { TurnDaemonLifecycle } from '../lifecycle/turnDaemonLifecycle.js';
|
||||
import { buildTurnDaemonStreamKeys, RedisTurnDaemonCommandStream } from '../lifecycle/redisCommandStream.js';
|
||||
import { DatabaseTurnDaemonCommandQueue } from '../lifecycle/databaseCommandQueue.js';
|
||||
import type { MapLoaderOptions } from '../scenario/mapLoader.js';
|
||||
import { createDatabaseTurnHooks } from './databaseHooks.js';
|
||||
import type { GeneralTurnHandler, InMemoryTurnWorldOptions, TurnCalendarHandler } from './inMemoryWorld.js';
|
||||
@@ -201,7 +201,6 @@ export const createTurnDaemonRuntime = async (options: TurnDaemonRuntimeOptions)
|
||||
let auctionFinalizer: Awaited<ReturnType<typeof createAuctionFinalizer>> | null = null;
|
||||
let auctionBidder: Awaited<ReturnType<typeof createAuctionBidder>> | null = null;
|
||||
let tournamentRewardFinalizer: Awaited<ReturnType<typeof createTournamentRewardFinalizer>> | null = null;
|
||||
let redisCommandStream: RedisTurnDaemonCommandStream | null = null;
|
||||
let pauseGate: (() => Promise<boolean>) | undefined;
|
||||
let adminActionConsumer: Awaited<ReturnType<typeof createGatewayAdminActionConsumer>> | null = null;
|
||||
const gatewayGate = options.profileName
|
||||
@@ -222,17 +221,14 @@ export const createTurnDaemonRuntime = async (options: TurnDaemonRuntimeOptions)
|
||||
auctionBidder = await createAuctionBidder({
|
||||
databaseUrl: options.databaseUrl,
|
||||
world,
|
||||
hooks: dbHooks.hooks,
|
||||
});
|
||||
auctionFinalizer = await createAuctionFinalizer({
|
||||
databaseUrl: options.databaseUrl,
|
||||
world,
|
||||
hooks: dbHooks.hooks,
|
||||
});
|
||||
tournamentRewardFinalizer = await createTournamentRewardFinalizer({
|
||||
databaseUrl: options.databaseUrl,
|
||||
world,
|
||||
hooks: dbHooks.hooks,
|
||||
});
|
||||
hooks = {
|
||||
...dbHooks.hooks,
|
||||
@@ -290,10 +286,6 @@ export const createTurnDaemonRuntime = async (options: TurnDaemonRuntimeOptions)
|
||||
redisConnector = createRedisConnector(redisConfig);
|
||||
await redisConnector.connect();
|
||||
const redisClient = redisConnector.client;
|
||||
redisCommandStream = new RedisTurnDaemonCommandStream(redisClient, {
|
||||
keys: buildTurnDaemonStreamKeys(options.profileName ?? options.profile),
|
||||
startId: options.commandStreamStartId,
|
||||
});
|
||||
const realtimeChannel = buildGameEventChannel(options.profileName ?? options.profile);
|
||||
publishRealtimeEvent = async (event: RealtimeEvent) => {
|
||||
await redisClient.publish(realtimeChannel, JSON.stringify(event));
|
||||
@@ -320,6 +312,13 @@ export const createTurnDaemonRuntime = async (options: TurnDaemonRuntimeOptions)
|
||||
};
|
||||
}
|
||||
|
||||
const commandConnector = hooks ? createGamePostgresConnector({ url: options.databaseUrl }) : null;
|
||||
const databaseCommandQueue = commandConnector ? new DatabaseTurnDaemonCommandQueue(commandConnector.prisma) : null;
|
||||
if (commandConnector && databaseCommandQueue) {
|
||||
await commandConnector.connect();
|
||||
await databaseCommandQueue.initialize();
|
||||
}
|
||||
|
||||
const baseClose = close;
|
||||
close = async () => {
|
||||
await baseClose();
|
||||
@@ -330,12 +329,12 @@ export const createTurnDaemonRuntime = async (options: TurnDaemonRuntimeOptions)
|
||||
if (redisConnector) {
|
||||
await redisConnector.disconnect();
|
||||
}
|
||||
await commandConnector?.disconnect();
|
||||
};
|
||||
|
||||
const resolvedControlQueue = options.controlQueue ?? redisCommandStream ?? controlQueue;
|
||||
const resolvedControlQueue = options.controlQueue ?? databaseCommandQueue ?? controlQueue;
|
||||
const commandHandler = createTurnDaemonCommandHandler({
|
||||
world,
|
||||
hooks,
|
||||
auctionFinalizer: auctionFinalizer ?? undefined,
|
||||
auctionBidder: auctionBidder ?? undefined,
|
||||
tournamentRewardFinalizer: tournamentRewardFinalizer ?? undefined,
|
||||
@@ -357,7 +356,7 @@ export const createTurnDaemonRuntime = async (options: TurnDaemonRuntimeOptions)
|
||||
hooks,
|
||||
pauseGate,
|
||||
commandHandler,
|
||||
commandResponder: redisCommandStream ?? undefined,
|
||||
commandResponder: options.controlQueue ? undefined : (databaseCommandQueue ?? undefined),
|
||||
},
|
||||
{ profile: options.profile, defaultBudget }
|
||||
);
|
||||
|
||||
@@ -1,10 +1,10 @@
|
||||
import type {
|
||||
TurnDaemonHooks,
|
||||
TurnDaemonCommandHandler,
|
||||
TurnDaemonCommand,
|
||||
TurnDaemonCommandExecutionContext,
|
||||
TurnDaemonCommandResult,
|
||||
TurnRunResult,
|
||||
} from '../lifecycle/types.js';
|
||||
import type { GamePrisma } from '@sammo-ts/infra';
|
||||
import { asRecord, JosaUtil, LiteHashDRBG, RandUtil } from '@sammo-ts/common';
|
||||
import {
|
||||
LogCategory,
|
||||
@@ -23,25 +23,6 @@ import {
|
||||
import type { InMemoryTurnWorld } from './inMemoryWorld.js';
|
||||
import type { TurnGeneral } from './types.js';
|
||||
|
||||
const buildFlushResult = (world: InMemoryTurnWorld): TurnRunResult => {
|
||||
const state = world.getState();
|
||||
return {
|
||||
lastTurnTime: state.lastTurnTime.toISOString(),
|
||||
processedGenerals: 0,
|
||||
processedTurns: 0,
|
||||
durationMs: 0,
|
||||
partial: false,
|
||||
checkpoint: world.getCheckpoint(),
|
||||
};
|
||||
};
|
||||
|
||||
const flushWorld = async (world: InMemoryTurnWorld, hooks?: TurnDaemonHooks): Promise<void> => {
|
||||
if (!hooks?.flushChanges) {
|
||||
return;
|
||||
}
|
||||
await hooks.flushChanges(buildFlushResult(world));
|
||||
};
|
||||
|
||||
let itemRegistryPromise: Promise<Map<string, ItemModule>> | null = null;
|
||||
|
||||
const getItemRegistry = async (): Promise<Map<string, ItemModule>> => {
|
||||
@@ -67,29 +48,35 @@ const readMetaNumber = (meta: Record<string, unknown>, key: string, fallback: nu
|
||||
|
||||
interface CommandHandlerContext {
|
||||
world: InMemoryTurnWorld;
|
||||
hooks?: TurnDaemonHooks;
|
||||
commandDb?: GamePrisma.TransactionClient;
|
||||
auctionFinalizer?: AuctionFinalizer;
|
||||
auctionBidder?: AuctionBidder;
|
||||
tournamentRewardFinalizer?: TournamentRewardFinalizer;
|
||||
}
|
||||
|
||||
interface AuctionFinalizer {
|
||||
finalize(auctionId: number): Promise<TurnDaemonCommandResult>;
|
||||
finalize(auctionId: number, db?: GamePrisma.TransactionClient): Promise<TurnDaemonCommandResult>;
|
||||
}
|
||||
|
||||
interface AuctionBidder {
|
||||
bid(command: Extract<TurnDaemonCommand, { type: 'auctionBid' }>): Promise<TurnDaemonCommandResult>;
|
||||
bid(
|
||||
command: Extract<TurnDaemonCommand, { type: 'auctionBid' }>,
|
||||
db?: GamePrisma.TransactionClient
|
||||
): Promise<TurnDaemonCommandResult>;
|
||||
}
|
||||
|
||||
interface TournamentRewardFinalizer {
|
||||
finalize(command: Extract<TurnDaemonCommand, { type: 'tournamentReward' }>): Promise<TurnDaemonCommandResult>;
|
||||
finalize(
|
||||
command: Extract<TurnDaemonCommand, { type: 'tournamentReward' }>,
|
||||
db?: GamePrisma.TransactionClient
|
||||
): Promise<TurnDaemonCommandResult>;
|
||||
}
|
||||
|
||||
async function handleSetNationMeta(
|
||||
ctx: CommandHandlerContext,
|
||||
command: Extract<TurnDaemonCommand, { type: 'setNationMeta' }>
|
||||
): Promise<TurnDaemonCommandResult> {
|
||||
const { world, hooks } = ctx;
|
||||
const { world } = ctx;
|
||||
const nation = world.getNationById(command.nationId);
|
||||
if (!nation) {
|
||||
return {
|
||||
@@ -122,7 +109,6 @@ async function handleSetNationMeta(
|
||||
world.updateNation(command.nationId, {
|
||||
meta: nextMeta,
|
||||
});
|
||||
await flushWorld(world, hooks);
|
||||
return {
|
||||
type: 'setNationMeta',
|
||||
ok: true,
|
||||
@@ -135,7 +121,7 @@ async function handleAdjustGeneralResources(
|
||||
ctx: CommandHandlerContext,
|
||||
command: Extract<TurnDaemonCommand, { type: 'adjustGeneralResources' }>
|
||||
): Promise<TurnDaemonCommandResult> {
|
||||
const { world, hooks } = ctx;
|
||||
const { world } = ctx;
|
||||
if (!command.adjustments || command.adjustments.length === 0) {
|
||||
return { type: 'adjustGeneralResources', ok: false, reason: '조정 대상이 없습니다.' };
|
||||
}
|
||||
@@ -176,8 +162,6 @@ async function handleAdjustGeneralResources(
|
||||
totalGoldDelta += goldDelta;
|
||||
totalRiceDelta += riceDelta;
|
||||
}
|
||||
|
||||
await flushWorld(world, hooks);
|
||||
return {
|
||||
type: 'adjustGeneralResources',
|
||||
ok: true,
|
||||
@@ -192,7 +176,7 @@ async function handleAdjustGeneralMeta(
|
||||
ctx: CommandHandlerContext,
|
||||
command: Extract<TurnDaemonCommand, { type: 'adjustGeneralMeta' }>
|
||||
): Promise<TurnDaemonCommandResult> {
|
||||
const { world, hooks } = ctx;
|
||||
const { world } = ctx;
|
||||
if (!command.adjustments || command.adjustments.length === 0) {
|
||||
return {
|
||||
type: 'adjustGeneralMeta',
|
||||
@@ -224,8 +208,6 @@ async function handleAdjustGeneralMeta(
|
||||
world.updateGeneral(adjustment.generalId, { meta: nextMeta });
|
||||
processed += 1;
|
||||
}
|
||||
|
||||
await flushWorld(world, hooks);
|
||||
return {
|
||||
type: 'adjustGeneralMeta',
|
||||
ok: true,
|
||||
@@ -238,7 +220,7 @@ async function handleTournamentMatchResult(
|
||||
ctx: CommandHandlerContext,
|
||||
command: Extract<TurnDaemonCommand, { type: 'tournamentMatchResult' }>
|
||||
): Promise<TurnDaemonCommandResult> {
|
||||
const { world, hooks } = ctx;
|
||||
const { world } = ctx;
|
||||
const resolvePrefix = (type: number): string => {
|
||||
switch (type) {
|
||||
case 1:
|
||||
@@ -342,8 +324,6 @@ async function handleTournamentMatchResult(
|
||||
nextMeta[rankKey('g')] = defenderG + defenderGDelta;
|
||||
world.updateGeneral(defender.id, { meta: nextMeta });
|
||||
}
|
||||
|
||||
await flushWorld(world, hooks);
|
||||
return {
|
||||
type: 'tournamentMatchResult',
|
||||
ok: true,
|
||||
@@ -358,7 +338,7 @@ async function handlePatchGeneral(
|
||||
ctx: CommandHandlerContext,
|
||||
command: Extract<TurnDaemonCommand, { type: 'patchGeneral' }>
|
||||
): Promise<TurnDaemonCommandResult> {
|
||||
const { world, hooks } = ctx;
|
||||
const { world } = ctx;
|
||||
const general = world.getGeneralById(command.generalId);
|
||||
if (!general) {
|
||||
return {
|
||||
@@ -396,7 +376,6 @@ async function handlePatchGeneral(
|
||||
}
|
||||
|
||||
world.updateGeneral(command.generalId, patch);
|
||||
await flushWorld(world, hooks);
|
||||
return { type: 'patchGeneral', ok: true, generalId: command.generalId };
|
||||
}
|
||||
|
||||
@@ -404,7 +383,7 @@ async function handleTroopJoin(
|
||||
ctx: CommandHandlerContext,
|
||||
command: Extract<TurnDaemonCommand, { type: 'troopJoin' }>
|
||||
): Promise<TurnDaemonCommandResult> {
|
||||
const { world, hooks } = ctx;
|
||||
const { world } = ctx;
|
||||
const general = world.getGeneralById(command.generalId);
|
||||
if (!general) {
|
||||
return {
|
||||
@@ -448,7 +427,6 @@ async function handleTroopJoin(
|
||||
world.updateGeneral(command.generalId, {
|
||||
troopId: command.troopId,
|
||||
});
|
||||
await flushWorld(world, hooks);
|
||||
return {
|
||||
type: 'troopJoin',
|
||||
ok: true,
|
||||
@@ -461,7 +439,7 @@ async function handleTroopExit(
|
||||
ctx: CommandHandlerContext,
|
||||
command: Extract<TurnDaemonCommand, { type: 'troopExit' }>
|
||||
): Promise<TurnDaemonCommandResult> {
|
||||
const { world, hooks } = ctx;
|
||||
const { world } = ctx;
|
||||
const general = world.getGeneralById(command.generalId);
|
||||
if (!general) {
|
||||
return {
|
||||
@@ -484,7 +462,6 @@ async function handleTroopExit(
|
||||
world.updateGeneral(command.generalId, {
|
||||
troopId: 0,
|
||||
});
|
||||
await flushWorld(world, hooks);
|
||||
return {
|
||||
type: 'troopExit',
|
||||
ok: true,
|
||||
@@ -499,7 +476,6 @@ async function handleTroopExit(
|
||||
world.updateGeneral(member.id, { troopId: 0 });
|
||||
}
|
||||
world.removeTroop(troopId);
|
||||
await flushWorld(world, hooks);
|
||||
return {
|
||||
type: 'troopExit',
|
||||
ok: true,
|
||||
@@ -512,7 +488,7 @@ async function handleDieOnPrestart(
|
||||
ctx: CommandHandlerContext,
|
||||
command: Extract<TurnDaemonCommand, { type: 'dieOnPrestart' }>
|
||||
): Promise<TurnDaemonCommandResult> {
|
||||
const { world, hooks } = ctx;
|
||||
const { world } = ctx;
|
||||
const general = world.getGeneralById(command.generalId);
|
||||
if (!general) {
|
||||
return {
|
||||
@@ -533,7 +509,6 @@ async function handleDieOnPrestart(
|
||||
}
|
||||
|
||||
world.removeGeneral(command.generalId);
|
||||
await flushWorld(world, hooks);
|
||||
return { type: 'dieOnPrestart', ok: true, generalId: command.generalId };
|
||||
}
|
||||
|
||||
@@ -619,7 +594,7 @@ async function handleSetMySetting(
|
||||
ctx: CommandHandlerContext,
|
||||
command: Extract<TurnDaemonCommand, { type: 'setMySetting' }>
|
||||
): Promise<TurnDaemonCommandResult> {
|
||||
const { world, hooks } = ctx;
|
||||
const { world } = ctx;
|
||||
const general = world.getGeneralById(command.generalId);
|
||||
if (!general) {
|
||||
return {
|
||||
@@ -635,7 +610,6 @@ async function handleSetMySetting(
|
||||
...command.settings,
|
||||
},
|
||||
});
|
||||
await flushWorld(world, hooks);
|
||||
return { type: 'setMySetting', ok: true, generalId: command.generalId };
|
||||
}
|
||||
|
||||
@@ -643,7 +617,7 @@ async function handleDropItem(
|
||||
ctx: CommandHandlerContext,
|
||||
command: Extract<TurnDaemonCommand, { type: 'dropItem' }>
|
||||
): Promise<TurnDaemonCommandResult> {
|
||||
const { world, hooks } = ctx;
|
||||
const { world } = ctx;
|
||||
const general = world.getGeneralById(command.generalId);
|
||||
if (!general) {
|
||||
return { type: 'dropItem', ok: false, generalId: command.generalId, reason: '장수 정보를 찾을 수 없습니다.' };
|
||||
@@ -664,7 +638,6 @@ async function handleDropItem(
|
||||
items,
|
||||
},
|
||||
});
|
||||
await flushWorld(world, hooks);
|
||||
return { type: 'dropItem', ok: true, generalId: command.generalId };
|
||||
}
|
||||
|
||||
@@ -680,7 +653,7 @@ async function handleAuctionFinalize(
|
||||
reason: '경매 확정기가 준비되지 않았습니다.',
|
||||
};
|
||||
}
|
||||
return ctx.auctionFinalizer.finalize(command.auctionId);
|
||||
return ctx.auctionFinalizer.finalize(command.auctionId, ctx.commandDb);
|
||||
}
|
||||
|
||||
async function handleAuctionBid(
|
||||
@@ -695,14 +668,14 @@ async function handleAuctionBid(
|
||||
reason: '경매 입찰기가 준비되지 않았습니다.',
|
||||
};
|
||||
}
|
||||
return ctx.auctionBidder.bid(command);
|
||||
return ctx.auctionBidder.bid(command, ctx.commandDb);
|
||||
}
|
||||
|
||||
async function handleChangePermission(
|
||||
ctx: CommandHandlerContext,
|
||||
command: Extract<TurnDaemonCommand, { type: 'changePermission' }>
|
||||
): Promise<TurnDaemonCommandResult> {
|
||||
const { world, hooks } = ctx;
|
||||
const { world } = ctx;
|
||||
const general = world.getGeneralById(command.generalId);
|
||||
if (!general) {
|
||||
return {
|
||||
@@ -728,8 +701,6 @@ async function handleChangePermission(
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
await flushWorld(world, hooks);
|
||||
return { type: 'changePermission', ok: true, generalId: command.generalId };
|
||||
}
|
||||
|
||||
@@ -737,7 +708,7 @@ async function handleKick(
|
||||
ctx: CommandHandlerContext,
|
||||
command: Extract<TurnDaemonCommand, { type: 'kick' }>
|
||||
): Promise<TurnDaemonCommandResult> {
|
||||
const { world, hooks } = ctx;
|
||||
const { world } = ctx;
|
||||
const general = world.getGeneralById(command.generalId);
|
||||
if (!general) {
|
||||
return { type: 'kick', ok: false, generalId: command.generalId, reason: '장수 정보를 찾을 수 없습니다.' };
|
||||
@@ -761,8 +732,6 @@ async function handleKick(
|
||||
nationId: 0,
|
||||
officerLevel: 0,
|
||||
});
|
||||
|
||||
await flushWorld(world, hooks);
|
||||
return { type: 'kick', ok: true, generalId: command.generalId };
|
||||
}
|
||||
|
||||
@@ -770,7 +739,7 @@ async function handleAppoint(
|
||||
ctx: CommandHandlerContext,
|
||||
command: Extract<TurnDaemonCommand, { type: 'appoint' }>
|
||||
): Promise<TurnDaemonCommandResult> {
|
||||
const { world, hooks } = ctx;
|
||||
const { world } = ctx;
|
||||
const general = world.getGeneralById(command.generalId);
|
||||
if (!general) {
|
||||
return { type: 'appoint', ok: false, generalId: command.generalId, reason: '장수 정보를 찾을 수 없습니다.' };
|
||||
@@ -825,8 +794,6 @@ async function handleAppoint(
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
await flushWorld(world, hooks);
|
||||
return { type: 'appoint', ok: true, generalId: command.generalId };
|
||||
}
|
||||
|
||||
@@ -834,7 +801,7 @@ async function handleTournamentRefund(
|
||||
ctx: CommandHandlerContext,
|
||||
command: Extract<TurnDaemonCommand, { type: 'tournamentRefund' }>
|
||||
): Promise<TurnDaemonCommandResult> {
|
||||
const { world, hooks } = ctx;
|
||||
const { world } = ctx;
|
||||
if (!command.refunds || command.refunds.length === 0) {
|
||||
return {
|
||||
type: 'tournamentRefund',
|
||||
@@ -866,8 +833,6 @@ async function handleTournamentRefund(
|
||||
processed += 1;
|
||||
totalRefund += refund.amount;
|
||||
}
|
||||
|
||||
await flushWorld(world, hooks);
|
||||
return {
|
||||
type: 'tournamentRefund',
|
||||
ok: true,
|
||||
@@ -882,7 +847,7 @@ async function handleTournamentBettingPayout(
|
||||
ctx: CommandHandlerContext,
|
||||
command: Extract<TurnDaemonCommand, { type: 'tournamentBettingPayout' }>
|
||||
): Promise<TurnDaemonCommandResult> {
|
||||
const { world, hooks } = ctx;
|
||||
const { world } = ctx;
|
||||
if (!command.payouts || command.payouts.length === 0) {
|
||||
return {
|
||||
type: 'tournamentBettingPayout',
|
||||
@@ -935,8 +900,6 @@ async function handleTournamentBettingPayout(
|
||||
nextMeta[betwingoldKey] = currentBetwingold + delta.betwingold;
|
||||
world.updateGeneral(generalId, { meta: nextMeta });
|
||||
}
|
||||
|
||||
await flushWorld(world, hooks);
|
||||
return {
|
||||
type: 'tournamentBettingPayout',
|
||||
ok: true,
|
||||
@@ -960,7 +923,7 @@ async function handleTournamentReward(
|
||||
reason: '보상 처리기가 준비되지 않았습니다.',
|
||||
};
|
||||
}
|
||||
return ctx.tournamentRewardFinalizer.finalize(command);
|
||||
return ctx.tournamentRewardFinalizer.finalize(command, ctx.commandDb);
|
||||
}
|
||||
|
||||
// 설문 보상은 API에서 전달된 RNG 결과를 재검증한 뒤 월드에 반영한다.
|
||||
@@ -968,7 +931,7 @@ async function handleVoteReward(
|
||||
ctx: CommandHandlerContext,
|
||||
command: Extract<TurnDaemonCommand, { type: 'voteReward' }>
|
||||
): Promise<TurnDaemonCommandResult> {
|
||||
const { world, hooks } = ctx;
|
||||
const { world } = ctx;
|
||||
const general = world.getGeneralById(command.generalId);
|
||||
if (!general) {
|
||||
return {
|
||||
@@ -1153,7 +1116,6 @@ async function handleVoteReward(
|
||||
}
|
||||
|
||||
world.updateGeneral(command.generalId, patch);
|
||||
await flushWorld(world, hooks);
|
||||
return {
|
||||
type: 'voteReward',
|
||||
ok: true,
|
||||
@@ -1166,54 +1128,81 @@ async function handleVoteReward(
|
||||
|
||||
export const createTurnDaemonCommandHandler = (options: {
|
||||
world: InMemoryTurnWorld;
|
||||
hooks?: TurnDaemonHooks;
|
||||
auctionFinalizer?: AuctionFinalizer;
|
||||
auctionBidder?: AuctionBidder;
|
||||
tournamentRewardFinalizer?: TournamentRewardFinalizer;
|
||||
}): TurnDaemonCommandHandler => {
|
||||
const ctx = {
|
||||
const ctx: CommandHandlerContext = {
|
||||
world: options.world,
|
||||
hooks: options.hooks,
|
||||
auctionFinalizer: options.auctionFinalizer,
|
||||
auctionBidder: options.auctionBidder,
|
||||
tournamentRewardFinalizer: options.tournamentRewardFinalizer,
|
||||
};
|
||||
|
||||
type HandlerMap = Partial<Record<TurnDaemonCommand['type'], (command: TurnDaemonCommand) => Promise<TurnDaemonCommandResult>>>;
|
||||
type HandlerMap = Partial<
|
||||
Record<TurnDaemonCommand['type'], (command: TurnDaemonCommand) => Promise<TurnDaemonCommandResult>>
|
||||
>;
|
||||
|
||||
const handlers: HandlerMap = {
|
||||
troopJoin: (command) => handleTroopJoin(ctx, command as Extract<TurnDaemonCommand, { type: 'troopJoin' }>),
|
||||
troopExit: (command) => handleTroopExit(ctx, command as Extract<TurnDaemonCommand, { type: 'troopExit' }>),
|
||||
dieOnPrestart: (command) => handleDieOnPrestart(ctx, command as Extract<TurnDaemonCommand, { type: 'dieOnPrestart' }>),
|
||||
buildNationCandidate: (command) => handleBuildNationCandidate(ctx, command as Extract<TurnDaemonCommand, { type: 'buildNationCandidate' }>),
|
||||
instantRetreat: (command) => handleInstantRetreat(ctx, command as Extract<TurnDaemonCommand, { type: 'instantRetreat' }>),
|
||||
dieOnPrestart: (command) =>
|
||||
handleDieOnPrestart(ctx, command as Extract<TurnDaemonCommand, { type: 'dieOnPrestart' }>),
|
||||
buildNationCandidate: (command) =>
|
||||
handleBuildNationCandidate(ctx, command as Extract<TurnDaemonCommand, { type: 'buildNationCandidate' }>),
|
||||
instantRetreat: (command) =>
|
||||
handleInstantRetreat(ctx, command as Extract<TurnDaemonCommand, { type: 'instantRetreat' }>),
|
||||
vacation: (command) => handleVacation(ctx, command as Extract<TurnDaemonCommand, { type: 'vacation' }>),
|
||||
setMySetting: (command) => handleSetMySetting(ctx, command as Extract<TurnDaemonCommand, { type: 'setMySetting' }>),
|
||||
setMySetting: (command) =>
|
||||
handleSetMySetting(ctx, command as Extract<TurnDaemonCommand, { type: 'setMySetting' }>),
|
||||
dropItem: (command) => handleDropItem(ctx, command as Extract<TurnDaemonCommand, { type: 'dropItem' }>),
|
||||
auctionFinalize: (command) => handleAuctionFinalize(ctx, command as Extract<TurnDaemonCommand, { type: 'auctionFinalize' }>),
|
||||
auctionFinalize: (command) =>
|
||||
handleAuctionFinalize(ctx, command as Extract<TurnDaemonCommand, { type: 'auctionFinalize' }>),
|
||||
auctionBid: (command) => handleAuctionBid(ctx, command as Extract<TurnDaemonCommand, { type: 'auctionBid' }>),
|
||||
changePermission: (command) => handleChangePermission(ctx, command as Extract<TurnDaemonCommand, { type: 'changePermission' }>),
|
||||
changePermission: (command) =>
|
||||
handleChangePermission(ctx, command as Extract<TurnDaemonCommand, { type: 'changePermission' }>),
|
||||
kick: (command) => handleKick(ctx, command as Extract<TurnDaemonCommand, { type: 'kick' }>),
|
||||
appoint: (command) => handleAppoint(ctx, command as Extract<TurnDaemonCommand, { type: 'appoint' }>),
|
||||
tournamentRefund: (command) => handleTournamentRefund(ctx, command as Extract<TurnDaemonCommand, { type: 'tournamentRefund' }>),
|
||||
tournamentBettingPayout: (command) => handleTournamentBettingPayout(ctx, command as Extract<TurnDaemonCommand, { type: 'tournamentBettingPayout' }>),
|
||||
tournamentReward: (command) => handleTournamentReward(ctx, command as Extract<TurnDaemonCommand, { type: 'tournamentReward' }>),
|
||||
tournamentRefund: (command) =>
|
||||
handleTournamentRefund(ctx, command as Extract<TurnDaemonCommand, { type: 'tournamentRefund' }>),
|
||||
tournamentBettingPayout: (command) =>
|
||||
handleTournamentBettingPayout(
|
||||
ctx,
|
||||
command as Extract<TurnDaemonCommand, { type: 'tournamentBettingPayout' }>
|
||||
),
|
||||
tournamentReward: (command) =>
|
||||
handleTournamentReward(ctx, command as Extract<TurnDaemonCommand, { type: 'tournamentReward' }>),
|
||||
voteReward: (command) => handleVoteReward(ctx, command as Extract<TurnDaemonCommand, { type: 'voteReward' }>),
|
||||
setNationMeta: (command) => handleSetNationMeta(ctx, command as Extract<TurnDaemonCommand, { type: 'setNationMeta' }>),
|
||||
adjustGeneralResources: (command) => handleAdjustGeneralResources(ctx, command as Extract<TurnDaemonCommand, { type: 'adjustGeneralResources' }>),
|
||||
adjustGeneralMeta: (command) => handleAdjustGeneralMeta(ctx, command as Extract<TurnDaemonCommand, { type: 'adjustGeneralMeta' }>),
|
||||
setNationMeta: (command) =>
|
||||
handleSetNationMeta(ctx, command as Extract<TurnDaemonCommand, { type: 'setNationMeta' }>),
|
||||
adjustGeneralResources: (command) =>
|
||||
handleAdjustGeneralResources(
|
||||
ctx,
|
||||
command as Extract<TurnDaemonCommand, { type: 'adjustGeneralResources' }>
|
||||
),
|
||||
adjustGeneralMeta: (command) =>
|
||||
handleAdjustGeneralMeta(ctx, command as Extract<TurnDaemonCommand, { type: 'adjustGeneralMeta' }>),
|
||||
tournamentMatchResult: (command) =>
|
||||
handleTournamentMatchResult(ctx, command as Extract<TurnDaemonCommand, { type: 'tournamentMatchResult' }>),
|
||||
patchGeneral: (command) => handlePatchGeneral(ctx, command as Extract<TurnDaemonCommand, { type: 'patchGeneral' }>),
|
||||
patchGeneral: (command) =>
|
||||
handlePatchGeneral(ctx, command as Extract<TurnDaemonCommand, { type: 'patchGeneral' }>),
|
||||
};
|
||||
|
||||
return {
|
||||
handle: async (command): Promise<TurnDaemonCommandResult | null> => {
|
||||
handle: async (
|
||||
command,
|
||||
executionContext?: TurnDaemonCommandExecutionContext
|
||||
): Promise<TurnDaemonCommandResult | null> => {
|
||||
const handler = handlers[command.type];
|
||||
if (!handler) {
|
||||
return null;
|
||||
}
|
||||
return handler(command);
|
||||
ctx.commandDb = executionContext?.db;
|
||||
try {
|
||||
return await handler(command);
|
||||
} finally {
|
||||
ctx.commandDb = undefined;
|
||||
}
|
||||
},
|
||||
};
|
||||
};
|
||||
|
||||
Reference in New Issue
Block a user