fix: implement persistent MongoDB connections and resolve race conditions

- Replace connect/disconnect pattern with persistent connection pool
- Add explicit database initialization on server startup
- Remove all dbDisconnect() calls from API endpoints to prevent race conditions
- Fix MongoNotConnectedError when scheduler runs concurrently with API requests
- Add connection pooling with proper MongoDB driver options
- Add safety check for recipes array in favorites utility

🤖 Generated with [Claude Code](https://claude.ai/code)

Co-Authored-By: Claude <noreply@anthropic.com>
This commit is contained in:
2025-09-14 19:53:55 +02:00
parent 08d7d8541b
commit c8e542eec8
28 changed files with 198 additions and 111 deletions

View File

@@ -1,6 +1,6 @@
import type { RequestHandler } from '@sveltejs/kit';
import { Recipe } from '../../../../models/Recipe';
import { dbConnect, dbDisconnect } from '../../../../utils/db';
import { dbConnect } from '../../../../utils/db';
import { error } from '@sveltejs/kit';
// header: use for bearer token for now
// recipe json in body
@@ -22,7 +22,6 @@ export const POST: RequestHandler = async ({request, cookies, locals}) => {
} catch(e){
throw error(400, e)
}
await dbDisconnect();
return new Response(JSON.stringify({msg: "Added recipe successfully"}),{
status: 200,
});

View File

@@ -1,6 +1,6 @@
import type { RequestHandler } from '@sveltejs/kit';
import { Recipe } from '../../../../models/Recipe';
import { dbConnect, dbDisconnect } from '../../../../utils/db';
import { dbConnect } from '../../../../utils/db';
import type {RecipeModelType} from '../../../../types/types';
import { error } from '@sveltejs/kit';
// header: use for bearer token for now
@@ -14,7 +14,6 @@ export const POST: RequestHandler = async ({request, locals}) => {
const short_name = message.old_short_name
await dbConnect();
await Recipe.findOneAndDelete({short_name: short_name});
await dbDisconnect();
return new Response(JSON.stringify({msg: "Deleted recipe successfully"}),{
status: 200,
});

View File

@@ -1,6 +1,6 @@
import type { RequestHandler } from '@sveltejs/kit';
import { Recipe } from '../../../../models/Recipe';
import { dbConnect, dbDisconnect } from '../../../../utils/db';
import { dbConnect } from '../../../../utils/db';
import type {RecipeModelType} from '../../../../types/types';
import { error } from '@sveltejs/kit';
// header: use for bearer token for now
@@ -15,7 +15,6 @@ export const POST: RequestHandler = async ({request, locals}) => {
else{
await dbConnect();
await Recipe.findOneAndUpdate({short_name: message.old_short_name }, recipe_json);
await dbDisconnect();
return new Response(JSON.stringify({msg: "Edited recipe successfully"}),{
status: 200,
});

View File

@@ -1,7 +1,7 @@
import { json, type RequestHandler } from '@sveltejs/kit';
import { UserFavorites } from '../../../../models/UserFavorites';
import { Recipe } from '../../../../models/Recipe';
import { dbConnect, dbDisconnect } from '../../../../utils/db';
import { dbConnect } from '../../../../utils/db';
import { error } from '@sveltejs/kit';
import mongoose from 'mongoose';
@@ -19,13 +19,11 @@ export const GET: RequestHandler = async ({ locals }) => {
username: session.user.nickname
}).lean();
await dbDisconnect();
return json({
favorites: userFavorites?.favorites || []
});
} catch (e) {
await dbDisconnect();
throw error(500, 'Failed to fetch favorites');
}
};
@@ -49,8 +47,7 @@ export const POST: RequestHandler = async ({ request, locals }) => {
// Validate that the recipe exists and get its ObjectId
const recipe = await Recipe.findOne({ short_name: recipeId });
if (!recipe) {
await dbDisconnect();
throw error(404, 'Recipe not found');
throw error(404, 'Recipe not found');
}
await UserFavorites.findOneAndUpdate(
@@ -59,11 +56,9 @@ export const POST: RequestHandler = async ({ request, locals }) => {
{ upsert: true, new: true }
);
await dbDisconnect();
return json({ success: true });
} catch (e) {
await dbDisconnect();
if (e instanceof Error && e.message.includes('404')) {
throw e;
}
@@ -90,8 +85,7 @@ export const DELETE: RequestHandler = async ({ request, locals }) => {
// Find the recipe's ObjectId
const recipe = await Recipe.findOne({ short_name: recipeId });
if (!recipe) {
await dbDisconnect();
throw error(404, 'Recipe not found');
throw error(404, 'Recipe not found');
}
await UserFavorites.findOneAndUpdate(
@@ -99,11 +93,9 @@ export const DELETE: RequestHandler = async ({ request, locals }) => {
{ $pull: { favorites: recipe._id } }
);
await dbDisconnect();
return json({ success: true });
} catch (e) {
await dbDisconnect();
if (e instanceof Error && e.message.includes('404')) {
throw e;
}

View File

@@ -1,7 +1,7 @@
import { json, type RequestHandler } from '@sveltejs/kit';
import { UserFavorites } from '../../../../../../models/UserFavorites';
import { Recipe } from '../../../../../../models/Recipe';
import { dbConnect, dbDisconnect } from '../../../../../../utils/db';
import { dbConnect } from '../../../../../../utils/db';
import { error } from '@sveltejs/kit';
export const GET: RequestHandler = async ({ locals, params }) => {
@@ -17,7 +17,6 @@ export const GET: RequestHandler = async ({ locals, params }) => {
// Find the recipe by short_name to get its ObjectId
const recipe = await Recipe.findOne({ short_name: params.shortName });
if (!recipe) {
await dbDisconnect();
throw error(404, 'Recipe not found');
}
@@ -27,13 +26,11 @@ export const GET: RequestHandler = async ({ locals, params }) => {
favorites: recipe._id
}).lean();
await dbDisconnect();
return json({
isFavorite: !!userFavorites
});
} catch (e) {
await dbDisconnect();
if (e instanceof Error && e.message.includes('404')) {
throw e;
}

View File

@@ -1,7 +1,7 @@
import { json, type RequestHandler } from '@sveltejs/kit';
import { UserFavorites } from '../../../../../models/UserFavorites';
import { Recipe } from '../../../../../models/Recipe';
import { dbConnect, dbDisconnect } from '../../../../../utils/db';
import { dbConnect } from '../../../../../utils/db';
import type { RecipeModelType } from '../../../../../types/types';
import { error } from '@sveltejs/kit';
@@ -20,7 +20,6 @@ export const GET: RequestHandler = async ({ locals }) => {
}).lean();
if (!userFavorites?.favorites?.length) {
await dbDisconnect();
return json([]);
}
@@ -28,13 +27,11 @@ export const GET: RequestHandler = async ({ locals }) => {
_id: { $in: userFavorites.favorites }
}).lean() as RecipeModelType[];
await dbDisconnect();
recipes = JSON.parse(JSON.stringify(recipes));
return json(recipes);
} catch (e) {
await dbDisconnect();
throw error(500, 'Failed to fetch favorite recipes');
}
};

View File

@@ -1,13 +1,12 @@
import { json, type RequestHandler } from '@sveltejs/kit';
import { Recipe } from '../../../../../models/Recipe';
import { dbConnect, dbDisconnect } from '../../../../../utils/db';
import { dbConnect } from '../../../../../utils/db';
import type {RecipeModelType} from '../../../../../types/types';
import { error } from '@sveltejs/kit';
export const GET: RequestHandler = async ({params}) => {
await dbConnect();
let recipe = (await Recipe.findOne({ short_name: params.name}).lean()) as RecipeModelType[];
await dbDisconnect();
recipe = JSON.parse(JSON.stringify(recipe));
if(recipe == null){

View File

@@ -1,12 +1,11 @@
import { json, type RequestHandler } from '@sveltejs/kit';
import type { BriefRecipeType } from '../../../../../types/types';
import { Recipe } from '../../../../../models/Recipe'
import { dbConnect, dbDisconnect } from '../../../../../utils/db';
import { dbConnect } from '../../../../../utils/db';
import { rand_array } from '$lib/js/randomize';
export const GET: RequestHandler = async ({params}) => {
await dbConnect();
let found_brief = rand_array(await Recipe.find({}, 'name short_name tags category icon description season dateModified').lean()) as BriefRecipeType[];
await dbDisconnect();
return json(JSON.parse(JSON.stringify(found_brief)));
};

View File

@@ -1,12 +1,11 @@
import { json, type RequestHandler } from '@sveltejs/kit';
import { Recipe } from '../../../../../models/Recipe';
import { dbConnect, dbDisconnect } from '../../../../../utils/db';
import { dbConnect } from '../../../../../utils/db';
import type {BriefRecipeType} from '../../../../../types/types';
export const GET: RequestHandler = async ({params}) => {
await dbConnect();
let categories = (await Recipe.distinct('category').lean());
await dbDisconnect();
categories= JSON.parse(JSON.stringify(categories));
return json(categories);

View File

@@ -1,13 +1,12 @@
import { json, type RequestHandler } from '@sveltejs/kit';
import { Recipe } from '../../../../../../models/Recipe';
import { dbConnect, dbDisconnect } from '../../../../../../utils/db';
import { dbConnect } from '../../../../../../utils/db';
import type {BriefRecipeType} from '../../../../../../types/types';
import { rand_array } from '$lib/js/randomize';
export const GET: RequestHandler = async ({params}) => {
await dbConnect();
let recipes = rand_array(await Recipe.find({category: params.category}, 'name short_name images tags category icon description season dateModified').lean()) as BriefRecipeType[];
await dbDisconnect();
recipes = JSON.parse(JSON.stringify(recipes));
return json(recipes);

View File

@@ -1,12 +1,11 @@
import { json, type RequestHandler } from '@sveltejs/kit';
import { Recipe } from '../../../../../models/Recipe';
import { dbConnect, dbDisconnect } from '../../../../../utils/db';
import { dbConnect } from '../../../../../utils/db';
import type {BriefRecipeType} from '../../../../../types/types';
export const GET: RequestHandler = async ({params}) => {
await dbConnect();
let icons = (await Recipe.distinct('icon').lean());
await dbDisconnect();
icons = JSON.parse(JSON.stringify(icons));
return json(icons);

View File

@@ -1,13 +1,12 @@
import { json, type RequestHandler } from '@sveltejs/kit';
import { Recipe } from '../../../../../../models/Recipe';
import { dbConnect, dbDisconnect } from '../../../../../../utils/db';
import { dbConnect } from '../../../../../../utils/db';
import type {BriefRecipeType} from '../../../../../../types/types';
import { rand_array } from '$lib/js/randomize';
export const GET: RequestHandler = async ({params}) => {
await dbConnect();
let recipes = rand_array(await Recipe.find({icon: params.icon}, 'name short_name images tags category icon description season dateModified').lean()) as BriefRecipeType[];
await dbDisconnect();
recipes = JSON.parse(JSON.stringify(recipes));
return json(recipes);

View File

@@ -1,13 +1,12 @@
import type {rand_array} from '$lib/js/randomize';
import { json, type RequestHandler } from '@sveltejs/kit';
import { Recipe } from '../../../../../../models/Recipe'
import { dbConnect, dbDisconnect } from '../../../../../../utils/db';
import { dbConnect } from '../../../../../../utils/db';
import { rand_array } from '$lib/js/randomize';
export const GET: RequestHandler = async ({params}) => {
await dbConnect();
let found_in_season = rand_array(await Recipe.find({season: params.month, icon: {$ne: "🍽️"}}, 'name short_name images tags category icon description season dateModified').lean());
await dbDisconnect();
found_in_season = JSON.parse(JSON.stringify(found_in_season));
return json(found_in_season);
};

View File

@@ -1,12 +1,11 @@
import { json, type RequestHandler } from '@sveltejs/kit';
import { Recipe } from '../../../../../models/Recipe';
import { dbConnect, dbDisconnect } from '../../../../../utils/db';
import { dbConnect } from '../../../../../utils/db';
import type {BriefRecipeType} from '../../../../../types/types';
export const GET: RequestHandler = async ({params}) => {
await dbConnect();
let categories = (await Recipe.distinct('tags').lean());
await dbDisconnect();
categories= JSON.parse(JSON.stringify(categories));
return json(categories);

View File

@@ -1,13 +1,12 @@
import { json, type RequestHandler } from '@sveltejs/kit';
import { Recipe } from '../../../../../../models/Recipe';
import { dbConnect, dbDisconnect } from '../../../../../../utils/db';
import { dbConnect } from '../../../../../../utils/db';
import type {BriefRecipeType} from '../../../../../../types/types';
import { rand_array } from '$lib/js/randomize';
export const GET: RequestHandler = async ({params}) => {
await dbConnect();
let recipes = rand_array(await Recipe.find({tags: params.tag}, 'name short_name images tags category icon description season dateModified').lean()) as BriefRecipeType[];
await dbDisconnect();
recipes = JSON.parse(JSON.stringify(recipes));
return json(recipes);

View File

@@ -1,6 +1,6 @@
import { json, type RequestHandler } from '@sveltejs/kit';
import { Recipe } from '../../../../../models/Recipe';
import { dbConnect, dbDisconnect } from '../../../../../utils/db';
import { dbConnect } from '../../../../../utils/db';
import { generateRecipeJsonLd } from '$lib/js/recipeJsonLd';
import type { RecipeModelType } from '../../../../../types/types';
import { error } from '@sveltejs/kit';
@@ -8,7 +8,6 @@ import { error } from '@sveltejs/kit';
export const GET: RequestHandler = async ({ params, setHeaders }) => {
await dbConnect();
let recipe = (await Recipe.findOne({ short_name: params.name }).lean()) as RecipeModelType;
await dbDisconnect();
recipe = JSON.parse(JSON.stringify(recipe));
if (recipe == null) {

View File

@@ -1,7 +1,7 @@
import { json, type RequestHandler } from '@sveltejs/kit';
import type { BriefRecipeType } from '../../../../types/types';
import { Recipe } from '../../../../models/Recipe';
import { dbConnect, dbDisconnect } from '../../../../utils/db';
import { dbConnect } from '../../../../utils/db';
export const GET: RequestHandler = async ({ url, locals }) => {
await dbConnect();
@@ -64,11 +64,9 @@ export const GET: RequestHandler = async ({ url, locals }) => {
});
}
await dbDisconnect();
return json(JSON.parse(JSON.stringify(recipes)));
} catch (error) {
await dbDisconnect();
return json({ error: 'Search failed' }, { status: 500 });
}
};