perf: optimize DB connections, queries, and indexes
All checks were successful
CI / update (push) Successful in 3m28s
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:
@@ -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;
|
||||
}
|
||||
|
||||
@@ -89,6 +89,8 @@ const PaymentSchema = new mongoose.Schema(
|
||||
}
|
||||
);
|
||||
|
||||
PaymentSchema.index({ date: -1, createdAt: -1 });
|
||||
|
||||
PaymentSchema.virtual('splits', {
|
||||
ref: 'PaymentSplit',
|
||||
localField: '_id',
|
||||
|
||||
@@ -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);
|
||||
@@ -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 });
|
||||
|
||||
|
||||
@@ -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()
|
||||
};
|
||||
|
||||
@@ -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());
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
};
|
||||
|
||||
Reference in New Issue
Block a user