feat: implement prefetching of initial email data
This commit is contained in:
+26
-32
@@ -650,7 +650,10 @@ export default function Home() {
|
||||
}
|
||||
}, [initialCheckDone, isAuthenticated, authLoading]);
|
||||
|
||||
// Load mailboxes and emails when authenticated (only if not already loaded)
|
||||
// Fallback fetch for paths that didn't go through login()'s prefetch
|
||||
// (notably checkAuth on page refresh). The prefetch in auth-store/login()
|
||||
// populates mailboxes before this effect first runs, so on the post-login
|
||||
// path this block is a no-op.
|
||||
useEffect(() => {
|
||||
if (isAuthenticated && client && mailboxes.length === 0) {
|
||||
let retryTimer: ReturnType<typeof setTimeout> | null = null;
|
||||
@@ -658,18 +661,14 @@ export default function Home() {
|
||||
|
||||
const loadData = async (attempt = 1) => {
|
||||
try {
|
||||
// First fetch mailboxes and quota (inbox will be auto-selected in fetchMailboxes)
|
||||
await Promise.all([
|
||||
fetchMailboxes(client),
|
||||
fetchQuota(client)
|
||||
]);
|
||||
|
||||
// Get the selected mailbox (should be inbox by default)
|
||||
const state = useEmailStore.getState();
|
||||
const selectedMailboxId = state.selectedMailbox;
|
||||
|
||||
// On first login the server may still be provisioning mailboxes.
|
||||
// Retry a few times with back-off before giving up.
|
||||
if (state.mailboxes.length === 0 && attempt <= 5 && !cancelled) {
|
||||
const delay = Math.min(1000 * attempt, 5000);
|
||||
debug.log('jmap', `[Mailbox] No mailboxes returned (attempt ${attempt}), retrying in ${delay}ms`);
|
||||
@@ -677,34 +676,13 @@ export default function Home() {
|
||||
return;
|
||||
}
|
||||
|
||||
// Fetch emails for the selected mailbox
|
||||
if (selectedMailboxId) {
|
||||
await fetchEmails(client, selectedMailboxId);
|
||||
} else {
|
||||
await fetchEmails(client);
|
||||
}
|
||||
|
||||
// Fetch tag counts
|
||||
fetchTagCounts(client);
|
||||
|
||||
// Setup push notifications after successful data load
|
||||
try {
|
||||
// Register state change callback
|
||||
client.onStateChange((change) => handleStateChange(change, client));
|
||||
|
||||
// Start receiving push notifications
|
||||
const pushEnabled = client.setupPushNotifications();
|
||||
|
||||
if (pushEnabled) {
|
||||
setPushConnected(true);
|
||||
debug.log('push', '[Push] Push notifications successfully enabled');
|
||||
} else {
|
||||
debug.log('push', '[Push] Push notifications not available on this server');
|
||||
}
|
||||
} catch (error) {
|
||||
// Push notifications are optional - don't break the app if they fail
|
||||
debug.log('push', '[Push] Failed to setup push notifications:', error);
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('Error loading email data:', error);
|
||||
}
|
||||
@@ -714,17 +692,33 @@ export default function Home() {
|
||||
return () => {
|
||||
cancelled = true;
|
||||
if (retryTimer) clearTimeout(retryTimer);
|
||||
client.closePushNotifications();
|
||||
};
|
||||
}
|
||||
}, [isAuthenticated, client, mailboxes.length, fetchMailboxes, fetchEmails, fetchQuota, fetchTagCounts]);
|
||||
|
||||
// Cleanup push notifications on unmount
|
||||
return () => {
|
||||
if (client) {
|
||||
client.closePushNotifications();
|
||||
// Push notifications: set up once per client and tear down when the client
|
||||
// goes away (logout or account switch). Kept separate from the fetch effect
|
||||
// above so it still runs when data was prefetched at login time.
|
||||
useEffect(() => {
|
||||
if (!isAuthenticated || !client) return;
|
||||
|
||||
try {
|
||||
client.onStateChange((change) => handleStateChange(change, client));
|
||||
const pushEnabled = client.setupPushNotifications();
|
||||
if (pushEnabled) {
|
||||
setPushConnected(true);
|
||||
debug.log('push', '[Push] Push notifications successfully enabled');
|
||||
} else {
|
||||
debug.log('push', '[Push] Push notifications not available on this server');
|
||||
}
|
||||
} catch (error) {
|
||||
debug.log('push', '[Push] Failed to setup push notifications:', error);
|
||||
}
|
||||
|
||||
return () => {
|
||||
client.closePushNotifications();
|
||||
};
|
||||
}, [isAuthenticated, client, mailboxes.length, fetchMailboxes, fetchEmails, fetchQuota, fetchTagCounts, handleStateChange, setPushConnected]);
|
||||
}, [isAuthenticated, client, handleStateChange, setPushConnected]);
|
||||
|
||||
// Keep unified mailbox counts in sync when the feature is enabled and more
|
||||
// than one account is connected. Runs whenever the set of connected accounts
|
||||
|
||||
@@ -512,6 +512,15 @@ export const useAuthStore = create<AuthState>()(
|
||||
activeAccountId: accountId,
|
||||
});
|
||||
|
||||
// Kick off mailbox/quota/email fetches now so they overlap with the
|
||||
// soft-nav + home-page hydration that follows login. Dynamic import
|
||||
// avoids a static circular dep with email-store.
|
||||
import('@/stores/email-store').then(({ useEmailStore }) => {
|
||||
useEmailStore.getState().prefetchInitialData(client).catch((err) => {
|
||||
debug.error('Initial data prefetch failed:', err);
|
||||
});
|
||||
}).catch(() => {});
|
||||
|
||||
// Schedule token refresh for TOTP-upgraded sessions
|
||||
if (upgradedToOAuth && oauthExpiresIn > 0) {
|
||||
scheduleRefresh(oauthExpiresIn, get().refreshAccessToken, accountId);
|
||||
@@ -709,6 +718,12 @@ export const useAuthStore = create<AuthState>()(
|
||||
activeAccountId: accountId,
|
||||
});
|
||||
|
||||
import('@/stores/email-store').then(({ useEmailStore }) => {
|
||||
useEmailStore.getState().prefetchInitialData(client).catch((err) => {
|
||||
debug.error('Initial data prefetch failed:', err);
|
||||
});
|
||||
}).catch(() => {});
|
||||
|
||||
scheduleRefresh(expires_in, get().refreshAccessToken, accountId);
|
||||
|
||||
notifyParent('sso:auth-success', { username });
|
||||
@@ -841,6 +856,12 @@ export const useAuthStore = create<AuthState>()(
|
||||
activeAccountId: accountId,
|
||||
});
|
||||
|
||||
import('@/stores/email-store').then(({ useEmailStore }) => {
|
||||
useEmailStore.getState().prefetchInitialData(client).catch((err) => {
|
||||
debug.error('Initial data prefetch failed:', err);
|
||||
});
|
||||
}).catch(() => {});
|
||||
|
||||
scheduleRefresh(expires_in, get().refreshAccessToken, accountId);
|
||||
|
||||
notifyParent('sso:auth-success', { username });
|
||||
|
||||
@@ -72,6 +72,10 @@ interface EmailStore {
|
||||
// JMAP operations
|
||||
fetchMailboxes: (client: IJMAPClient) => Promise<void>;
|
||||
fetchEmails: (client: IJMAPClient, mailboxId?: string) => Promise<void>;
|
||||
// Eager post-login bootstrap: fires mailboxes/quota/emails so the round-trips
|
||||
// overlap with Next's soft-nav + home-page hydration. Safe to call multiple
|
||||
// times; later calls are no-ops while a prior one is in flight.
|
||||
prefetchInitialData: (client: IJMAPClient) => Promise<void>;
|
||||
loadMoreEmails: (client: IJMAPClient) => Promise<void>;
|
||||
fetchEmailContent: (client: IJMAPClient, emailId: string) => Promise<Email | null>;
|
||||
fetchQuota: (client: IJMAPClient) => Promise<void>;
|
||||
@@ -350,6 +354,33 @@ export const useEmailStore = create<EmailStore>((set, get) => ({
|
||||
}
|
||||
},
|
||||
|
||||
prefetchInitialData: async (client) => {
|
||||
// Coalesce overlapping callers (e.g. login() and a slow home-page useEffect
|
||||
// racing for the same fetch). The promise is stashed on the client so we
|
||||
// don't need a separate keyed map and stale entries can't outlive the client.
|
||||
const target = client as IJMAPClient & { __prefetchPromise?: Promise<void> };
|
||||
if (target.__prefetchPromise) return target.__prefetchPromise;
|
||||
target.__prefetchPromise = (async () => {
|
||||
try {
|
||||
await Promise.all([
|
||||
get().fetchMailboxes(client),
|
||||
get().fetchQuota(client),
|
||||
]);
|
||||
const { selectedMailbox } = get();
|
||||
if (selectedMailbox) {
|
||||
await get().fetchEmails(client, selectedMailbox);
|
||||
} else {
|
||||
await get().fetchEmails(client);
|
||||
}
|
||||
// Tag counts can finish whenever; don't block the prefetch on them.
|
||||
void get().fetchTagCounts(client);
|
||||
} finally {
|
||||
delete target.__prefetchPromise;
|
||||
}
|
||||
})();
|
||||
return target.__prefetchPromise;
|
||||
},
|
||||
|
||||
fetchEmails: async (client, mailboxId) => {
|
||||
set({ isLoading: true, error: null }); // Keep previous emails visible during transition
|
||||
try {
|
||||
|
||||
Reference in New Issue
Block a user