diff --git a/app/api/courrier/session/route.ts b/app/api/courrier/session/route.ts index f58d3d30..7ea20500 100644 --- a/app/api/courrier/session/route.ts +++ b/app/api/courrier/session/route.ts @@ -3,7 +3,7 @@ import { getServerSession } from 'next-auth'; import { authOptions } from '@/app/api/auth/[...nextauth]/route'; import { getUserEmailCredentials } from '@/lib/services/email-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 @@ -11,6 +11,12 @@ import { getCachedEmailCredentials } from '@/lib/redis'; */ export async function GET() { try { + // Warm up Redis connection + await warmupRedisCache(); + + // Get Redis status to include in response + const redisStatus = await getRedisStatus(); + // Get server session to verify authentication const session = await getServerSession(authOptions); @@ -18,6 +24,7 @@ export async function GET() { if (!session?.user?.id) { return NextResponse.json({ authenticated: false, + redisStatus, message: "Not authenticated" }); } @@ -26,10 +33,12 @@ export async function GET() { // First, check Redis cache for credentials let credentials = await getCachedEmailCredentials(userId); + let credentialsSource = 'cache'; // If not in cache, check database if (!credentials) { credentials = await getUserEmailCredentials(userId); + credentialsSource = 'database'; } // If no credentials found @@ -37,6 +46,7 @@ export async function GET() { return NextResponse.json({ authenticated: true, hasEmailCredentials: false, + redisStatus, message: "No email credentials found" }); } @@ -52,7 +62,9 @@ export async function GET() { authenticated: true, hasEmailCredentials: true, email: credentials.email, - prefetchStarted: true + redisStatus, + prefetchStarted: true, + credentialsSource }); } catch (error) { console.error("Error checking session:", error); diff --git a/app/courrier/page.tsx b/app/courrier/page.tsx index 384e17e3..23dee7f3 100644 --- a/app/courrier/page.tsx +++ b/app/courrier/page.tsx @@ -41,6 +41,9 @@ import { useCourrier, EmailData } from '@/hooks/use-courrier'; // Import the prefetching function import { prefetchFolderEmails } from '@/lib/services/prefetch-service'; +// Import the RedisCacheStatus component +import { RedisCacheStatus } from '@/components/debug/RedisCacheStatus'; + // Simplified version for this component function SimplifiedLoadingFix() { // In production, don't render anything @@ -70,7 +73,7 @@ export default function CourrierPage() { // Get all the email functionality from the hook const { - emails, + emails = [], selectedEmail, selectedEmailIds, currentFolder, @@ -130,21 +133,34 @@ export default function CourrierPage() { // Calculate unread count (this would be replaced with actual data in production) useEffect(() => { // 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); }, [emails, currentFolder]); // Initialize session and start prefetching useEffect(() => { + // Flag to prevent multiple initialization attempts + let isMounted = true; + const initSession = async () => { 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 const response = await fetch('/api/courrier/session'); const data = await response.json(); + if (!isMounted) return; + if (data.authenticated && data.hasEmailCredentials) { console.log('Session initialized, prefetching started'); setPrefetchStarted(true); + + // Preload first page of emails for faster initial rendering + if (session?.user?.id) { + loadEmails(); + } } else if (data.authenticated && !data.hasEmailCredentials) { // User is authenticated but doesn't have email credentials setShowLoginNeeded(true); @@ -155,7 +171,11 @@ export default function CourrierPage() { }; initSession(); - }, []); + + return () => { + isMounted = false; + }; + }, [session?.user?.id, loadEmails]); // Helper to get folder icons const getFolderIcon = (folder: string) => { @@ -290,6 +310,7 @@ export default function CourrierPage() { return ( <> + {/* Main layout */}
diff --git a/components/debug/RedisCacheStatus.tsx b/components/debug/RedisCacheStatus.tsx new file mode 100644 index 00000000..38b7358d --- /dev/null +++ b/components/debug/RedisCacheStatus.tsx @@ -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(''); + + 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 ( +
+
+
+ Redis: {status} + ({lastCheck}) +
+
+ ); +} \ No newline at end of file diff --git a/hooks/use-courrier.ts b/hooks/use-courrier.ts index 7f9cda49..926c7d95 100644 --- a/hooks/use-courrier.ts +++ b/hooks/use-courrier.ts @@ -94,7 +94,16 @@ export const useCourrier = () => { // First try Redis cache with low timeout const cachedEmails = await getCachedEmailsWithTimeout(session.user.id, currentFolder, page, perPage, 100); 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); // Still refresh in background for fresh data @@ -129,7 +138,8 @@ export const useCourrier = () => { if (isLoadMore) { setEmails(prev => [...prev, ...data.emails]); } 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); @@ -147,6 +157,8 @@ export const useCourrier = () => { } } catch (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'); toast({ variant: "destructive", diff --git a/lib/redis.ts b/lib/redis.ts index 02a5882b..c17c469d 100644 --- a/lib/redis.ts +++ b/lib/redis.ts @@ -332,6 +332,45 @@ export async function invalidateEmailContentCache( await redis.del(key); } +/** + * Warm up Redis connection to avoid cold starts + */ +export async function warmupRedisCache(): Promise { + 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) */ diff --git a/lib/services/prefetch-service.ts b/lib/services/prefetch-service.ts index c4dbf6dc..8f82636b 100644 --- a/lib/services/prefetch-service.ts +++ b/lib/services/prefetch-service.ts @@ -6,7 +6,8 @@ import { cacheEmailContent, cacheImapSession, getCachedEmailList, - getRedisClient + getRedisClient, + warmupRedisCache } from '@/lib/redis'; /** @@ -31,7 +32,25 @@ export async function getCachedEmailsWithTimeout( clearTimeout(timeoutId); if (result) { 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 { resolve(null); } @@ -167,20 +186,4 @@ export async function prefetchFolderEmails( } catch (error) { console.error(`Error prefetching folder ${folder}:`, error); } -} - -/** - * Warm up Redis connection to avoid cold starts - */ -export async function warmupRedisCache(): Promise { - 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; - } } \ No newline at end of file