Neah/lib/services/prefetch-service.ts

278 lines
9.3 KiB
TypeScript

'use server';
import { getImapConnection, getEmails, getEmailContent } from './email-service';
import {
cacheEmailList,
cacheEmailContent,
cacheImapSession,
getCachedEmailList,
getRedisClient,
warmupRedisCache
} from '@/lib/redis';
// Keep track of ongoing prefetch operations to prevent duplicates
const prefetchInProgress = new Map<string, boolean>();
const lastPrefetchTime = new Map<string, number>();
const PREFETCH_COOLDOWN_MS = 30000; // 30 seconds between prefetch operations
/**
* Check if we should prefetch for a user based on cooldown
*/
function shouldPrefetch(userId: string, key: string = 'general'): boolean {
const prefetchKey = `${userId}:${key}`;
// Check if prefetch is already in progress
if (prefetchInProgress.get(prefetchKey)) {
console.log(`Prefetch already in progress for ${prefetchKey}`);
return false;
}
// Check cooldown
const lastTime = lastPrefetchTime.get(prefetchKey) || 0;
const now = Date.now();
if (now - lastTime < PREFETCH_COOLDOWN_MS) {
console.log(`Prefetch cooldown active for ${prefetchKey}, last was ${Math.round((now - lastTime)/1000)}s ago`);
return false;
}
// Mark as in progress and update last time
prefetchInProgress.set(prefetchKey, true);
lastPrefetchTime.set(prefetchKey, now);
return true;
}
/**
* Mark prefetch as completed
*/
function markPrefetchCompleted(userId: string, key: string = 'general'): void {
const prefetchKey = `${userId}:${key}`;
prefetchInProgress.set(prefetchKey, false);
}
/**
* Get cached emails with timeout to ensure fast UI response
* If cache access takes longer than timeout, return null to use regular IMAP fetch
*/
export async function getCachedEmailsWithTimeout(
userId: string,
folder: string,
page: number,
perPage: number,
timeoutMs: number = 100,
accountId?: string
): Promise<any | null> {
// Skip cache if accountId is 'loading-account'
if (accountId === 'loading-account') {
console.log(`Skipping cache for loading account`);
return null;
}
// Normalize folder name by removing account prefix if present
// This ensures consistent cache key format regardless of how folder name is passed
const normalizedFolder = folder.includes(':') ? folder.split(':')[1] : folder;
// Log the normalization for debugging
if (folder !== normalizedFolder) {
console.log(`Normalized folder name from ${folder} to ${normalizedFolder} for cache lookup`);
}
return new Promise((resolve) => {
const timeoutId = setTimeout(() => {
console.log(`Cache access timeout for ${userId}:${normalizedFolder}:${page}:${perPage}${accountId ? ` for account ${accountId}` : ''}`);
resolve(null);
}, timeoutMs);
getCachedEmailList(userId, accountId || 'default', normalizedFolder, page, perPage)
.then(result => {
clearTimeout(timeoutId);
if (result) {
console.log(`Using cached data for ${userId}:${normalizedFolder}:${page}:${perPage}${accountId ? ` for account ${accountId}` : ''}`);
resolve(result);
} else {
resolve(null);
}
})
.catch(err => {
clearTimeout(timeoutId);
console.error('Error accessing cache:', err);
resolve(null);
});
});
}
/**
* Refresh emails in background without blocking UI
* This allows the UI to show cached data immediately while refreshing in background
*/
export async function refreshEmailsInBackground(
userId: string,
folder: string = 'INBOX',
page: number = 1,
perPage: number = 20,
accountId?: string
): Promise<void> {
// Normalize folder name by removing account prefix if present
const normalizedFolder = folder.includes(':') ? folder.split(':')[1] : folder;
const folderAccountId = folder.includes(':') ? folder.split(':')[0] : accountId;
const prefetchKey = `refresh:${normalizedFolder}:${page}:${folderAccountId || ''}`;
// Skip if already in progress or in cooldown
if (!shouldPrefetch(userId, prefetchKey)) {
return;
}
// Use setTimeout to ensure this runs after current execution context
setTimeout(async () => {
try {
console.log(`Background refresh for ${userId}:${normalizedFolder}:${page}:${perPage}${folderAccountId ? ` for account ${folderAccountId}` : ''}`);
const freshData = await getEmails(userId, normalizedFolder, page, perPage, folderAccountId);
console.log(`Background refresh completed for ${userId}:${normalizedFolder}${folderAccountId ? ` for account ${folderAccountId}` : ''}`);
} catch (error) {
console.error('Background refresh error:', error);
} finally {
markPrefetchCompleted(userId, prefetchKey);
}
}, 100);
}
/**
* Prefetch basic email data for faster initial loading
* This function should be called when a user logs in
*/
export async function prefetchUserEmailData(userId: string): Promise<void> {
// Skip if already in progress or in cooldown
if (!shouldPrefetch(userId)) {
return;
}
console.log(`Starting email prefetch for user ${userId}`);
const startTime = Date.now();
try {
// Connect to IMAP server
const client = await getImapConnection(userId);
// 1. Prefetch mailbox list
const mailboxes = await client.list();
const mailboxPaths = mailboxes.map(mailbox => mailbox.path);
// Cache mailbox list in session data
await cacheImapSession(userId, {
lastActive: Date.now(),
mailboxes: mailboxPaths
});
console.log(`Prefetched ${mailboxPaths.length} folders for user ${userId}`);
// 2. Prefetch email lists for important folders
const importantFolders = [
'INBOX',
mailboxPaths.find(path => path.toLowerCase().includes('sent')) || 'Sent',
mailboxPaths.find(path => path.toLowerCase().includes('draft')) || 'Drafts'
].filter(Boolean);
// Fetch first page of each important folder
for (const folder of importantFolders) {
try {
console.log(`Prefetching emails for ${folder}`);
const emailList = await getEmails(userId, folder, 1, 20);
console.log(`Prefetched ${emailList.emails.length} emails for ${folder}`);
} catch (error) {
console.error(`Error prefetching emails for folder ${folder}:`, error);
// Continue with other folders even if one fails
}
}
// 3. Prefetch content of recent unread emails in INBOX
try {
// Get the list again (it's already cached so this will be fast)
const inboxList = await getEmails(userId, 'INBOX', 1, 20);
// Prefetch content for up to 5 recent unread emails
const unreadEmails = inboxList.emails
.filter(email => !email.flags.seen)
.slice(0, 5);
if (unreadEmails.length > 0) {
console.log(`Prefetching content for ${unreadEmails.length} unread emails`);
// Fetch content in parallel for speed
await Promise.allSettled(
unreadEmails.map(email =>
getEmailContent(userId, email.id, 'INBOX')
.catch(err => console.error(`Error prefetching email ${email.id}:`, err))
)
);
console.log(`Completed prefetching content for unread emails`);
}
} catch (error) {
console.error('Error prefetching unread email content:', error);
}
const duration = (Date.now() - startTime) / 1000;
console.log(`Email prefetch completed for user ${userId} in ${duration.toFixed(2)}s`);
} catch (error) {
console.error('Error during email prefetch:', error);
} finally {
markPrefetchCompleted(userId);
}
}
/**
* Prefetch a specific folder's emails
* This can be used when the user navigates to a folder to preload more pages
*/
export async function prefetchFolderEmails(
userId: string,
folder: string,
pages: number = 3,
startPage: number = 1,
accountId?: string
): Promise<void> {
// Normalize folder name by removing account prefix if present
const normalizedFolder = folder.includes(':') ? folder.split(':')[1] : folder;
const folderAccountId = folder.includes(':') ? folder.split(':')[0] : accountId;
const prefetchKey = `folder:${normalizedFolder}:${startPage}:${folderAccountId || ''}`;
// Skip if already in progress or in cooldown
if (!shouldPrefetch(userId, prefetchKey)) {
return;
}
try {
console.log(`Prefetching ${pages} pages of emails for folder ${normalizedFolder} starting from page ${startPage}${folderAccountId ? ` for account ${folderAccountId}` : ''}`);
// Calculate the range of pages to prefetch
const pagesToFetch = Array.from(
{ length: pages },
(_, i) => startPage + i
);
console.log(`Will prefetch pages: ${pagesToFetch.join(', ')}`);
// Fetch multiple pages in parallel
await Promise.allSettled(
pagesToFetch.map(page =>
getEmails(userId, normalizedFolder, page, 20, folderAccountId)
.then(result => {
console.log(`Successfully prefetched and cached page ${page} of ${normalizedFolder} with ${result.emails.length} emails`);
return result;
})
.catch(err => {
console.error(`Error prefetching page ${page} of ${normalizedFolder}:`, err);
return null;
})
)
);
console.log(`Completed prefetching ${pages} pages for ${normalizedFolder}`);
} catch (error) {
console.error(`Error during folder prefetch:`, error);
} finally {
markPrefetchCompleted(userId, prefetchKey);
}
}