feat: add Redis caching for recipe queries with automatic invalidation
All checks were successful
CI / update (push) Successful in 13s
All checks were successful
CI / update (push) Successful in 13s
Implements Redis caching layer for recipe endpoints to reduce MongoDB load and improve response times: - Install ioredis for Redis client with TypeScript support - Create cache.ts with namespaced keys (homepage: prefix) to avoid conflicts with other Redis applications - Add caching to recipe query endpoints (all_brief, by tag, in_season) with 1-hour TTL - Implement automatic cache invalidation on recipe create/edit/delete operations - Cache recipes before randomization to maximize cache reuse while maintaining random order per request - Add graceful fallback to MongoDB if Redis is unavailable - Update .env.example with Redis configuration (REDIS_HOST, REDIS_PORT) Co-Authored-By: Claude Sonnet 4.5 <noreply@anthropic.com>
This commit is contained in:
@@ -2,6 +2,7 @@ import type { RequestHandler } from '@sveltejs/kit';
|
||||
import { Recipe } from '../../../../models/Recipe';
|
||||
import { dbConnect } from '../../../../utils/db';
|
||||
import { error } from '@sveltejs/kit';
|
||||
import { invalidateRecipeCaches } from '$lib/server/cache';
|
||||
// header: use for bearer token for now
|
||||
// recipe json in body
|
||||
export const POST: RequestHandler = async ({request, cookies, locals}) => {
|
||||
@@ -19,6 +20,8 @@ export const POST: RequestHandler = async ({request, cookies, locals}) => {
|
||||
await dbConnect();
|
||||
try{
|
||||
await Recipe.create(recipe_json);
|
||||
// Invalidate recipe caches after successful creation
|
||||
await invalidateRecipeCaches();
|
||||
} catch(e){
|
||||
throw error(400, e)
|
||||
}
|
||||
|
||||
@@ -4,6 +4,7 @@ import { UserFavorites } from '../../../../models/UserFavorites';
|
||||
import { dbConnect } from '../../../../utils/db';
|
||||
import type {RecipeModelType} from '../../../../types/types';
|
||||
import { error } from '@sveltejs/kit';
|
||||
import { invalidateRecipeCaches } from '$lib/server/cache';
|
||||
// header: use for bearer token for now
|
||||
// recipe json in body
|
||||
export const POST: RequestHandler = async ({request, locals}) => {
|
||||
@@ -69,6 +70,9 @@ export const POST: RequestHandler = async ({request, locals}) => {
|
||||
// Delete the recipe
|
||||
await Recipe.findOneAndDelete({short_name: short_name});
|
||||
|
||||
// Invalidate recipe caches after successful deletion
|
||||
await invalidateRecipeCaches();
|
||||
|
||||
return new Response(JSON.stringify({msg: "Deleted recipe successfully"}),{
|
||||
status: 200,
|
||||
});
|
||||
|
||||
@@ -6,6 +6,7 @@ import { error } from '@sveltejs/kit';
|
||||
import { rename } from 'fs/promises';
|
||||
import { join } from 'path';
|
||||
import { existsSync } from 'fs';
|
||||
import { invalidateRecipeCaches } from '$lib/server/cache';
|
||||
|
||||
// header: use for bearer token for now
|
||||
// recipe json in body
|
||||
@@ -46,6 +47,10 @@ export const POST: RequestHandler = async ({request, locals}) => {
|
||||
}
|
||||
|
||||
await Recipe.findOneAndUpdate({short_name: message.old_short_name }, recipe_json);
|
||||
|
||||
// Invalidate recipe caches after successful update
|
||||
await invalidateRecipeCaches();
|
||||
|
||||
return new Response(JSON.stringify({msg: "Edited recipe successfully"}),{
|
||||
status: 200,
|
||||
});
|
||||
|
||||
@@ -3,9 +3,27 @@ import type { BriefRecipeType } from '../../../../../types/types';
|
||||
import { Recipe } from '../../../../../models/Recipe'
|
||||
import { dbConnect } from '../../../../../utils/db';
|
||||
import { rand_array } from '$lib/js/randomize';
|
||||
import cache from '$lib/server/cache';
|
||||
|
||||
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[];
|
||||
return json(JSON.parse(JSON.stringify(found_brief)));
|
||||
const cacheKey = 'recipes:all_brief';
|
||||
|
||||
// Try cache first
|
||||
let recipes: BriefRecipeType[] | null = null;
|
||||
const cached = await cache.get(cacheKey);
|
||||
|
||||
if (cached) {
|
||||
recipes = JSON.parse(cached);
|
||||
} else {
|
||||
// Cache miss - fetch from DB
|
||||
await dbConnect();
|
||||
recipes = await Recipe.find({}, 'name short_name tags category icon description season dateModified').lean() as BriefRecipeType[];
|
||||
|
||||
// Store in cache (1 hour TTL)
|
||||
await cache.set(cacheKey, JSON.stringify(recipes), 3600);
|
||||
}
|
||||
|
||||
// Apply randomization after fetching (so each request gets different order)
|
||||
const randomized = rand_array(recipes);
|
||||
return json(JSON.parse(JSON.stringify(randomized)));
|
||||
};
|
||||
|
||||
@@ -3,10 +3,27 @@ import { json, type RequestHandler } from '@sveltejs/kit';
|
||||
import { Recipe } from '../../../../../../models/Recipe'
|
||||
import { dbConnect } from '../../../../../../utils/db';
|
||||
import { rand_array } from '$lib/js/randomize';
|
||||
import cache from '$lib/server/cache';
|
||||
|
||||
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());
|
||||
found_in_season = JSON.parse(JSON.stringify(found_in_season));
|
||||
return json(found_in_season);
|
||||
const cacheKey = `recipes:in_season:${params.month}`;
|
||||
|
||||
// Try cache first
|
||||
let recipes = null;
|
||||
const cached = await cache.get(cacheKey);
|
||||
|
||||
if (cached) {
|
||||
recipes = JSON.parse(cached);
|
||||
} else {
|
||||
// Cache miss - fetch from DB
|
||||
await dbConnect();
|
||||
recipes = await Recipe.find({season: params.month, icon: {$ne: "🍽️"}}, 'name short_name images tags category icon description season dateModified').lean();
|
||||
|
||||
// Store in cache (1 hour TTL)
|
||||
await cache.set(cacheKey, JSON.stringify(recipes), 3600);
|
||||
}
|
||||
|
||||
// Apply randomization after fetching (so each request gets different order)
|
||||
const randomized = rand_array(recipes);
|
||||
return json(JSON.parse(JSON.stringify(randomized)));
|
||||
};
|
||||
|
||||
@@ -3,11 +3,27 @@ import { Recipe } from '../../../../../../models/Recipe';
|
||||
import { dbConnect } from '../../../../../../utils/db';
|
||||
import type {BriefRecipeType} from '../../../../../../types/types';
|
||||
import { rand_array } from '$lib/js/randomize';
|
||||
import cache from '$lib/server/cache';
|
||||
|
||||
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[];
|
||||
const cacheKey = `recipes:tag:${params.tag}`;
|
||||
|
||||
recipes = JSON.parse(JSON.stringify(recipes));
|
||||
return json(recipes);
|
||||
// Try cache first
|
||||
let recipes: BriefRecipeType[] | null = null;
|
||||
const cached = await cache.get(cacheKey);
|
||||
|
||||
if (cached) {
|
||||
recipes = JSON.parse(cached);
|
||||
} else {
|
||||
// Cache miss - fetch from DB
|
||||
await dbConnect();
|
||||
recipes = await Recipe.find({tags: params.tag}, 'name short_name images tags category icon description season dateModified').lean() as BriefRecipeType[];
|
||||
|
||||
// Store in cache (1 hour TTL)
|
||||
await cache.set(cacheKey, JSON.stringify(recipes), 3600);
|
||||
}
|
||||
|
||||
// Apply randomization after fetching (so each request gets different order)
|
||||
const randomized = rand_array(recipes);
|
||||
return json(JSON.parse(JSON.stringify(randomized)));
|
||||
};
|
||||
|
||||
Reference in New Issue
Block a user