Add performance optimizations and component refactors
- Cache read-heavy public API routes via redisCache (leaderboard, values, shop, articles, photos, guilds, teams, staff, users, home, radio, badges) - Add single-flight and bounded-memory cache layer with unit tests - Parallelize independent DB queries on search, rares, shop, staff, polls and profile pages - Push radio points leaderboard aggregation to SQL with a LIMIT - Split studio-client and import-furni-client into focused modules - Clean up next.config.ts
This commit is contained in:
1 parent
54f2bc5e0c
commit
f285a7cd98
42 files changed
+1616
-1205
No files matched your search
@@ -5,6 +5,7 @@
|
||||
import { count, eq } from "drizzle-orm";
|
||||
import { apiJson } from "@/lib/api";
|
||||
import { db, Guilds, GuildsMembers } from "@/lib/db";
|
||||
import { apiCacheKey, cacheSafe, redisCache } from "@/lib/redis-cache";
|
||||
|
||||
export async function GET(
|
||||
_req: Request,
|
||||
@@ -18,38 +19,50 @@ export async function GET(
|
||||
const guildId = Number(id);
|
||||
|
||||
try {
|
||||
const [guild] = await db
|
||||
.select({
|
||||
id: Guilds.id,
|
||||
name: Guilds.name,
|
||||
description: Guilds.description,
|
||||
userId: Guilds.userId,
|
||||
roomId: Guilds.roomId,
|
||||
badge: Guilds.badge,
|
||||
dateCreated: Guilds.dateCreated,
|
||||
})
|
||||
.from(Guilds)
|
||||
.where(eq(Guilds.id, guildId))
|
||||
.limit(1);
|
||||
const data = await redisCache(
|
||||
apiCacheKey(`guilds:${id}`),
|
||||
120,
|
||||
async () => {
|
||||
const [guild] = await db
|
||||
.select({
|
||||
id: Guilds.id,
|
||||
name: Guilds.name,
|
||||
description: Guilds.description,
|
||||
userId: Guilds.userId,
|
||||
roomId: Guilds.roomId,
|
||||
badge: Guilds.badge,
|
||||
dateCreated: Guilds.dateCreated,
|
||||
})
|
||||
.from(Guilds)
|
||||
.where(eq(Guilds.id, guildId))
|
||||
.limit(1);
|
||||
|
||||
if (!guild) {
|
||||
if (!guild) {
|
||||
return null;
|
||||
}
|
||||
|
||||
// Member count is a separate guarded query (no relation is modelled).
|
||||
let memberCount = 0;
|
||||
try {
|
||||
const [row] = await db
|
||||
.select({ total: count() })
|
||||
.from(GuildsMembers)
|
||||
.where(eq(GuildsMembers.guildId, guild.id));
|
||||
memberCount = row?.total ?? 0;
|
||||
} catch {
|
||||
memberCount = 0;
|
||||
}
|
||||
|
||||
const { userId, ...rest } = guild;
|
||||
return cacheSafe({ data: { ...rest, ownerId: userId, memberCount } });
|
||||
},
|
||||
);
|
||||
|
||||
if (data === null) {
|
||||
return apiJson({ error: "Guild not found" }, { status: 404 });
|
||||
}
|
||||
|
||||
// Member count is a separate guarded query (no relation is modelled).
|
||||
let memberCount = 0;
|
||||
try {
|
||||
const [row] = await db
|
||||
.select({ total: count() })
|
||||
.from(GuildsMembers)
|
||||
.where(eq(GuildsMembers.guildId, guild.id));
|
||||
memberCount = row?.total ?? 0;
|
||||
} catch {
|
||||
memberCount = 0;
|
||||
}
|
||||
|
||||
const { userId, ...rest } = guild;
|
||||
return apiJson({ data: { ...rest, ownerId: userId, memberCount } });
|
||||
return apiJson(data);
|
||||
} catch {
|
||||
// DB unavailable — treat as not found rather than a 500.
|
||||
return apiJson({ error: "Guild not found" }, { status: 200 });
|
||||
|
||||
+39
-28
@@ -5,43 +5,54 @@
|
||||
import { count, desc } from "drizzle-orm";
|
||||
import { apiJson, pagination } from "@/lib/api";
|
||||
import { db, Guilds } from "@/lib/db";
|
||||
import { apiCacheKey, cacheSafe, redisCache } from "@/lib/redis-cache";
|
||||
|
||||
export async function GET(req: Request) {
|
||||
const sp = new URL(req.url).searchParams;
|
||||
const { page, perPage, skip, take } = pagination(sp);
|
||||
|
||||
try {
|
||||
const [totalRows, rows] = await Promise.all([
|
||||
db.select({ total: count() }).from(Guilds),
|
||||
db
|
||||
.select({
|
||||
id: Guilds.id,
|
||||
name: Guilds.name,
|
||||
description: Guilds.description,
|
||||
userId: Guilds.userId,
|
||||
})
|
||||
.from(Guilds)
|
||||
.orderBy(desc(Guilds.id))
|
||||
.limit(take)
|
||||
.offset(skip),
|
||||
]);
|
||||
const total = totalRows[0]?.total ?? 0;
|
||||
const data = await redisCache(
|
||||
apiCacheKey(`guilds:${page}:${perPage}`),
|
||||
120,
|
||||
async () => {
|
||||
const [totalRows, rows] = await Promise.all([
|
||||
db.select({ total: count() }).from(Guilds),
|
||||
db
|
||||
.select({
|
||||
id: Guilds.id,
|
||||
name: Guilds.name,
|
||||
description: Guilds.description,
|
||||
userId: Guilds.userId,
|
||||
})
|
||||
.from(Guilds)
|
||||
.orderBy(desc(Guilds.id))
|
||||
.limit(take)
|
||||
.offset(skip),
|
||||
]);
|
||||
|
||||
// Rename userId → ownerId for the public payload.
|
||||
const data = rows.map(({ userId, ...rest }) => ({
|
||||
...rest,
|
||||
ownerId: userId,
|
||||
}));
|
||||
// Rename userId → ownerId for the public payload.
|
||||
const renamed = rows.map(({ userId, ...rest }) => ({
|
||||
...rest,
|
||||
ownerId: userId,
|
||||
}));
|
||||
|
||||
return apiJson({
|
||||
data,
|
||||
meta: {
|
||||
page,
|
||||
perPage,
|
||||
total,
|
||||
lastPage: Math.max(1, Math.ceil(total / perPage)),
|
||||
return cacheSafe({
|
||||
data: renamed,
|
||||
meta: {
|
||||
page,
|
||||
perPage,
|
||||
total: totalRows[0]?.total ?? 0,
|
||||
lastPage: Math.max(
|
||||
1,
|
||||
Math.ceil((totalRows[0]?.total ?? 0) / perPage),
|
||||
),
|
||||
},
|
||||
});
|
||||
},
|
||||
});
|
||||
);
|
||||
|
||||
return apiJson(data);
|
||||
} catch {
|
||||
// DB unreachable — never 500; return an empty, well-formed payload.
|
||||
return apiJson(
|
||||
|
||||
Reference in new issue
Block a user