fix reserved turn queue concurrency

This commit is contained in:
2026-07-26 18:32:52 +00:00
parent ab6ed3553a
commit 3cd1ad3b7f
18 changed files with 613 additions and 106 deletions
@@ -3,7 +3,7 @@ import { TRPCError } from '@trpc/server';
import { authedProcedure } from '../../../trpc.js';
import { getMyGeneral } from '../../shared/general.js';
import { resolveSecretPermission } from '../../shared/secretPermission.js';
import { MAX_NATION_TURNS, listNationTurns } from '../../../turns/reservedTurns.js';
import { MAX_NATION_TURNS, getNationTurnSnapshot } from '../../../turns/reservedTurns.js';
import { assertNationAccess } from '../shared.js';
export const getChiefCenter = authedProcedure.query(async ({ ctx }) => {
@@ -56,9 +56,7 @@ export const getChiefCenter = authedProcedure.query(async ({ ctx }) => {
const chiefLevels = [12, 10, 8, 6, 11, 9, 7, 5];
const generalByLevel = new Map(nationGenerals.map((general) => [general.officerLevel, general]));
const turnsByLevel = await Promise.all(
chiefLevels.map((level) => listNationTurns(ctx.db, nation.id, level))
);
const turnsByLevel = await Promise.all(chiefLevels.map((level) => getNationTurnSnapshot(ctx.db, nation.id, level)));
const chiefs = chiefLevels.map((level, idx) => {
const entry = generalByLevel.get(level);
@@ -67,7 +65,8 @@ export const getChiefCenter = authedProcedure.query(async ({ ctx }) => {
name: entry?.name ?? null,
npcState: entry?.npcState ?? null,
turnTime: entry?.turnTime ? entry.turnTime.toISOString() : null,
turns: turnsByLevel[idx],
revision: turnsByLevel[idx]?.revision ?? 0,
turns: turnsByLevel[idx]?.turns ?? [],
};
});
+52 -23
View File
@@ -14,8 +14,9 @@ import {
import {
MAX_GENERAL_TURNS,
MAX_NATION_TURNS,
listGeneralTurns,
listNationTurns,
ReservedTurnRevisionConflictError,
getGeneralTurnSnapshot,
getNationTurnSnapshot,
setGeneralTurn,
setNationTurn,
shiftGeneralTurns,
@@ -45,6 +46,21 @@ const parseCommandArgs = async (scope: 'general' | 'nation', action: string, arg
}
};
const mutateReservedTurns = async <T>(mutation: () => Promise<T>): Promise<T> => {
try {
return await mutation();
} catch (error) {
if (error instanceof ReservedTurnRevisionConflictError) {
throw new TRPCError({
code: 'CONFLICT',
message: 'Reserved turn queue changed. Reload and retry.',
cause: error,
});
}
throw error;
}
};
export const turnsRouter = router({
getCommandTable: authedProcedure
.input(
@@ -164,7 +180,7 @@ export const turnsRouter = router({
.query(async ({ ctx, input }) => {
await getOwnedGeneral(ctx, input.generalId);
return listGeneralTurns(ctx.db, input.generalId);
return getGeneralTurnSnapshot(ctx.db, input.generalId);
}),
getNation: authedProcedure
.input(
@@ -187,7 +203,7 @@ export const turnsRouter = router({
});
}
return listNationTurns(ctx.db, general.nationId, general.officerLevel);
return getNationTurnSnapshot(ctx.db, general.nationId, general.officerLevel);
}),
setGeneral: authedProcedure
.input(
@@ -200,33 +216,33 @@ export const turnsRouter = router({
.max(MAX_GENERAL_TURNS - 1),
action: z.string().min(1),
args: z.unknown().optional(),
expectedRevision: z.number().int().nonnegative(),
})
)
.mutation(async ({ ctx, input }) => {
await getOwnedGeneral(ctx, input.generalId);
const args = await parseCommandArgs('general', input.action, input.args);
const turns = await setGeneralTurn(
ctx.db,
input.generalId,
input.turnIndex,
input.action,
args
const snapshot = await mutateReservedTurns(() =>
setGeneralTurn(ctx.db, input.generalId, input.turnIndex, input.action, args, input.expectedRevision)
);
return { ok: true, turns };
return { ok: true, ...snapshot };
}),
shiftGeneral: authedProcedure
.input(
z.object({
generalId: z.number().int().positive(),
amount: buildShiftAmountSchema(MAX_GENERAL_TURNS),
expectedRevision: z.number().int().nonnegative(),
})
)
.mutation(async ({ ctx, input }) => {
await getOwnedGeneral(ctx, input.generalId);
const turns = await shiftGeneralTurns(ctx.db, input.generalId, input.amount);
return { ok: true, turns };
const snapshot = await mutateReservedTurns(() =>
shiftGeneralTurns(ctx.db, input.generalId, input.amount, input.expectedRevision)
);
return { ok: true, ...snapshot };
}),
setNation: authedProcedure
.input(
@@ -239,6 +255,7 @@ export const turnsRouter = router({
.max(MAX_NATION_TURNS - 1),
action: z.string().min(1),
args: z.unknown().optional(),
expectedRevision: z.number().int().nonnegative(),
})
)
.mutation(async ({ ctx, input }) => {
@@ -257,21 +274,25 @@ export const turnsRouter = router({
}
const args = await parseCommandArgs('nation', input.action, input.args);
const turns = await setNationTurn(
ctx.db,
general.nationId,
general.officerLevel,
input.turnIndex,
input.action,
args
const snapshot = await mutateReservedTurns(() =>
setNationTurn(
ctx.db,
general.nationId,
general.officerLevel,
input.turnIndex,
input.action,
args,
input.expectedRevision
)
);
return { ok: true, turns };
return { ok: true, ...snapshot };
}),
shiftNation: authedProcedure
.input(
z.object({
generalId: z.number().int().positive(),
amount: buildShiftAmountSchema(MAX_NATION_TURNS),
expectedRevision: z.number().int().nonnegative(),
})
)
.mutation(async ({ ctx, input }) => {
@@ -289,8 +310,16 @@ export const turnsRouter = router({
});
}
const turns = await shiftNationTurns(ctx.db, general.nationId, general.officerLevel, input.amount);
return { ok: true, turns };
const snapshot = await mutateReservedTurns(() =>
shiftNationTurns(
ctx.db,
general.nationId,
general.officerLevel,
input.amount,
input.expectedRevision
)
);
return { ok: true, ...snapshot };
}),
}),
});