Add performance optimizations and component refactors
CI / check (push) Successful in 34s
CI / release (push) Skipped
CI / deploy (push) Successful in 1m23s

- 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:
openhands committed 2026-08-17 22:02:21 +02:00
1 parent 54f2bc5e0c
commit f285a7cd98
42 files changed
+1616 -1205

No files matched your search

+90 -85
View File
@@ -191,19 +191,69 @@ export default async function ProfilePage({
// Currency ledger rows (duckets/diamonds). Credits live on the users row
// itself; the alt-currencies sit in the users_currency ledger keyed by type.
const currencyRows = await db
.select({
type: UsersCurrency.type,
amount: UsersCurrency.amount,
})
.from(UsersCurrency)
.where(
and(
eq(UsersCurrency.userId, user.id),
inArray(UsersCurrency.type, [CURRENCY_DUCKETS, CURRENCY_DIAMONDS]),
),
)
.catch(() => []);
// Currency ledger rows (duckets/diamonds). Credits live on the users row
// itself; the alt-currencies sit in the users_currency ledger keyed by type.
// All sub-sections run in parallel (each query keeps its own .catch so a DB
// hiccup still degrades only that section instead of 500-ing the profile).
const [currencyRows, friendships, rooms, badges, photos, guestbook] =
await Promise.all([
db
.select({
type: UsersCurrency.type,
amount: UsersCurrency.amount,
})
.from(UsersCurrency)
.where(
and(
eq(UsersCurrency.userId, user.id),
inArray(UsersCurrency.type, [CURRENCY_DUCKETS, CURRENCY_DIAMONDS]),
),
)
.catch(() => []),
// Friends. messenger_friendships stores one directed row per pair
// keyed on user_one_id; resolve the user_two_id side below.
db
.select({ userTwoId: MessengerFriendships.userTwoId })
.from(MessengerFriendships)
.where(eq(MessengerFriendships.userOneId, user.id))
.orderBy(desc(MessengerFriendships.friendsSince))
.limit(18)
.catch(() => []),
// Rooms owned by this user.
db
.select({
id: Rooms.id,
name: Rooms.name,
users: Rooms.users,
usersMax: Rooms.usersMax,
state: Rooms.state,
})
.from(Rooms)
.where(eq(Rooms.ownerId, user.id))
.orderBy(desc(Rooms.users))
.limit(8)
.catch(() => []),
db
.select()
.from(UsersBadges)
.where(eq(UsersBadges.userId, user.id))
.orderBy(asc(UsersBadges.slotId))
.catch(() => []),
db
.select()
.from(CameraWeb)
.where(eq(CameraWeb.userId, user.id))
.orderBy(desc(CameraWeb.timestamp))
.limit(6)
.catch(() => []),
db
.select()
.from(WebsiteUserGuestbooks)
.where(eq(WebsiteUserGuestbooks.profileId, user.id))
.orderBy(desc(WebsiteUserGuestbooks.createdAt))
.limit(30)
.catch(() => []),
]);
const ducketsAmount =
currencyRows.find((c) => c.type === CURRENCY_DUCKETS)?.amount ?? 0;
const diamondsAmount =
@@ -232,83 +282,38 @@ export default async function ProfilePage({
},
];
// Friends. messenger_friendships stores one directed row per pair keyed on
// user_one_id; resolve the user_two_id side to usernames/looks for the grid.
const friendships = await db
.select({ userTwoId: MessengerFriendships.userTwoId })
.from(MessengerFriendships)
.where(eq(MessengerFriendships.userOneId, user.id))
.orderBy(desc(MessengerFriendships.friendsSince))
.limit(18)
.catch(() => []);
// Resolve friend usernames/looks and the guestbook author usernames/looks in
// two parallel queries (the ids come from the queries above).
const friendIds = Array.from(new Set(friendships.map((f) => f.userTwoId)));
const friendUsers = friendIds.length
? await db
.select({
id: User.id,
username: User.username,
look: User.look,
})
.from(User)
.where(inArray(User.id, friendIds))
.catch(() => [])
: [];
const authorIds = Array.from(new Set(guestbook.map((g) => g.userId)));
const [friendUsers, authors] = await Promise.all([
friendIds.length
? db
.select({
id: User.id,
username: User.username,
look: User.look,
})
.from(User)
.where(inArray(User.id, friendIds))
.catch(() => [])
: [],
authorIds.length
? db
.select({
id: User.id,
username: User.username,
look: User.look,
})
.from(User)
.where(inArray(User.id, authorIds))
.catch(() => [])
: [],
]);
const friendById = new Map(friendUsers.map((f) => [f.id, f]));
const friends = friendships
.map((f) => friendById.get(f.userTwoId))
.filter((f): f is NonNullable<typeof f> => Boolean(f));
// Rooms owned by this user.
const rooms = await db
.select({
id: Rooms.id,
name: Rooms.name,
users: Rooms.users,
usersMax: Rooms.usersMax,
state: Rooms.state,
})
.from(Rooms)
.where(eq(Rooms.ownerId, user.id))
.orderBy(desc(Rooms.users))
.limit(8)
.catch(() => []);
const badges = await db
.select()
.from(UsersBadges)
.where(eq(UsersBadges.userId, user.id))
.orderBy(asc(UsersBadges.slotId))
.catch(() => []);
const photos = await db
.select()
.from(CameraWeb)
.where(eq(CameraWeb.userId, user.id))
.orderBy(desc(CameraWeb.timestamp))
.limit(6)
.catch(() => []);
const guestbook = await db
.select()
.from(WebsiteUserGuestbooks)
.where(eq(WebsiteUserGuestbooks.profileId, user.id))
.orderBy(desc(WebsiteUserGuestbooks.createdAt))
.limit(30)
.catch(() => []);
// Resolve author usernames/looks for the guestbook entries in one query.
const authorIds = Array.from(new Set(guestbook.map((g) => g.userId)));
const authors = authorIds.length
? await db
.select({
id: User.id,
username: User.username,
look: User.look,
})
.from(User)
.where(inArray(User.id, authorIds))
.catch(() => [])
: [];
const authorById = new Map(authors.map((a) => [a.id, a]));
return (