import type { CalendarEventsAcrossCalendarsParams, CalendarEventsParams, CalendarListParams, } from './calendar.ts' import type { JsonObject } from './types.ts' /** Default wall clock for a cross-calendar scan. Under the ~32s MCP client window. */ export const listEventsAcrossCalendarsDefaultBudgetMs = 20_000 /** Hard cap so an explicit budget cannot run unbounded. */ export const listEventsAcrossCalendarsMaxBudgetMs = 60_000 export type CalendarEventsAcrossCalendarsResult = { items: JsonObject[] calendars: Array<{ id: string; summary: string }> failures: string[] budgetMs: number truncated: boolean truncatedReason: 'budget' | null calendarsScanned: number } /** * Missing, non-finite, and non-positive budgets use the default. * Larger values are capped. There is no unlimited scan. */ export function resolveListEventsAcrossCalendarsBudgetMs( budgetMs: number | undefined, ) { if (budgetMs == null || !Number.isFinite(budgetMs) || budgetMs < 1) { return listEventsAcrossCalendarsDefaultBudgetMs } return Math.min(Math.floor(budgetMs), listEventsAcrossCalendarsMaxBudgetMs) } export async function scanEventsAcrossCalendars(input: { params: CalendarEventsAcrossCalendarsParams now?: () => number listCalendars: (params: CalendarListParams) => Promise listEvents: (params: CalendarEventsParams) => Promise log?: (message: string) => void }): Promise { const now = input.now ?? Date.now const log = input.log ?? (() => undefined) const { account, calendarMaxResults = 250, showHiddenCalendars = true } = input.params const budgetMs = resolveListEventsAcrossCalendarsBudgetMs(input.params.budgetMs) const started = now() const deadline = started + budgetMs const calendars: Array<{ id: string; summary: string }> = [] const failures: string[] = [] const items: JsonObject[] = [] let truncated = false let calendarsScanned = 0 const expired = () => now() >= deadline log( `listEventsAcrossCalendars: start budgetMs=${budgetMs} showHidden=${showHiddenCalendars}`, ) let calendarPageToken: string | undefined try { do { if (expired()) { truncated = true break } const result = await input.listCalendars({ account, maxResults: calendarMaxResults, pageToken: calendarPageToken, showHidden: showHiddenCalendars, }) for (const calendar of (result.items || []) as Array) { if (!calendar.id) continue calendars.push({ id: String(calendar.id), summary: String(calendar.summary || calendar.id), }) } calendarPageToken = result.nextPageToken ? String(result.nextPageToken) : undefined } while (calendarPageToken) } catch (error) { failures.push(error instanceof Error ? error.message : String(error)) } log(`listEventsAcrossCalendars: listed ${calendars.length} calendars`) if (!calendars.length && !truncated) { calendars.push({ id: 'primary', summary: 'Primary' }) } for (const calendar of calendars) { if (expired()) { truncated = true break } calendarsScanned += 1 let eventPageToken: string | undefined try { do { if (expired()) { truncated = true break } const result = await input.listEvents({ account, calendarId: calendar.id, timeMin: input.params.timeMin, timeMax: input.params.timeMax, maxResults: input.params.maxResults ?? 250, q: input.params.q, singleEvents: input.params.singleEvents, orderBy: input.params.orderBy, fields: input.params.fields, pageToken: eventPageToken, }) for (const event of (result.items || []) as Array) { items.push({ ...event, calendarId: calendar.id, calendarSummary: calendar.summary, }) } eventPageToken = result.nextPageToken ? String(result.nextPageToken) : undefined } while (eventPageToken) } catch (error) { failures.push( `${calendar.summary}: ${error instanceof Error ? error.message : String(error)}`, ) } log( `listEventsAcrossCalendars: scanned ${calendar.summary} events=${items.length}`, ) if (truncated) break } log( `listEventsAcrossCalendars: done events=${items.length} calendarsScanned=${calendarsScanned}/${calendars.length} truncated=${truncated} elapsedMs=${now() - started}`, ) return { items, calendars, failures, budgetMs, truncated, truncatedReason: truncated ? 'budget' : null, calendarsScanned, } }