feat: make general creation daemon-atomic

This commit is contained in:
2026-07-31 00:33:03 +00:00
parent 115218ded8
commit c6a2a0da93
34 changed files with 2227 additions and 655 deletions
+71 -10
View File
@@ -45,6 +45,7 @@ import {
reselectGeneralFromSelectionPool,
SelectPoolError,
} from './selectPoolService.js';
import { createGeneralFromJoin, JoinCreateGeneralError } from './joinCreateGeneralService.js';
let itemRegistryPromise: Promise<Map<string, ItemModule>> | null = null;
@@ -131,21 +132,85 @@ const requireCommandDatabase = (ctx: CommandHandlerContext): DatabaseClient => {
const resolveCommandAcceptedAt = async (
db: DatabaseClient,
command: Extract<TurnDaemonCommand, { type: 'selectPoolCreate' | 'selectPoolReselect' }>
command: Extract<TurnDaemonCommand, { type: 'joinCreateGeneral' | 'selectPoolCreate' | 'selectPoolReselect' }>
): Promise<Date> => {
if (!command.requestId) {
throw new Error(`${command.type} requestId is required.`);
}
const event = await db.inputEvent.findUnique({
where: { requestId: command.requestId },
select: { createdAt: true },
select: { createdAt: true, actorUserId: true },
});
if (!event) {
throw new Error(`ENGINE input event ${command.requestId} is missing.`);
}
if (event.actorUserId !== command.userId) {
throw new Error(`ENGINE input event actor does not match ${command.type} user.`);
}
return event.createdAt;
};
async function handleJoinCreateGeneral(
ctx: CommandHandlerContext,
command: Extract<TurnDaemonCommand, { type: 'joinCreateGeneral' }>
): Promise<TurnDaemonCommandResult> {
const db = requireCommandDatabase(ctx);
const worldState = await db.worldState.findUnique({
where: { id: ctx.world.getState().id },
});
if (!worldState) {
throw new Error('Join world state is missing.');
}
const acceptedAt = await resolveCommandAcceptedAt(db, command);
try {
return {
type: 'joinCreateGeneral',
...(await createGeneralFromJoin({
db,
world: ctx.world,
worldState,
input: {
userId: command.userId,
ownerDisplayName: command.ownerDisplayName,
seedOwnerIdentity: command.seedOwnerIdentity,
name: command.name,
leadership: command.leadership,
strength: command.strength,
intel: command.intel,
pic: command.pic,
character: command.character,
profileId: command.profileId,
...(command.ownerPicture !== undefined ? { ownerPicture: command.ownerPicture } : {}),
...(command.ownerImageServer !== undefined ? { ownerImageServer: command.ownerImageServer } : {}),
...(command.ownerCanUsePicture !== undefined
? { ownerCanUsePicture: command.ownerCanUsePicture }
: {}),
...(command.ownerLegacyPenalty !== undefined
? { ownerLegacyPenalty: command.ownerLegacyPenalty }
: {}),
...(command.inheritSpecial !== undefined ? { inheritSpecial: command.inheritSpecial } : {}),
...(command.inheritTurntimeZone !== undefined
? { inheritTurntimeZone: command.inheritTurntimeZone }
: {}),
...(command.inheritCity !== undefined ? { inheritCity: command.inheritCity } : {}),
...(command.inheritBonusStat !== undefined ? { inheritBonusStat: command.inheritBonusStat } : {}),
},
acceptedAt,
})),
};
} catch (error) {
if (error instanceof JoinCreateGeneralError) {
return {
type: 'joinCreateGeneral',
ok: false,
code: error.code,
reason: error.message,
};
}
throw error;
}
}
async function handleSelectPoolCreate(
ctx: CommandHandlerContext,
command: Extract<TurnDaemonCommand, { type: 'selectPoolCreate' }>
@@ -1952,16 +2017,12 @@ export const createTurnDaemonCommandHandler = (options: {
handleTournamentMatchResult(ctx, command as Extract<TurnDaemonCommand, { type: 'tournamentMatchResult' }>),
patchGeneral: (command) =>
handlePatchGeneral(ctx, command as Extract<TurnDaemonCommand, { type: 'patchGeneral' }>),
joinCreateGeneral: (command) =>
handleJoinCreateGeneral(ctx, command as Extract<TurnDaemonCommand, { type: 'joinCreateGeneral' }>),
selectPoolCreate: (command) =>
handleSelectPoolCreate(
ctx,
command as Extract<TurnDaemonCommand, { type: 'selectPoolCreate' }>
),
handleSelectPoolCreate(ctx, command as Extract<TurnDaemonCommand, { type: 'selectPoolCreate' }>),
selectPoolReselect: (command) =>
handleSelectPoolReselect(
ctx,
command as Extract<TurnDaemonCommand, { type: 'selectPoolReselect' }>
),
handleSelectPoolReselect(ctx, command as Extract<TurnDaemonCommand, { type: 'selectPoolReselect' }>),
shiftSchedule: (command) =>
handleShiftSchedule(ctx, command as Extract<TurnDaemonCommand, { type: 'shiftSchedule' }>),
};