courrier redis

This commit is contained in:
alma 2025-04-27 14:14:27 +02:00
parent 3f415be882
commit 5ea4d457fe
6 changed files with 161 additions and 25 deletions

View File

@ -3,7 +3,7 @@ import { getServerSession } from 'next-auth';
import { authOptions } from '@/app/api/auth/[...nextauth]/route'; import { authOptions } from '@/app/api/auth/[...nextauth]/route';
import { getUserEmailCredentials } from '@/lib/services/email-service'; import { getUserEmailCredentials } from '@/lib/services/email-service';
import { prefetchUserEmailData } from '@/lib/services/prefetch-service'; import { prefetchUserEmailData } from '@/lib/services/prefetch-service';
import { getCachedEmailCredentials } from '@/lib/redis'; import { getCachedEmailCredentials, getRedisStatus, warmupRedisCache } from '@/lib/redis';
/** /**
* This endpoint is called when the app initializes to check if the user has email credentials * This endpoint is called when the app initializes to check if the user has email credentials
@ -11,6 +11,12 @@ import { getCachedEmailCredentials } from '@/lib/redis';
*/ */
export async function GET() { export async function GET() {
try { try {
// Warm up Redis connection
await warmupRedisCache();
// Get Redis status to include in response
const redisStatus = await getRedisStatus();
// Get server session to verify authentication // Get server session to verify authentication
const session = await getServerSession(authOptions); const session = await getServerSession(authOptions);
@ -18,6 +24,7 @@ export async function GET() {
if (!session?.user?.id) { if (!session?.user?.id) {
return NextResponse.json({ return NextResponse.json({
authenticated: false, authenticated: false,
redisStatus,
message: "Not authenticated" message: "Not authenticated"
}); });
} }
@ -26,10 +33,12 @@ export async function GET() {
// First, check Redis cache for credentials // First, check Redis cache for credentials
let credentials = await getCachedEmailCredentials(userId); let credentials = await getCachedEmailCredentials(userId);
let credentialsSource = 'cache';
// If not in cache, check database // If not in cache, check database
if (!credentials) { if (!credentials) {
credentials = await getUserEmailCredentials(userId); credentials = await getUserEmailCredentials(userId);
credentialsSource = 'database';
} }
// If no credentials found // If no credentials found
@ -37,6 +46,7 @@ export async function GET() {
return NextResponse.json({ return NextResponse.json({
authenticated: true, authenticated: true,
hasEmailCredentials: false, hasEmailCredentials: false,
redisStatus,
message: "No email credentials found" message: "No email credentials found"
}); });
} }
@ -52,7 +62,9 @@ export async function GET() {
authenticated: true, authenticated: true,
hasEmailCredentials: true, hasEmailCredentials: true,
email: credentials.email, email: credentials.email,
prefetchStarted: true redisStatus,
prefetchStarted: true,
credentialsSource
}); });
} catch (error) { } catch (error) {
console.error("Error checking session:", error); console.error("Error checking session:", error);

View File

@ -41,6 +41,9 @@ import { useCourrier, EmailData } from '@/hooks/use-courrier';
// Import the prefetching function // Import the prefetching function
import { prefetchFolderEmails } from '@/lib/services/prefetch-service'; import { prefetchFolderEmails } from '@/lib/services/prefetch-service';
// Import the RedisCacheStatus component
import { RedisCacheStatus } from '@/components/debug/RedisCacheStatus';
// Simplified version for this component // Simplified version for this component
function SimplifiedLoadingFix() { function SimplifiedLoadingFix() {
// In production, don't render anything // In production, don't render anything
@ -70,7 +73,7 @@ export default function CourrierPage() {
// Get all the email functionality from the hook // Get all the email functionality from the hook
const { const {
emails, emails = [],
selectedEmail, selectedEmail,
selectedEmailIds, selectedEmailIds,
currentFolder, currentFolder,
@ -130,21 +133,34 @@ export default function CourrierPage() {
// Calculate unread count (this would be replaced with actual data in production) // Calculate unread count (this would be replaced with actual data in production)
useEffect(() => { useEffect(() => {
// Example: counting unread emails in the inbox // Example: counting unread emails in the inbox
const unreadInInbox = emails.filter(email => !email.read && currentFolder === 'INBOX').length; const unreadInInbox = (emails || []).filter(email => !email.read && currentFolder === 'INBOX').length;
setUnreadCount(unreadInInbox); setUnreadCount(unreadInInbox);
}, [emails, currentFolder]); }, [emails, currentFolder]);
// Initialize session and start prefetching // Initialize session and start prefetching
useEffect(() => { useEffect(() => {
// Flag to prevent multiple initialization attempts
let isMounted = true;
const initSession = async () => { const initSession = async () => {
try { try {
// First check if Redis is ready before making API calls
const redisStatus = await fetch('/api/redis/status').then(res => res.json()).catch(() => null);
// Call the session API to check email credentials and start prefetching // Call the session API to check email credentials and start prefetching
const response = await fetch('/api/courrier/session'); const response = await fetch('/api/courrier/session');
const data = await response.json(); const data = await response.json();
if (!isMounted) return;
if (data.authenticated && data.hasEmailCredentials) { if (data.authenticated && data.hasEmailCredentials) {
console.log('Session initialized, prefetching started'); console.log('Session initialized, prefetching started');
setPrefetchStarted(true); setPrefetchStarted(true);
// Preload first page of emails for faster initial rendering
if (session?.user?.id) {
loadEmails();
}
} else if (data.authenticated && !data.hasEmailCredentials) { } else if (data.authenticated && !data.hasEmailCredentials) {
// User is authenticated but doesn't have email credentials // User is authenticated but doesn't have email credentials
setShowLoginNeeded(true); setShowLoginNeeded(true);
@ -155,7 +171,11 @@ export default function CourrierPage() {
}; };
initSession(); initSession();
}, []);
return () => {
isMounted = false;
};
}, [session?.user?.id, loadEmails]);
// Helper to get folder icons // Helper to get folder icons
const getFolderIcon = (folder: string) => { const getFolderIcon = (folder: string) => {
@ -290,6 +310,7 @@ export default function CourrierPage() {
return ( return (
<> <>
<SimplifiedLoadingFix /> <SimplifiedLoadingFix />
<RedisCacheStatus />
{/* Main layout */} {/* Main layout */}
<main className="w-full h-screen bg-black"> <main className="w-full h-screen bg-black">

View File

@ -0,0 +1,49 @@
'use client';
import { useState, useEffect } from 'react';
/**
* Debug component to show Redis connection status
* Only visible in development mode
*/
export function RedisCacheStatus() {
const [status, setStatus] = useState<'connected' | 'error' | 'loading'>('loading');
const [lastCheck, setLastCheck] = useState<string>('');
useEffect(() => {
async function checkRedis() {
try {
setStatus('loading');
const response = await fetch('/api/redis/status');
const data = await response.json();
setStatus(data.status);
setLastCheck(new Date().toLocaleTimeString());
} catch (e) {
setStatus('error');
setLastCheck(new Date().toLocaleTimeString());
}
}
checkRedis();
const interval = setInterval(checkRedis, 30000); // Check every 30 seconds
return () => clearInterval(interval);
}, []);
// Only show in development mode
if (process.env.NODE_ENV !== 'development') {
return null;
}
return (
<div className="fixed bottom-4 left-4 text-xs bg-gray-800/80 text-white p-2 rounded shadow-md z-50">
<div className="flex items-center gap-2">
<div className={`w-2 h-2 rounded-full ${
status === 'connected' ? 'bg-green-500' :
status === 'loading' ? 'bg-yellow-500' : 'bg-red-500'
}`}></div>
<span>Redis: {status}</span>
<span className="opacity-60">({lastCheck})</span>
</div>
</div>
);
}

View File

@ -94,7 +94,16 @@ export const useCourrier = () => {
// First try Redis cache with low timeout // First try Redis cache with low timeout
const cachedEmails = await getCachedEmailsWithTimeout(session.user.id, currentFolder, page, perPage, 100); const cachedEmails = await getCachedEmailsWithTimeout(session.user.id, currentFolder, page, perPage, 100);
if (cachedEmails) { if (cachedEmails) {
setEmails(cachedEmails); // Ensure cached data has emails array property
if (Array.isArray(cachedEmails.emails)) {
setEmails(prevEmails => isLoadMore ? [...prevEmails, ...cachedEmails.emails] : cachedEmails.emails);
} else if (Array.isArray(cachedEmails)) {
// Direct array response
setEmails(prevEmails => isLoadMore ? [...prevEmails, ...cachedEmails] : cachedEmails);
} else {
console.warn('Invalid cache format:', cachedEmails);
}
setIsLoading(false); setIsLoading(false);
// Still refresh in background for fresh data // Still refresh in background for fresh data
@ -129,7 +138,8 @@ export const useCourrier = () => {
if (isLoadMore) { if (isLoadMore) {
setEmails(prev => [...prev, ...data.emails]); setEmails(prev => [...prev, ...data.emails]);
} else { } else {
setEmails(data.emails); // Ensure we always set an array even if API returns invalid data
setEmails(Array.isArray(data.emails) ? data.emails : []);
} }
setTotalEmails(data.totalEmails); setTotalEmails(data.totalEmails);
@ -147,6 +157,8 @@ export const useCourrier = () => {
} }
} catch (err) { } catch (err) {
console.error('Error loading emails:', err); console.error('Error loading emails:', err);
// Set emails to empty array on error to prevent runtime issues
setEmails([]);
setError(err instanceof Error ? err.message : 'Failed to load emails'); setError(err instanceof Error ? err.message : 'Failed to load emails');
toast({ toast({
variant: "destructive", variant: "destructive",

View File

@ -332,6 +332,45 @@ export async function invalidateEmailContentCache(
await redis.del(key); await redis.del(key);
} }
/**
* Warm up Redis connection to avoid cold starts
*/
export async function warmupRedisCache(): Promise<boolean> {
try {
// Ping Redis to establish connection early
const redis = getRedisClient();
await redis.ping();
console.log('Redis connection warmed up');
return true;
} catch (error) {
console.error('Error warming up Redis:', error);
return false;
}
}
/**
* Get Redis connection status
*/
export async function getRedisStatus(): Promise<{
status: 'connected' | 'error';
ping?: string;
error?: string;
}> {
try {
const redis = getRedisClient();
const pong = await redis.ping();
return {
status: 'connected',
ping: pong
};
} catch (error) {
return {
status: 'error',
error: error instanceof Error ? error.message : String(error)
};
}
}
/** /**
* Invalidate all user email caches (email lists and content) * Invalidate all user email caches (email lists and content)
*/ */

View File

@ -6,7 +6,8 @@ import {
cacheEmailContent, cacheEmailContent,
cacheImapSession, cacheImapSession,
getCachedEmailList, getCachedEmailList,
getRedisClient getRedisClient,
warmupRedisCache
} from '@/lib/redis'; } from '@/lib/redis';
/** /**
@ -31,7 +32,25 @@ export async function getCachedEmailsWithTimeout(
clearTimeout(timeoutId); clearTimeout(timeoutId);
if (result) { if (result) {
console.log(`Using cached data for ${userId}:${folder}:${page}:${perPage}`); console.log(`Using cached data for ${userId}:${folder}:${page}:${perPage}`);
resolve(result);
// Validate and normalize the data structure
if (typeof result === 'object') {
// Make sure we have an emails array
if (!result.emails && Array.isArray(result)) {
// If result is an array, convert to proper structure
resolve({ emails: result });
} else if (!result.emails) {
// If no emails property, add empty array
resolve({ ...result, emails: [] });
} else {
// Normal case, return as is
resolve(result);
}
} else {
// Invalid data, return null
console.warn('Invalid cached data format:', result);
resolve(null);
}
} else { } else {
resolve(null); resolve(null);
} }
@ -167,20 +186,4 @@ export async function prefetchFolderEmails(
} catch (error) { } catch (error) {
console.error(`Error prefetching folder ${folder}:`, error); console.error(`Error prefetching folder ${folder}:`, error);
} }
}
/**
* Warm up Redis connection to avoid cold starts
*/
export async function warmupRedisCache(): Promise<boolean> {
try {
// Ping Redis to establish connection early
const redis = getRedisClient();
await redis.ping();
console.log('Redis connection warmed up');
return true;
} catch (error) {
console.error('Error warming up Redis:', error);
return false;
}
} }