From 5de12dfb79e66a34874867987e2a6bb4f5033ee5 Mon Sep 17 00:00:00 2001 From: Linus Rath <139418639+rathlinus@users.noreply.github.com> Date: Sun, 17 May 2026 17:28:10 +0200 Subject: [PATCH] perf: speed up calendar invitation banner load Parallelize ICS parse with raw blob fetch, render the banner as soon as parsing returns instead of awaiting the existing-event lookup, and filter that lookup by UID server-side instead of fetching every event on the calendar. --- .../email/calendar-invitation-banner.tsx | 79 +++++++++++-------- 1 file changed, 47 insertions(+), 32 deletions(-) diff --git a/components/email/calendar-invitation-banner.tsx b/components/email/calendar-invitation-banner.tsx index 93470997..b87a42ac 100644 --- a/components/email/calendar-invitation-banner.tsx +++ b/components/email/calendar-invitation-banner.tsx @@ -388,41 +388,56 @@ export function CalendarInvitationBanner({ email }: CalendarInvitationBannerProp setActionNotice(null); setActionError(null); try { - const events = await client.parseCalendarEvents(client.getCalendarsAccountId(), attachment.blobId); - if (events.length > 0) { - const parsed = events[0]; - setParsedEvent(parsed); - - // JMAP strips parameters from Content-Type (RFC 8621), so method=REQUEST - // is lost. Fetch raw ICS to extract METHOD as a reliable fallback. - try { - const blob = await client.fetchBlob(attachment.blobId, 'invite.ics', 'text/calendar'); - const rawText = await blob.text(); - const icsMethod = extractMethodFromRawIcs(rawText); - if (icsMethod !== 'unknown') { - setRawIcsMethod(icsMethod); + // JMAP strips parameters from Content-Type (RFC 8621), so method=REQUEST + // is lost. Fetch raw ICS to extract METHOD as a reliable fallback — in + // parallel with parsing to save a roundtrip. + const [events, rawText] = await Promise.all([ + client.parseCalendarEvents(client.getCalendarsAccountId(), attachment.blobId), + (async () => { + try { + const blob = await client.fetchBlob(attachment.blobId, 'invite.ics', 'text/calendar'); + return await blob.text(); + } catch { + return null; } - } catch { /* ignore - fall back to heuristic detection */ } + })(), + ]); - if (parsed.uid && supportsCalendar) { - const storeHasIt = useCalendarStore.getState().events.some((e) => e.uid === parsed.uid); - if (!storeHasIt) { - try { - const serverEvents = await client.queryCalendarEvents({}); - const matching = serverEvents.filter((e) => e.uid === parsed.uid); - if (matching.length > 0) { - useCalendarStore.setState((s) => { - const existingIds = new Set(s.events.map((e) => e.id)); - const newEvents = matching.filter((e) => !existingIds.has(e.id)); - return newEvents.length > 0 ? { events: [...s.events, ...newEvents] } : s; - }); - } - } catch { /* ignore lookup failure */ } - } - } - setState('parsed'); - } else { + if (events.length === 0) { setState('error'); + return; + } + + const parsed = events[0]; + setParsedEvent(parsed); + + if (rawText) { + const icsMethod = extractMethodFromRawIcs(rawText); + if (icsMethod !== 'unknown') { + setRawIcsMethod(icsMethod); + } + } + + setState('parsed'); + + // Hydrate the calendar store with the matching event in the background — + // only needed for the "already in calendar" pill, must not block the banner. + // Filter by UID server-side; the previous unfiltered query fetched up to + // 1000 events plus multiple /get batches just to find one match. + if (parsed.uid && supportsCalendar) { + const storeHasIt = useCalendarStore.getState().events.some((e) => e.uid === parsed.uid); + if (!storeHasIt) { + client.queryCalendarEvents({ uid: parsed.uid }) + .then((matching) => { + if (matching.length === 0) return; + useCalendarStore.setState((s) => { + const existingIds = new Set(s.events.map((e) => e.id)); + const newEvents = matching.filter((e) => !existingIds.has(e.id)); + return newEvents.length > 0 ? { events: [...s.events, ...newEvents] } : s; + }); + }) + .catch(() => { /* ignore lookup failure */ }); + } } } catch { setState('error');