Skip to content
← Public packages

@kody/google

Call Gmail, Calendar, Tasks, Drive, Docs, Sheets, People, YouTube, and Analytics through saved Google OAuth.

src/calendar-across.ts

160 lines · 4.5 KB · TypeScript
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<JsonObject>
	listEvents: (params: CalendarEventsParams) => Promise<JsonObject>
	log?: (message: string) => void
}): Promise<CalendarEventsAcrossCalendarsResult> {
	const now = input.now ?? Date.now
	const log = input.log ?? (() => undefined)
	const { calendarMaxResults = 250, showHiddenCalendars = true } = input.params
	const auth = {
		integration: input.params.integration,
		integrationName: input.params.integrationName,
		account: input.params.account,
	}
	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({
				...auth,
				maxResults: calendarMaxResults,
				pageToken: calendarPageToken,
				showHidden: showHiddenCalendars,
			})
			for (const calendar of (result.items || []) as Array<JsonObject>) {
				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({
					...auth,
					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<JsonObject>) {
					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,
	}
}