Merge branch 'main' into feature/command-argument-ui

This commit is contained in:
2026-07-26 09:07:46 +00:00
24 changed files with 1830 additions and 59 deletions
+62
View File
@@ -15,6 +15,18 @@ const zGeneralSettings = z.object({
});
const zGeneralLogType = z.enum(['generalHistory', 'battleDetail', 'battleResult', 'generalAction']);
const MAIN_RECORD_LIMIT = 15;
const trimRecentRecords = <Entry extends { id: number }>(entries: Entry[], cursor: number): Entry[] => {
if (entries.length === 0) {
return entries;
}
const result = [...entries];
if (result.at(-1)?.id === cursor || result.length > MAIN_RECORD_LIMIT) {
result.pop();
}
return result;
};
const readNumber = (value: unknown, fallback: number): number => {
if (typeof value === 'number' && Number.isFinite(value)) {
@@ -319,4 +331,54 @@ export const generalRouter = router({
})),
};
}),
getRecentRecords: authedProcedure
.input(
z.object({
lastGeneralRecordId: z.number().int().nonnegative().default(0),
lastWorldHistoryId: z.number().int().nonnegative().default(0),
})
)
.query(async ({ ctx, input }) => {
const me = await getMyGeneral(ctx);
const take = MAIN_RECORD_LIMIT + 1;
const [global, general, history] = await Promise.all([
ctx.db.logEntry.findMany({
where: {
scope: LogScope.SYSTEM,
category: LogCategory.SUMMARY,
id: { gte: input.lastGeneralRecordId },
},
orderBy: { id: 'desc' },
take,
select: { id: true, text: true },
}),
ctx.db.logEntry.findMany({
where: {
scope: LogScope.GENERAL,
category: LogCategory.ACTION,
generalId: me.id,
id: { gte: input.lastGeneralRecordId },
},
orderBy: { id: 'desc' },
take,
select: { id: true, text: true },
}),
ctx.db.logEntry.findMany({
where: {
scope: LogScope.SYSTEM,
category: LogCategory.HISTORY,
id: { gte: input.lastWorldHistoryId },
},
orderBy: { id: 'desc' },
take,
select: { id: true, text: true },
}),
]);
return {
global: trimRecentRecords(global, input.lastGeneralRecordId),
general: trimRecentRecords(general, input.lastGeneralRecordId),
history: trimRecentRecords(history, input.lastWorldHistoryId),
};
}),
});
+34 -2
View File
@@ -1,5 +1,6 @@
import { TRPCError } from '@trpc/server';
import { asRecord } from '@sammo-ts/common';
import { LogCategory, LogScope } from '@sammo-ts/infra';
import { z } from 'zod';
import type { GameApiContext } from '../../context.js';
@@ -241,14 +242,45 @@ export const publicRouter = router({
return loadMapLayout(ctx.profile.scenario);
}),
getCachedMap: procedure.query(async ({ ctx }) => {
const map = await loadPublicMap(ctx, true);
const cacheKey = buildPublicCacheKey(ctx, 'cachedMapWithHistory');
const cached = await ctx.redis.get(cacheKey);
if (cached) {
try {
return JSON.parse(cached) as NonNullable<Awaited<ReturnType<typeof loadPublicMap>>> & {
history: { id: number; text: string }[];
};
} catch {
// Ignore cache parse errors.
}
}
const [map, history] = await Promise.all([
loadPublicMap(ctx, true),
ctx.db.logEntry.findMany({
where: {
scope: LogScope.SYSTEM,
category: LogCategory.HISTORY,
},
select: {
id: true,
text: true,
},
orderBy: { id: 'desc' },
take: 10,
}),
]);
if (!map) {
throw new TRPCError({
code: 'PRECONDITION_FAILED',
message: 'World state is not initialized.',
});
}
return map;
const snapshot = {
...map,
history,
};
await ctx.redis.set(cacheKey, JSON.stringify(snapshot), { EX: PUBLIC_CACHE_TTL_SECONDS });
return snapshot;
}),
getWorldTrend: procedure.query(async ({ ctx }) => {
return loadCachedWorldTrend(ctx);