feat: add Redis caching to cospend API endpoints

Implement comprehensive caching for all cospend routes to improve performance:

Cache Implementation:
- Balance API: 30-minute TTL for user balances and global balances
- Debts API: 15-minute TTL for debt breakdown calculations
- Payments List: 10-minute TTL with pagination support
- Individual Payment: 30-minute TTL for payment details

Cache Invalidation:
- Created invalidateCospendCaches() helper function
- Invalidates user balances, debts, and payment lists on mutations
- Applied to payment create, update, and delete operations
- Applied to recurring payment execution (manual and cron)
This commit is contained in:
2026-01-13 19:45:09 +01:00
parent 5aa9a9e8fa
commit e238c940a1
7 changed files with 163 additions and 29 deletions
+23 -5
View File
@@ -4,6 +4,7 @@ import { PaymentSplit } from '../../../../models/PaymentSplit';
import { dbConnect } from '../../../../utils/db';
import { convertToCHF, isValidCurrencyCode } from '../../../../lib/utils/currency';
import { error, json } from '@sveltejs/kit';
import cache, { invalidateCospendCaches } from '$lib/server/cache';
export const GET: RequestHandler = async ({ locals, url }) => {
const auth = await locals.auth();
@@ -15,8 +16,16 @@ export const GET: RequestHandler = async ({ locals, url }) => {
const offset = parseInt(url.searchParams.get('offset') || '0');
await dbConnect();
try {
// Try cache first (include pagination params in key)
const cacheKey = `cospend:payments:list:${limit}:${offset}`;
const cached = await cache.get(cacheKey);
if (cached) {
return json(JSON.parse(cached));
}
const payments = await Payment.find()
.populate('splits')
.sort({ date: -1, createdAt: -1 })
@@ -24,7 +33,12 @@ export const GET: RequestHandler = async ({ locals, url }) => {
.skip(offset)
.lean();
return json({ payments });
const result = { payments };
// Cache for 10 minutes (shorter TTL since this changes frequently)
await cache.set(cacheKey, JSON.stringify(result), 600);
return json(result);
} catch (e) {
throw error(500, 'Failed to fetch payments');
} finally {
@@ -138,9 +152,13 @@ export const POST: RequestHandler = async ({ request, locals }) => {
await Promise.all(splitPromises);
return json({
success: true,
payment: payment._id
// Invalidate caches for all affected users
const affectedUsernames = splits.map((split: any) => split.username);
await invalidateCospendCaches(affectedUsernames, payment._id.toString());
return json({
success: true,
payment: payment._id
});
} catch (e) {
@@ -3,6 +3,7 @@ import { Payment } from '../../../../../models/Payment';
import { PaymentSplit } from '../../../../../models/PaymentSplit';
import { dbConnect } from '../../../../../utils/db';
import { error, json } from '@sveltejs/kit';
import cache, { invalidateCospendCaches } from '$lib/server/cache';
export const GET: RequestHandler = async ({ params, locals }) => {
const auth = await locals.auth();
@@ -13,15 +14,28 @@ export const GET: RequestHandler = async ({ params, locals }) => {
const { id } = params;
await dbConnect();
try {
// Try cache first
const cacheKey = `cospend:payment:${id}`;
const cached = await cache.get(cacheKey);
if (cached) {
return json(JSON.parse(cached));
}
const payment = await Payment.findById(id).populate('splits').lean();
if (!payment) {
throw error(404, 'Payment not found');
}
return json({ payment });
const result = { payment };
// Cache for 30 minutes
await cache.set(cacheKey, JSON.stringify(result), 1800);
return json(result);
} catch (e) {
if (e.status === 404) throw e;
throw error(500, 'Failed to fetch payment');
@@ -40,10 +54,10 @@ export const PUT: RequestHandler = async ({ params, request, locals }) => {
const data = await request.json();
await dbConnect();
try {
const payment = await Payment.findById(id);
if (!payment) {
throw error(404, 'Payment not found');
}
@@ -52,9 +66,13 @@ export const PUT: RequestHandler = async ({ params, request, locals }) => {
throw error(403, 'Not authorized to edit this payment');
}
// Get old splits to invalidate caches for users who were in the original payment
const oldSplits = await PaymentSplit.find({ paymentId: id }).lean();
const oldUsernames = oldSplits.map(split => split.username);
const updatedPayment = await Payment.findByIdAndUpdate(
id,
{
{
title: data.title,
description: data.description,
amount: data.amount,
@@ -67,9 +85,10 @@ export const PUT: RequestHandler = async ({ params, request, locals }) => {
{ new: true }
);
let newUsernames: string[] = [];
if (data.splits) {
await PaymentSplit.deleteMany({ paymentId: id });
const splitPromises = data.splits.map((split: any) => {
return PaymentSplit.create({
paymentId: id,
@@ -81,8 +100,13 @@ export const PUT: RequestHandler = async ({ params, request, locals }) => {
});
await Promise.all(splitPromises);
newUsernames = data.splits.map((split: any) => split.username);
}
// Invalidate caches for all users (old and new)
const allAffectedUsers = [...new Set([...oldUsernames, ...newUsernames])];
await invalidateCospendCaches(allAffectedUsers, id);
return json({ success: true, payment: updatedPayment });
} catch (e) {
if (e.status) throw e;
@@ -101,10 +125,10 @@ export const DELETE: RequestHandler = async ({ params, locals }) => {
const { id } = params;
await dbConnect();
try {
const payment = await Payment.findById(id);
if (!payment) {
throw error(404, 'Payment not found');
}
@@ -113,9 +137,16 @@ export const DELETE: RequestHandler = async ({ params, locals }) => {
throw error(403, 'Not authorized to delete this payment');
}
// Get splits to invalidate caches for affected users
const splits = await PaymentSplit.find({ paymentId: id }).lean();
const affectedUsernames = splits.map(split => split.username);
await PaymentSplit.deleteMany({ paymentId: id });
await Payment.findByIdAndDelete(id);
// Invalidate caches for all affected users
await invalidateCospendCaches(affectedUsernames, id);
return json({ success: true });
} catch (e) {
if (e.status) throw e;