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 { 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);

View File

@ -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 (
<>
<SimplifiedLoadingFix />
<RedisCacheStatus />
{/* Main layout */}
<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
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",

View File

@ -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<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)
*/

View File

@ -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<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;
}
}