perf: optimize DB connections, queries, and indexes
All checks were successful
CI / update (push) Successful in 3m28s

Fix dev-mode reconnect storm by persisting mongoose connection state on
globalThis instead of a module-level flag that resets on Vite HMR.

Eliminate redundant in_season DB query on /rezepte — derive seasonal
subset from all_brief client-side. Parallelize all page load fetches.

Replace N+1 settlement queries in balance route with single batch $in
query. Parallelize balance sum and recent splits aggregations.

Trim unused dateModified/dateCreated from recipe brief projections.

Add indexes: Payment(date, createdAt), PaymentSplit(username),
Recipe(short_name), Recipe(season).
This commit is contained in:
2026-04-06 12:42:45 +02:00
parent b2e271c3ea
commit 09cd410eaa
7 changed files with 74 additions and 71 deletions

View File

@@ -20,8 +20,8 @@ export function briefQueryConfig(recipeLang: string) {
prefix: en ? 'translations.en.' : '',
/** Projection for brief list queries */
projection: en
? '_id translations.en short_name images season dateModified icon'
: 'name short_name images tags category icon description season dateModified',
? '_id translations.en short_name images season icon'
: 'name short_name images tags category icon description season',
};
}
@@ -45,8 +45,6 @@ export function toBrief(recipe: RecipeModelType, recipeLang: string): BriefRecip
icon: recipe.icon,
description: en?.description,
season: recipe.season || [],
dateCreated: recipe.dateCreated,
dateModified: recipe.dateModified,
germanShortName: recipe.short_name,
} as unknown as BriefRecipeType;
}

View File

@@ -89,6 +89,8 @@ const PaymentSchema = new mongoose.Schema(
}
);
PaymentSchema.index({ date: -1, createdAt: -1 });
PaymentSchema.virtual('splits', {
ref: 'PaymentSplit',
localField: '_id',

View File

@@ -52,5 +52,6 @@ const PaymentSplitSchema = new mongoose.Schema(
);
PaymentSplitSchema.index({ paymentId: 1, username: 1 }, { unique: true });
PaymentSplitSchema.index({ username: 1 });
export const PaymentSplit = mongoose.model<IPaymentSplit>("PaymentSplit", PaymentSplitSchema);

View File

@@ -193,6 +193,8 @@ const RecipeSchema = new mongoose.Schema(
);
// Indexes for efficient querying
RecipeSchema.index({ short_name: 1 });
RecipeSchema.index({ season: 1 });
RecipeSchema.index({ "translations.en.short_name": 1 });
RecipeSchema.index({ "translations.en.translationStatus": 1 });

View File

@@ -3,22 +3,22 @@ import { getUserFavorites, addFavoriteStatusToRecipes } from "$lib/server/favori
export const load: PageServerLoad = async ({ fetch, locals, params }) => {
const apiBase = `/api/${params.recipeLang}`;
const currentMonth = new Date().getMonth() + 1;
let current_month = new Date().getMonth() + 1
const res_season = await fetch(`${apiBase}/items/in_season/` + current_month);
const res_all_brief = await fetch(`${apiBase}/items/all_brief`);
const item_season = await res_season.json();
const item_all_brief = await res_all_brief.json();
// Get user favorites and session
const [userFavorites, session] = await Promise.all([
// Fetch all_brief, favorites, and session in parallel
const [res_all_brief, userFavorites, session] = await Promise.all([
fetch(`${apiBase}/items/all_brief`).then(r => r.json()),
getUserFavorites(fetch, locals),
locals.auth()
]);
const all_brief = addFavoriteStatusToRecipes(res_all_brief, userFavorites);
// Derive seasonal subset from all_brief instead of a separate DB query
const season = all_brief.filter((r: any) => r.season?.includes(currentMonth) && r.icon !== '🍽️');
return {
season: addFavoriteStatusToRecipes(item_season, userFavorites),
all_brief: addFavoriteStatusToRecipes(item_all_brief, userFavorites),
season,
all_brief,
session,
heroIndex: Math.random()
};

View File

@@ -52,37 +52,48 @@ export const GET: RequestHandler = async ({ locals, url }) => {
return json(result);
} else {
const userSplits = await PaymentSplit.find({ username }).lean();
// Calculate net balance: negative = you are owed money, positive = you owe money
const netBalance = userSplits.reduce((sum, split) => sum + split.amount, 0);
const recentSplits = await PaymentSplit.aggregate([
{ $match: { username } },
{
$lookup: {
from: 'payments',
localField: 'paymentId',
foreignField: '_id',
as: 'paymentId'
}
},
{ $unwind: '$paymentId' },
{ $sort: { 'paymentId.date': -1, 'paymentId.createdAt': -1 } },
{ $limit: 30 }
// Run balance sum and recent splits in parallel
const [balanceResult, recentSplits] = await Promise.all([
PaymentSplit.aggregate([
{ $match: { username } },
{ $group: { _id: null, total: { $sum: '$amount' } } }
]),
PaymentSplit.aggregate([
{ $match: { username } },
{
$lookup: {
from: 'payments',
localField: 'paymentId',
foreignField: '_id',
as: 'paymentId'
}
},
{ $unwind: '$paymentId' },
{ $sort: { 'paymentId.date': -1, 'paymentId.createdAt': -1 } },
{ $limit: 30 }
])
]);
// For settlements, fetch the other user's split info
for (const split of recentSplits) {
if (split.paymentId && split.paymentId.category === 'settlement') {
// This is a settlement, find the other user
const otherSplit = await PaymentSplit.findOne({
paymentId: split.paymentId._id,
username: { $ne: username }
}).lean();
const netBalance = balanceResult[0]?.total ?? 0;
if (otherSplit) {
split.otherUser = otherSplit.username;
// Batch-fetch other users for settlements (avoids N+1 queries)
const settlementIds = recentSplits
.filter(s => s.paymentId?.category === 'settlement')
.map(s => s.paymentId._id);
if (settlementIds.length > 0) {
const otherSplits = await PaymentSplit.find({
paymentId: { $in: settlementIds } as any,
username: { $ne: username }
}).lean();
const otherUserByPayment = new Map(
otherSplits.map(s => [s.paymentId.toString(), s.username])
);
for (const split of recentSplits) {
if (split.paymentId?.category === 'settlement') {
split.otherUser = otherUserByPayment.get(split.paymentId._id.toString());
}
}
}

View File

@@ -1,47 +1,36 @@
import mongoose from 'mongoose';
import { MONGO_URL } from '$env/static/private';
let isConnected = false;
// Use globalThis to persist connection promise across Vite HMR module reloads
const g = globalThis as unknown as { __mongoosePromise?: Promise<typeof mongoose> };
export const dbConnect = async () => {
// If already connected, return immediately
if (isConnected && mongoose.connection.readyState === 1) {
// Already connected return immediately
if (mongoose.connection.readyState === 1) {
return mongoose.connection;
}
// Connection in progress — await the existing promise
if (mongoose.connection.readyState === 2 && g.__mongoosePromise) {
await g.__mongoosePromise;
return mongoose.connection;
}
try {
// Configure MongoDB driver options
const options = {
maxPoolSize: 10, // Maintain up to 10 socket connections
serverSelectionTimeoutMS: 5000, // Keep trying to send operations for 5 seconds
socketTimeoutMS: 45000, // Close sockets after 45 seconds of inactivity
maxPoolSize: 10,
serverSelectionTimeoutMS: 5000,
socketTimeoutMS: 45000,
};
const connection = await mongoose.connect(MONGO_URL ?? '', options);
isConnected = true;
g.__mongoosePromise = mongoose.connect(MONGO_URL ?? '', options);
await g.__mongoosePromise;
console.log('MongoDB connected with persistent connection');
// Handle connection events
mongoose.connection.on('error', (err) => {
console.error('MongoDB connection error:', err);
isConnected = false;
});
mongoose.connection.on('disconnected', () => {
console.log('MongoDB disconnected');
isConnected = false;
});
mongoose.connection.on('reconnected', () => {
console.log('MongoDB reconnected');
isConnected = true;
});
return connection;
return mongoose.connection;
} catch (error) {
console.error('MongoDB connection failed:', error);
isConnected = false;
g.__mongoosePromise = undefined;
throw error;
}
};