Files
calendar/packages/features/calendar-batch
Volnei MunhozGitHubkeith@cal.com <keithwillcode@gmail.com>keith@cal.com <keithwillcode@gmail.com>keith@cal.com <keithwillcode@gmail.com>Devin AI <158243242+devin-ai-integration[bot]@users.noreply.github.com>keith@cal.com <keithwillcode@gmail.com>
253c33df33 perf: (googlecalendar): batch freebusy calls by delegation credential (#24332)
* perf(googlecalendar): batch freebusy calls by delegation credential

- Group selectedCalendars by delegationCredentialId before making API calls
- Make one batched freebusy query per delegation credential group
- Reduces total API calls while respecting credential boundaries
- Maintains existing caching behavior per group
- Updated both getAvailability and getAvailabilityWithTimeZones methods
- Added groupCalendarsByDelegationCredential helper method
- Handles edge case when no calendars provided but fallbackToPrimary is true
- Fixed linting issue: replaced hasOwnProperty with 'in' operator

Co-Authored-By: Volnei Munhoz <volnei.munhoz@gmail.com>

* test(googlecalendar): add test for delegation credential batching

- Verify calendars are grouped by delegationCredentialId
- Ensure exactly 3 API calls made for 3 delegation credential groups
- Confirm all busy times from different groups are properly returned
- Fix type-safety issues by replacing 'as any' with proper type constraints
- Fix ESLint warnings: unused variables and any types in mock functions

Co-Authored-By: Volnei Munhoz <volnei.munhoz@gmail.com>

* fix(googlecalendar): ensure fallback logic works with empty calendar groups

- Handle empty calendar groups by ensuring at least one iteration
- Add test for chunking groups larger than 50 calendars
- Verify all delegation credential batching logic works correctly

Co-Authored-By: Volnei Munhoz <volnei.munhoz@gmail.com>

* fix: fix remaining type errors from merge conflict resolution

- Changed getCacheOrFetchAvailability to getFreeBusyData in getAvailabilityWithTimeZones
- Removed orphaned merge conflict marker in test file

Co-Authored-By: Volnei Munhoz <volnei.munhoz@gmail.com>

* fix: reset test file to original PR version and update method name

- Reset CalendarService.test.ts to original PR version (0e9eb9e97a)
- Updated getCacheOrFetchAvailability to getFreeBusyData to match main branch

Co-Authored-By: Volnei Munhoz <volnei.munhoz@gmail.com>

* fix: reset test file to main branch version

The original PR's test file had tests for caching features that have been
removed from main. Reset to main's version to fix type errors.

Co-Authored-By: Volnei Munhoz <volnei.munhoz@gmail.com>

* test(googlecalendar): add tests for delegation credential batching logic

Co-Authored-By: Volnei Munhoz <volnei.munhoz@gmail.com>

* Revamp devin's implementation

* Remove comsoles.log

* fix(tests): update delegation credential batching tests to match reimplementation

- Remove tests for private methods that no longer exist (groupCalendarsByDelegationCredential, chunkArray)
- Update getAvailability test to verify calendar fetching without expecting multiple API calls per delegation credential
- Keep existing tests for fallback to primary calendar and non-google calendar handling

Co-Authored-By: Volnei Munhoz <volnei.munhoz@gmail.com>

* Remove dead code

* improve documentation

* docs: add README explaining Google Calendar availability batching feature

Co-Authored-By: Volnei Munhoz <volnei.munhoz@gmail.com>

* docs: translate README to English

Co-Authored-By: Volnei Munhoz <volnei.munhoz@gmail.com>

* docs: move README to calendar-batch package with comprehensive documentation

- Remove README from googlecalendar lib (wrong location)
- Add comprehensive README to packages/features/calendar-batch/
- Document CalendarBatchService and CalendarBatchWrapper
- Explain how getCalendar() integrates with batching
- Include architecture, data model, and performance considerations

Co-Authored-By: Volnei Munhoz <volnei.munhoz@gmail.com>

* fix: address cubic-dev-ai review comments

- Remove misleading comment from getCalendar.ts cache block
- Fix typo 'optmization' -> 'optimization' in comment
- Add comprehensive tests for CalendarBatchWrapper batching behavior
  - Test separate calls for calendars without delegationCredentialId
  - Test batching calendars with same delegationCredentialId
  - Test chunking into groups of 50 for API limits
  - Test mixed calendars handling
  - Test fallbackToPrimary with empty array
  - Test result flattening from batched calls
  - Test pass-through methods delegation

Co-Authored-By: Volnei Munhoz <volnei.munhoz@gmail.com>

* fix: add shouldServeCache param to CalendarBatchWrapper and verify batching call count

- Fix CalendarBatchWrapper.getAvailability signature to match Calendar interface (add shouldServeCache param)
- Update CalendarBatchWrapper tests to pass shouldServeCache parameter
- Add integration test in CalendarService.test.ts that verifies CalendarBatchWrapper makes separate API calls for different delegationCredentialIds (call count assertion)
- This fixes the getCalendarsEvents test failures caused by signature mismatch

Co-Authored-By: Volnei Munhoz <volnei.munhoz@gmail.com>

* Improve CalendarBatchImplementation

* test: add shouldServeCache forwarding and order-independent batching verification tests

Co-Authored-By: Volnei Munhoz <volnei.munhoz@gmail.com>

* test: make CalendarBatchWrapper tests order-independent

Refactored tests to avoid relying on Promise.all execution order:
- 'should make separate calls for calendars without delegationCredentialId'
  now uses set comparison instead of toHaveBeenNthCalledWith
- 'should batch calendars with the same delegationCredentialId together'
  now finds calls by delegation credential instead of call order

This addresses Sean's review comment about potential flakiness due to
Promise.all not guaranteeing execution order of parallel promises.

Co-Authored-By: Volnei Munhoz <volnei.munhoz@gmail.com>

* Remove hardcoded ID

* test: add comprehensive tests for resolveCalendarServeStrategy

Co-Authored-By: keith@cal.com <keithwillcode@gmail.com>

* feat: use Promise.allSettled for partial failure handling in CalendarBatchWrapper

- Changed Promise.all to Promise.allSettled in getAvailability and getAvailabilityWithTimeZones
- Returns partial results when some batches fail instead of failing entirely
- Logs warnings for failed batches with error details
- Added tests for partial failure scenarios

Co-Authored-By: keith@cal.com <keithwillcode@gmail.com>

* test: add comprehensive tests for getAvailabilityWithTimeZones

- Added batching behavior tests (separate calls, batching by delegationCredentialId, chunking)
- Added partial failure handling tests (partial results, all fail, no throw)
- Added edge case test for underlying calendar not implementing the method
- Total: 24 tests now covering both getAvailability and getAvailabilityWithTimeZones

Co-Authored-By: keith@cal.com <keithwillcode@gmail.com>

* fix: add required serviceAccountKey fields to delegatedTo mock objects

Co-Authored-By: keith@cal.com <keithwillcode@gmail.com>

* fix: add missing client_id and private_key to serviceAccountKey mock

Co-Authored-By: keith@cal.com <keithwillcode@gmail.com>

---------

Co-authored-by: Devin AI <158243242+devin-ai-integration[bot]@users.noreply.github.com>
Co-authored-by: keith@cal.com <keithwillcode@gmail.com>
2025-12-20 13:45:44 -03:00
..

Calendar Batch Feature

Overview

The Calendar Batch feature optimizes Google Calendar availability queries by intelligently batching FreeBusy API calls based on delegation credentials. This reduces the number of API calls while respecting Google's API limits.

Architecture

Components

1. CalendarBatchService

Location: lib/CalendarBatchService.ts

A simple utility class that determines which calendar types support batching optimization.

class CalendarBatchService {
  static isCalendarTypeSupported(type: string | null): boolean
}

Currently, only google_calendar is supported. This gate is used by getCalendar() to decide whether to wrap the calendar service with batching capabilities.

2. CalendarBatchWrapper

Location: lib/CalendarBatchWrapper.ts

A decorator that wraps the original calendar service and optimizes availability queries. It implements the Calendar interface, passing through most methods directly to the underlying calendar while intercepting getAvailability() and getAvailabilityWithTimeZones() for optimization.

Key Constants:

  • CALENDAR_BATCH_MAX_SIZE = 50: Google Calendar FreeBusy API limit for items per request

Key Methods:

splitCalendars(selectedCalendars)

Divides calendars into two groups based on their credential type:

  1. ownCredentials: Calendars WITHOUT delegationCredentialId

    • These are processed one-by-one because each may represent a different OAuth token
    • Conservative approach to avoid mixing credentials
  2. delegatedCredentials: Calendars WITH delegationCredentialId

    • Grouped by their delegationCredentialId
    • Calendars in the same group share the same delegated credential and can be safely batched together
partition(items, size)

Splits an array into chunks of the specified size (50 for Google API compliance).

getAvailability(dateFrom, dateTo, selectedCalendars, fallbackToPrimary)

The main optimization method:

  1. Splits calendars using splitCalendars()
  2. Creates parallel tasks:
    • One task per own credential calendar (processed individually)
    • For delegated credentials: groups by delegationCredentialId, partitions each group into batches of 50, creates one task per batch
  3. Executes all tasks in parallel with Promise.all()
  4. Flattens and returns combined results

Edge Case: If no calendars are provided, a single call is made with an empty array to honor fallbackToPrimary.

Integration with getCalendar

Location: packages/app-store/_utils/getCalendar.ts

The getCalendar() factory function decides which wrapper to use:

Decision Flow:
1. Create original calendar service from CalendarServiceMap
2. Check if CalendarCacheWrapper should be used (feature flag based)
   - If yes, return CalendarCacheWrapper (cache takes precedence)
3. Check if CalendarBatchService.isCalendarTypeSupported(calendarType)
   - If yes, return CalendarBatchWrapper
4. Otherwise, return unoptimized original calendar

Important: Cache wrapper takes precedence over batch wrapper. If calendar caching is enabled for a user, batching is not applied.

How It Works with Google Calendar

Two Levels of Chunking

The system has two independent chunking mechanisms:

  1. CalendarBatchWrapper (this feature): Chunks calendars into groups of 50 per API call based on delegation credentials

  2. GoogleCalendarService.fetchAvailabilityData(): Chunks time periods into 90-day windows (Google FreeBusy API limitation)

This means a single getAvailability() call can result in multiple Google API calls:

  • Multiple calls for different credential groups (batch wrapper)
  • Multiple calls for long date ranges > 90 days (Google service)

Example Flow

For a user with:

  • 2 personal calendars (no delegation)
  • 75 delegated calendars under delegation credential "A"
  • 30 delegated calendars under delegation credential "B"

The batch wrapper creates:

  • 2 tasks for personal calendars (one each)
  • 2 tasks for delegation "A" (50 + 25 calendars)
  • 1 task for delegation "B" (30 calendars)

Total: 5 parallel API calls instead of 107 sequential calls.

Data Model

IntegrationCalendar

The IntegrationCalendar interface extends SelectedCalendar from Prisma and includes:

  • externalId: The calendar's external identifier
  • credentialId: Reference to the Credential used
  • delegationCredentialId: Reference to a DelegationCredential (if using domain-wide delegation)

DelegationCredential

A DelegationCredential represents a shared credential that allows accessing multiple users' calendars within an organization (e.g., Google Workspace domain-wide delegation). Calendars sharing the same delegationCredentialId can be batched together because they use the same authentication context.

Performance Considerations

  1. Parallel Execution: All batched tasks run in parallel via Promise.all()

  2. Conservative Batching: Own credentials are never batched together to avoid potential token mixing issues

  3. Respects API Limits: Never exceeds 50 calendars per FreeBusy request

  4. Pass-through for Non-Availability Methods: createEvent, updateEvent, deleteEvent, and other methods are not batched - they pass directly to the underlying calendar service

Supported Calendar Types

Currently only google_calendar supports batching. To add support for other calendar types, update CalendarBatchService.isCalendarTypeSupported().