Revert "Exclude concurrent public traffic work"

This reverts commit 7fc0850f83.
This commit is contained in:
2026-07-26 05:23:25 +00:00
parent 7fc0850f83
commit fa87f5565c
6 changed files with 583 additions and 0 deletions
+117
View File
@@ -42,6 +42,14 @@ type NationCountRow = {
type NpcListSort = 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8;
type TrafficHistoryItem = {
year: number;
month: number;
refresh: number;
online: number;
date: string;
};
const PUBLIC_CACHE_TTL_SECONDS = 600;
const buildPublicCacheKey = (ctx: GameApiContext, key: string): string =>
@@ -163,6 +171,26 @@ const readFiniteMetaNumber = (meta: Record<string, unknown>, key: string): numbe
return typeof value === 'number' && Number.isFinite(value) ? value : 0;
};
const parseTrafficHistory = (value: unknown): TrafficHistoryItem[] => {
if (!Array.isArray(value)) {
return [];
}
const result: TrafficHistoryItem[] = [];
for (const item of value) {
const row = asRecord(item);
const year = readFiniteMetaNumber(row, 'year');
const month = readFiniteMetaNumber(row, 'month');
const refresh = readFiniteMetaNumber(row, 'refresh');
const online = readFiniteMetaNumber(row, 'online');
const date = typeof row.date === 'string' ? row.date : '';
if (year > 0 && month > 0 && date) {
result.push({ year, month, refresh, online, date });
}
}
return result;
};
const compareString = (left: string, right: string): number => {
if (left === right) {
return 0;
@@ -222,6 +250,95 @@ export const publicRouter = router({
getNationList: procedure.query(async ({ ctx }) => {
return loadCachedNationList(ctx);
}),
getTraffic: procedure.query(async ({ ctx }) => {
const worldState = await ctx.db.worldState.findFirst();
if (!worldState) {
throw new TRPCError({
code: 'PRECONDITION_FAILED',
message: 'World state is not initialized.',
});
}
const meta = asRecord(worldState.meta);
const rawOnlineSince = meta.lastTurnTime ?? meta.turntime;
const parsedOnlineSince =
typeof rawOnlineSince === 'string' || rawOnlineSince instanceof Date
? new Date(rawOnlineSince)
: null;
const onlineSince =
parsedOnlineSince && Number.isFinite(parsedOnlineSince.getTime())
? parsedOnlineSince
: new Date(Date.now() - worldState.tickSeconds * 1_000);
const [accessTotal, currentOnline, topAccess] = await Promise.all([
ctx.db.generalAccessLog.aggregate({
_sum: {
refresh: true,
refreshScoreTotal: true,
},
}),
ctx.db.generalAccessLog.count({
where: {
lastRefresh: {
gte: onlineSince,
},
},
}),
ctx.db.generalAccessLog.findMany({
orderBy: [{ refresh: 'desc' }, { generalId: 'asc' }],
take: 5,
select: {
generalId: true,
refresh: true,
refreshScoreTotal: true,
},
}),
]);
const generalIds = topAccess.map((entry) => entry.generalId);
const generalRows =
generalIds.length > 0
? await ctx.db.general.findMany({
where: { id: { in: generalIds } },
select: { id: true, name: true },
})
: [];
const generalName = new Map(generalRows.map((general) => [general.id, general.name]));
const totalRefresh = accessTotal._sum.refresh ?? 0;
const totalRefreshScore = accessTotal._sum.refreshScoreTotal ?? 0;
const currentRefresh = Math.max(readFiniteMetaNumber(meta, 'refresh'), totalRefresh);
const history = parseTrafficHistory(meta.recentTraffic);
history.push({
year: worldState.currentYear,
month: worldState.currentMonth,
refresh: currentRefresh,
online: currentOnline,
date: new Date().toISOString(),
});
return {
history,
maxRefresh: Math.max(
1,
readFiniteMetaNumber(meta, 'maxrefresh'),
...history.map((entry) => entry.refresh)
),
maxOnline: Math.max(1, readFiniteMetaNumber(meta, 'maxonline'), ...history.map((entry) => entry.online)),
suspects: [
{
generalId: null,
name: '접속자 총합',
refresh: totalRefresh,
refreshScoreTotal: totalRefreshScore,
},
...topAccess.map((entry) => ({
generalId: entry.generalId,
name: generalName.get(entry.generalId) ?? `장수 ${entry.generalId}`,
refresh: entry.refresh,
refreshScoreTotal: entry.refreshScoreTotal,
})),
],
};
}),
getGeneralList: procedure.query(async ({ ctx }) => {
const [generals, nations] = await Promise.all([
ctx.db.general.findMany({
+115
View File
@@ -0,0 +1,115 @@
import { describe, expect, it } from 'vitest';
import type { RedisConnector } from '@sammo-ts/infra';
import { RedisAccessTokenStore } from '../src/auth/accessTokenStore.js';
import { InMemoryFlushStore } from '../src/auth/flushStore.js';
import { InMemoryBattleSimTransport } from '../src/battleSim/inMemoryTransport.js';
import type { DatabaseClient, GameApiContext, GameProfile } from '../src/context.js';
import { InMemoryTurnDaemonTransport } from '../src/daemon/inMemoryTransport.js';
import { appRouter } from '../src/router.js';
const profile: GameProfile = {
id: 'che',
scenario: 'default',
name: 'che:default',
};
const buildContext = (): GameApiContext => {
const db = {
worldState: {
findFirst: async () => ({
id: 1,
currentYear: 185,
currentMonth: 3,
tickSeconds: 600,
config: {},
meta: {
lastTurnTime: '2026-07-26T03:00:00.000Z',
refresh: 12,
maxrefresh: 30,
maxonline: 5,
recentTraffic: [
{
year: 185,
month: 2,
refresh: 30,
online: 5,
date: '2026-07-26 02:50:00',
},
],
},
}),
},
generalAccessLog: {
aggregate: async () => ({
_sum: {
refresh: 12,
refreshScoreTotal: 21,
},
}),
count: async (args: { where: { lastRefresh: { gte: Date } } }) => {
expect(args.where.lastRefresh.gte).toEqual(new Date('2026-07-26T03:00:00.000Z'));
return 2;
},
findMany: async () => [
{ generalId: 7, refresh: 9, refreshScoreTotal: 15 },
{ generalId: 8, refresh: 3, refreshScoreTotal: 6 },
],
},
general: {
findMany: async () => [
{ id: 7, name: '갑' },
{ id: 8, name: '을' },
],
},
};
const redis = {
get: async () => null,
set: async () => null,
} as unknown as RedisConnector['client'];
return {
db: db as unknown as DatabaseClient,
turnDaemon: new InMemoryTurnDaemonTransport(),
battleSim: new InMemoryBattleSimTransport(),
profile,
auth: null,
uploadDir: 'uploads',
uploadPath: '/uploads',
uploadPublicUrl: null,
redis,
accessTokenStore: new RedisAccessTokenStore(redis, profile.name),
flushStore: new InMemoryFlushStore(),
gameTokenSecret: 'test-secret',
};
};
describe('public.getTraffic', () => {
it('is public and returns only aggregate traffic plus allowlisted general names', async () => {
const result = await appRouter.createCaller(buildContext()).public.getTraffic();
expect(result.history).toHaveLength(2);
expect(result.history[0]).toEqual({
year: 185,
month: 2,
refresh: 30,
online: 5,
date: '2026-07-26 02:50:00',
});
expect(result.history[1]).toMatchObject({
year: 185,
month: 3,
refresh: 12,
online: 2,
});
expect(result.maxRefresh).toBe(30);
expect(result.maxOnline).toBe(5);
expect(result.suspects).toEqual([
{ generalId: null, name: '접속자 총합', refresh: 12, refreshScoreTotal: 21 },
{ generalId: 7, name: '갑', refresh: 9, refreshScoreTotal: 15 },
{ generalId: 8, name: '을', refresh: 3, refreshScoreTotal: 6 },
]);
expect(JSON.stringify(result)).not.toContain('userId');
});
});