From 4a9be647cacc79118762570d3cce1c3c9744508b Mon Sep 17 00:00:00 2001 From: alma Date: Sat, 3 May 2025 15:04:27 +0200 Subject: [PATCH] widget cache --- app/api/calendars/route.ts | 20 +++ app/api/leantime/tasks/route.ts | 18 ++ app/api/news/route.ts | 19 ++- app/api/rocket-chat/messages/route.ts | 29 +++- components/calendar.tsx | 2 +- components/email.tsx | 2 +- components/flow.tsx | 2 +- components/news.tsx | 2 +- components/parole.tsx | 2 +- lib/redis.ts | 237 +++++++++++++++++++++++++- 10 files changed, 318 insertions(+), 15 deletions(-) diff --git a/app/api/calendars/route.ts b/app/api/calendars/route.ts index 3449365d..a7032ba7 100644 --- a/app/api/calendars/route.ts +++ b/app/api/calendars/route.ts @@ -2,6 +2,7 @@ import { NextRequest, NextResponse } from "next/server"; import { getServerSession } from "next-auth/next"; import { authOptions } from "@/app/api/auth/[...nextauth]/route"; import { prisma } from "@/lib/prisma"; +import { getCachedCalendarData, cacheCalendarData } from "@/lib/redis"; /** * Handles the GET request to retrieve calendars for the authenticated user. @@ -25,6 +26,21 @@ export async function GET(req: NextRequest) { } try { + // Check for force refresh parameter + const url = new URL(req.url); + const forceRefresh = url.searchParams.get('refresh') === 'true'; + + // Try to get data from cache if not forcing refresh + if (!forceRefresh) { + const cachedData = await getCachedCalendarData(session.user.id); + if (cachedData) { + console.log(`Using cached calendar data for user ${session.user.id}`); + return NextResponse.json(cachedData); + } + } + + // If no cache or forcing refresh, fetch from database + console.log(`Fetching calendar data from database for user ${session.user.id}`); const calendars = await prisma.calendar.findMany({ where: { userId: session.user.id, @@ -42,6 +58,10 @@ export async function GET(req: NextRequest) { }); console.log("Fetched calendars with events:", calendars); + + // Cache the results + await cacheCalendarData(session.user.id, calendars); + return NextResponse.json(calendars); } catch (error) { console.error("Erreur lors de la récupération des calendriers:", error); diff --git a/app/api/leantime/tasks/route.ts b/app/api/leantime/tasks/route.ts index a99c0513..03b49cab 100644 --- a/app/api/leantime/tasks/route.ts +++ b/app/api/leantime/tasks/route.ts @@ -1,6 +1,7 @@ import { NextRequest, NextResponse } from "next/server"; import { getServerSession } from "next-auth/next"; import { authOptions } from "@/app/api/auth/[...nextauth]/route"; +import { getCachedTasksData, cacheTasksData } from "@/lib/redis"; interface Task { id: string; @@ -94,6 +95,19 @@ export async function GET(request: NextRequest) { return NextResponse.json({ error: "Unauthorized" }, { status: 401 }); } + // Check for force refresh parameter + const url = new URL(request.url); + const forceRefresh = url.searchParams.get('refresh') === 'true'; + + // Try to get data from cache if not forcing refresh + if (!forceRefresh) { + const cachedTasks = await getCachedTasksData(session.user.id); + if (cachedTasks) { + console.log(`Using cached tasks data for user ${session.user.id}`); + return NextResponse.json(cachedTasks); + } + } + console.log('Fetching tasks for user:', session.user.email); const userId = await getLeantimeUserId(session.user.email); @@ -195,6 +209,10 @@ export async function GET(request: NextRequest) { })); console.log(`Found ${tasks.length} tasks assigned to user ${userId}`); + + // Cache the results + await cacheTasksData(session.user.id, tasks); + return NextResponse.json(tasks); } catch (error) { console.error('Error in tasks route:', error); diff --git a/app/api/news/route.ts b/app/api/news/route.ts index 0ca70e9c..9d123972 100644 --- a/app/api/news/route.ts +++ b/app/api/news/route.ts @@ -1,5 +1,6 @@ import { NextResponse } from 'next/server'; import { env } from '@/lib/env'; +import { getCachedNewsData, cacheNewsData } from '@/lib/redis'; // Helper function to clean HTML content function cleanHtmlContent(text: string): string { @@ -78,8 +79,21 @@ interface NewsItem { url: string; } -export async function GET() { +export async function GET(request: Request) { try { + // Check if we should bypass cache + const url = new URL(request.url); + const forceRefresh = url.searchParams.get('refresh') === 'true'; + + // Try to get data from cache if not forcing refresh + if (!forceRefresh) { + const cachedNews = await getCachedNewsData(); + if (cachedNews) { + console.log('Using cached news data'); + return NextResponse.json(cachedNews); + } + } + console.log('Fetching news from FastAPI server...'); const response = await fetch(`${env.NEWS_API_URL}/news?limit=12`, { @@ -129,6 +143,9 @@ export async function GET() { url: article.url })); + // Cache the results + await cacheNewsData(formattedNews); + return NextResponse.json(formattedNews); } catch (error) { console.error('News API error:', error); diff --git a/app/api/rocket-chat/messages/route.ts b/app/api/rocket-chat/messages/route.ts index e11b27a4..bf00a5c6 100644 --- a/app/api/rocket-chat/messages/route.ts +++ b/app/api/rocket-chat/messages/route.ts @@ -1,6 +1,7 @@ import { getServerSession } from "next-auth"; import { authOptions } from "@/app/api/auth/[...nextauth]/route"; import { NextResponse } from "next/server"; +import { getCachedMessagesData, cacheMessagesData } from "@/lib/redis"; // Helper function to get user token using admin credentials async function getUserToken(baseUrl: string) { @@ -43,6 +44,19 @@ export async function GET(request: Request) { return NextResponse.json({ messages: [] }, { status: 200 }); } + // Check for force refresh parameter + const url = new URL(request.url); + const forceRefresh = url.searchParams.get('refresh') === 'true'; + + // Try to get data from cache if not forcing refresh + if (!forceRefresh) { + const cachedMessages = await getCachedMessagesData(session.user.id); + if (cachedMessages) { + console.log(`Using cached messages data for user ${session.user.id}`); + return NextResponse.json(cachedMessages); + } + } + const baseUrl = process.env.NEXT_PUBLIC_IFRAME_PAROLE_URL?.split('/channel')[0]; if (!baseUrl) { console.error('Failed to get Rocket.Chat base URL'); @@ -337,14 +351,15 @@ export async function GET(request: Request) { }) .slice(0, 10); - return NextResponse.json({ - messages: sortedMessages, - total: Object.keys(latestMessagePerRoom).length, - hasMore: Object.keys(latestMessagePerRoom).length > 10 - }, { status: 200 }); + const finalResponse = { messages: sortedMessages, total: Object.keys(latestMessagePerRoom).length, hasMore: Object.keys(latestMessagePerRoom).length > 10 }; + + // Cache the results + await cacheMessagesData(session.user.id, finalResponse); + + return NextResponse.json(finalResponse); } catch (error) { - console.error('Error in messages endpoint:', error); - return NextResponse.json({ messages: [], total: 0, hasMore: false }, { status: 200 }); + console.error('Error fetching messages:', error); + return NextResponse.json({ error: 'Failed to fetch messages' }, { status: 500 }); } } diff --git a/components/calendar.tsx b/components/calendar.tsx index 2ea62955..e43dd484 100644 --- a/components/calendar.tsx +++ b/components/calendar.tsx @@ -25,7 +25,7 @@ export function Calendar() { const fetchEvents = async () => { setLoading(true); try { - const response = await fetch('/api/calendars'); + const response = await fetch('/api/calendars?refresh=true'); if (!response.ok) { throw new Error('Failed to fetch events'); } diff --git a/components/email.tsx b/components/email.tsx index 0f85c394..a1f18c70 100644 --- a/components/email.tsx +++ b/components/email.tsx @@ -38,7 +38,7 @@ export function Email() { setError(null); try { - const response = await fetch('/api/courrier?folder=INBOX&page=1&perPage=5'); + const response = await fetch('/api/courrier?folder=INBOX&page=1&perPage=5' + (isRefresh ? '&refresh=true' : '')); if (!response.ok) { throw new Error('Failed to fetch emails'); } diff --git a/components/flow.tsx b/components/flow.tsx index 50f7393d..34060573 100644 --- a/components/flow.tsx +++ b/components/flow.tsx @@ -92,7 +92,7 @@ export function Duties() { setLoading(true); setError(null); try { - const response = await fetch('/api/leantime/tasks'); + const response = await fetch('/api/leantime/tasks?refresh=true'); if (!response.ok) { throw new Error('Failed to fetch tasks'); } diff --git a/components/news.tsx b/components/news.tsx index 14fe3cfc..f3ac6050 100644 --- a/components/news.tsx +++ b/components/news.tsx @@ -30,7 +30,7 @@ export function News() { if (!isRefresh) setLoading(true); try { - const response = await fetch('/api/news'); + const response = await fetch(isRefresh ? '/api/news?refresh=true' : '/api/news'); if (!response.ok) { throw new Error('Failed to fetch news'); } diff --git a/components/parole.tsx b/components/parole.tsx index e1f43bb5..62bac378 100644 --- a/components/parole.tsx +++ b/components/parole.tsx @@ -48,7 +48,7 @@ export function Parole() { setRefreshing(true); } - const response = await fetch('/api/rocket-chat/messages', { + const response = await fetch('/api/rocket-chat/messages' + (isRefresh ? '?refresh=true' : ''), { cache: 'no-store', next: { revalidate: 0 }, }); diff --git a/lib/redis.ts b/lib/redis.ts index 801581b6..80bd3825 100644 --- a/lib/redis.ts +++ b/lib/redis.ts @@ -127,7 +127,12 @@ export const KEYS = { EMAIL_LIST: (userId: string, accountId: string, folder: string, page: number, perPage: number) => `email:list:${userId}:${accountId}:${folder}:${page}:${perPage}`, EMAIL_CONTENT: (userId: string, accountId: string, emailId: string) => - `email:content:${userId}:${accountId}:${emailId}` + `email:content:${userId}:${accountId}:${emailId}`, + // New widget cache keys + CALENDAR: (userId: string) => `widget:calendar:${userId}`, + NEWS: () => `widget:news`, // Global news cache, not user-specific + TASKS: (userId: string) => `widget:tasks:${userId}`, + MESSAGES: (userId: string) => `widget:messages:${userId}` }; // TTL constants in seconds @@ -135,7 +140,12 @@ export const TTL = { CREDENTIALS: 60 * 60 * 24, // 24 hours SESSION: 60 * 60 * 4, // 4 hours (increased from 30 minutes) EMAIL_LIST: 60 * 5, // 5 minutes - EMAIL_CONTENT: 60 * 15 // 15 minutes + EMAIL_CONTENT: 60 * 15, // 15 minutes + // New widget cache TTLs + CALENDAR: 60 * 10, // 10 minutes for calendar events + NEWS: 60 * 15, // 15 minutes for news + TASKS: 60 * 10, // 10 minutes for tasks + MESSAGES: 60 * 2 // 2 minutes for messages (more frequent updates) }; interface EmailCredentials { @@ -494,4 +504,227 @@ export async function getCachedEmailCredentials( accountId: string ): Promise { return getEmailCredentials(userId, accountId); +} + +/** + * Cache calendar data for a user + */ +export async function cacheCalendarData( + userId: string, + data: any +): Promise { + const redis = getRedisClient(); + const key = KEYS.CALENDAR(userId); + + try { + await redis.set(key, JSON.stringify(data), 'EX', TTL.CALENDAR); + console.log(`Calendar data cached for user ${userId}`); + } catch (error) { + console.error(`Error caching calendar data for user ${userId}:`, error); + } +} + +/** + * Get cached calendar data for a user + */ +export async function getCachedCalendarData( + userId: string +): Promise { + const redis = getRedisClient(); + const key = KEYS.CALENDAR(userId); + + try { + const cachedData = await redis.get(key); + if (!cachedData) { + return null; + } + + return JSON.parse(cachedData); + } catch (error) { + console.error(`Error getting cached calendar data for user ${userId}:`, error); + return null; + } +} + +/** + * Invalidate calendar cache for a user + */ +export async function invalidateCalendarCache( + userId: string +): Promise { + const redis = getRedisClient(); + const key = KEYS.CALENDAR(userId); + + try { + await redis.del(key); + console.log(`Calendar cache invalidated for user ${userId}`); + } catch (error) { + console.error(`Error invalidating calendar cache for user ${userId}:`, error); + } +} + +/** + * Cache news data (global, not user-specific) + */ +export async function cacheNewsData( + data: any +): Promise { + const redis = getRedisClient(); + const key = KEYS.NEWS(); + + try { + await redis.set(key, JSON.stringify(data), 'EX', TTL.NEWS); + console.log('News data cached successfully'); + } catch (error) { + console.error('Error caching news data:', error); + } +} + +/** + * Get cached news data + */ +export async function getCachedNewsData(): Promise { + const redis = getRedisClient(); + const key = KEYS.NEWS(); + + try { + const cachedData = await redis.get(key); + if (!cachedData) { + return null; + } + + return JSON.parse(cachedData); + } catch (error) { + console.error('Error getting cached news data:', error); + return null; + } +} + +/** + * Invalidate news cache + */ +export async function invalidateNewsCache(): Promise { + const redis = getRedisClient(); + const key = KEYS.NEWS(); + + try { + await redis.del(key); + console.log('News cache invalidated'); + } catch (error) { + console.error('Error invalidating news cache:', error); + } +} + +/** + * Cache tasks data for a user + */ +export async function cacheTasksData( + userId: string, + data: any +): Promise { + const redis = getRedisClient(); + const key = KEYS.TASKS(userId); + + try { + await redis.set(key, JSON.stringify(data), 'EX', TTL.TASKS); + console.log(`Tasks data cached for user ${userId}`); + } catch (error) { + console.error(`Error caching tasks data for user ${userId}:`, error); + } +} + +/** + * Get cached tasks data for a user + */ +export async function getCachedTasksData( + userId: string +): Promise { + const redis = getRedisClient(); + const key = KEYS.TASKS(userId); + + try { + const cachedData = await redis.get(key); + if (!cachedData) { + return null; + } + + return JSON.parse(cachedData); + } catch (error) { + console.error(`Error getting cached tasks data for user ${userId}:`, error); + return null; + } +} + +/** + * Invalidate tasks cache for a user + */ +export async function invalidateTasksCache( + userId: string +): Promise { + const redis = getRedisClient(); + const key = KEYS.TASKS(userId); + + try { + await redis.del(key); + console.log(`Tasks cache invalidated for user ${userId}`); + } catch (error) { + console.error(`Error invalidating tasks cache for user ${userId}:`, error); + } +} + +/** + * Cache messages data for a user + */ +export async function cacheMessagesData( + userId: string, + data: any +): Promise { + const redis = getRedisClient(); + const key = KEYS.MESSAGES(userId); + + try { + await redis.set(key, JSON.stringify(data), 'EX', TTL.MESSAGES); + console.log(`Messages data cached for user ${userId}`); + } catch (error) { + console.error(`Error caching messages data for user ${userId}:`, error); + } +} + +/** + * Get cached messages data for a user + */ +export async function getCachedMessagesData( + userId: string +): Promise { + const redis = getRedisClient(); + const key = KEYS.MESSAGES(userId); + + try { + const cachedData = await redis.get(key); + if (!cachedData) { + return null; + } + + return JSON.parse(cachedData); + } catch (error) { + console.error(`Error getting cached messages data for user ${userId}:`, error); + return null; + } +} + +/** + * Invalidate messages cache for a user + */ +export async function invalidateMessagesCache( + userId: string +): Promise { + const redis = getRedisClient(); + const key = KEYS.MESSAGES(userId); + + try { + await redis.del(key); + console.log(`Messages cache invalidated for user ${userId}`); + } catch (error) { + console.error(`Error invalidating messages cache for user ${userId}:`, error); + } } \ No newline at end of file