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]);
|
}, [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(() => {
|
useEffect(() => {
|
||||||
if (isAuthenticated && client && mailboxes.length === 0) {
|
if (isAuthenticated && client && mailboxes.length === 0) {
|
||||||
let retryTimer: ReturnType<typeof setTimeout> | null = null;
|
let retryTimer: ReturnType<typeof setTimeout> | null = null;
|
||||||
@@ -658,18 +661,14 @@ export default function Home() {
|
|||||||
|
|
||||||
const loadData = async (attempt = 1) => {
|
const loadData = async (attempt = 1) => {
|
||||||
try {
|
try {
|
||||||
// First fetch mailboxes and quota (inbox will be auto-selected in fetchMailboxes)
|
|
||||||
await Promise.all([
|
await Promise.all([
|
||||||
fetchMailboxes(client),
|
fetchMailboxes(client),
|
||||||
fetchQuota(client)
|
fetchQuota(client)
|
||||||
]);
|
]);
|
||||||
|
|
||||||
// Get the selected mailbox (should be inbox by default)
|
|
||||||
const state = useEmailStore.getState();
|
const state = useEmailStore.getState();
|
||||||
const selectedMailboxId = state.selectedMailbox;
|
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) {
|
if (state.mailboxes.length === 0 && attempt <= 5 && !cancelled) {
|
||||||
const delay = Math.min(1000 * attempt, 5000);
|
const delay = Math.min(1000 * attempt, 5000);
|
||||||
debug.log('jmap', `[Mailbox] No mailboxes returned (attempt ${attempt}), retrying in ${delay}ms`);
|
debug.log('jmap', `[Mailbox] No mailboxes returned (attempt ${attempt}), retrying in ${delay}ms`);
|
||||||
@@ -677,34 +676,13 @@ export default function Home() {
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
// Fetch emails for the selected mailbox
|
|
||||||
if (selectedMailboxId) {
|
if (selectedMailboxId) {
|
||||||
await fetchEmails(client, selectedMailboxId);
|
await fetchEmails(client, selectedMailboxId);
|
||||||
} else {
|
} else {
|
||||||
await fetchEmails(client);
|
await fetchEmails(client);
|
||||||
}
|
}
|
||||||
|
|
||||||
// Fetch tag counts
|
|
||||||
fetchTagCounts(client);
|
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) {
|
} catch (error) {
|
||||||
console.error('Error loading email data:', error);
|
console.error('Error loading email data:', error);
|
||||||
}
|
}
|
||||||
@@ -714,17 +692,33 @@ export default function Home() {
|
|||||||
return () => {
|
return () => {
|
||||||
cancelled = true;
|
cancelled = true;
|
||||||
if (retryTimer) clearTimeout(retryTimer);
|
if (retryTimer) clearTimeout(retryTimer);
|
||||||
client.closePushNotifications();
|
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
}, [isAuthenticated, client, mailboxes.length, fetchMailboxes, fetchEmails, fetchQuota, fetchTagCounts]);
|
||||||
|
|
||||||
// Cleanup push notifications on unmount
|
// Push notifications: set up once per client and tear down when the client
|
||||||
return () => {
|
// goes away (logout or account switch). Kept separate from the fetch effect
|
||||||
if (client) {
|
// above so it still runs when data was prefetched at login time.
|
||||||
client.closePushNotifications();
|
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
|
// 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
|
// than one account is connected. Runs whenever the set of connected accounts
|
||||||
|
|||||||
@@ -512,6 +512,15 @@ export const useAuthStore = create<AuthState>()(
|
|||||||
activeAccountId: accountId,
|
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
|
// Schedule token refresh for TOTP-upgraded sessions
|
||||||
if (upgradedToOAuth && oauthExpiresIn > 0) {
|
if (upgradedToOAuth && oauthExpiresIn > 0) {
|
||||||
scheduleRefresh(oauthExpiresIn, get().refreshAccessToken, accountId);
|
scheduleRefresh(oauthExpiresIn, get().refreshAccessToken, accountId);
|
||||||
@@ -709,6 +718,12 @@ export const useAuthStore = create<AuthState>()(
|
|||||||
activeAccountId: accountId,
|
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);
|
scheduleRefresh(expires_in, get().refreshAccessToken, accountId);
|
||||||
|
|
||||||
notifyParent('sso:auth-success', { username });
|
notifyParent('sso:auth-success', { username });
|
||||||
@@ -841,6 +856,12 @@ export const useAuthStore = create<AuthState>()(
|
|||||||
activeAccountId: accountId,
|
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);
|
scheduleRefresh(expires_in, get().refreshAccessToken, accountId);
|
||||||
|
|
||||||
notifyParent('sso:auth-success', { username });
|
notifyParent('sso:auth-success', { username });
|
||||||
|
|||||||
@@ -72,6 +72,10 @@ interface EmailStore {
|
|||||||
// JMAP operations
|
// JMAP operations
|
||||||
fetchMailboxes: (client: IJMAPClient) => Promise<void>;
|
fetchMailboxes: (client: IJMAPClient) => Promise<void>;
|
||||||
fetchEmails: (client: IJMAPClient, mailboxId?: string) => 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>;
|
loadMoreEmails: (client: IJMAPClient) => Promise<void>;
|
||||||
fetchEmailContent: (client: IJMAPClient, emailId: string) => Promise<Email | null>;
|
fetchEmailContent: (client: IJMAPClient, emailId: string) => Promise<Email | null>;
|
||||||
fetchQuota: (client: IJMAPClient) => Promise<void>;
|
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) => {
|
fetchEmails: async (client, mailboxId) => {
|
||||||
set({ isLoading: true, error: null }); // Keep previous emails visible during transition
|
set({ isLoading: true, error: null }); // Keep previous emails visible during transition
|
||||||
try {
|
try {
|
||||||
|
|||||||
Reference in New Issue
Block a user