-
Notifications
You must be signed in to change notification settings - Fork 10.4k
feat: Outlook Calendar Caching with Webhook Notifications for Calendar Changes #22604
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
feat: Outlook Calendar Caching with Webhook Notifications for Calendar Changes #22604
Conversation
@sidequestdeveloper is attempting to deploy a commit to the cal Team on Vercel. A member of the Team first needs to authorize it. |
WalkthroughThe changes introduce webhook-based caching support for Office365 calendar integrations. The database schema is extended with new fields and indexes to track Office365 webhook subscription IDs, expiration, and client state. A new API route handles Office365 webhook GET validation and POST notification events, validating client state and updating cached calendar availability asynchronously. The Office365 calendar service is enhanced with methods to manage webhook subscriptions (watch/unwatch), fetch availability with caching support, and populate initial cache data. Repository methods are updated to query Office365 subscription data and support batch processing. Environment variable configuration and test mocks are updated to include the Office365 webhook client state token. Estimated code review effort4 (~90 minutes) Assessment against linked issues
Assessment against linked issues: Out-of-scope changesNo out-of-scope changes were found. All modifications are directly related to implementing Office365 webhook-based calendar caching and its supporting infrastructure. Warning There were issues while running some tools. Please review the errors and either fix the tool's configuration or disable the tool if it's a critical failure. 🔧 ESLint
packages/app-store/office365calendar/lib/CalendarService.tsOops! Something went wrong! :( ESLint: 8.57.1 ESLint couldn't find the plugin "eslint-plugin-playwright". (The package "eslint-plugin-playwright" was not found when loaded as a Node module from the directory "".) It's likely that the plugin isn't installed correctly. Try reinstalling by running the following:
The plugin "eslint-plugin-playwright" was referenced from the config file in ".eslintrc.js". If you still can't figure out the problem, please stop by https://eslint.org/chat/help to chat with the team. packages/features/calendar-cache/lib/getShouldServeCache.tsOops! Something went wrong! :( ESLint: 8.57.1 ESLint couldn't find the plugin "eslint-plugin-playwright". (The package "eslint-plugin-playwright" was not found when loaded as a Node module from the directory "".) It's likely that the plugin isn't installed correctly. Try reinstalling by running the following:
The plugin "eslint-plugin-playwright" was referenced from the config file in ".eslintrc.js". If you still can't figure out the problem, please stop by https://eslint.org/chat/help to chat with the team. 📜 Recent review detailsConfiguration used: CodeRabbit UI 📒 Files selected for processing (2)
📓 Path-based instructions (1)**/*.{js,jsx,ts,tsx}📄 CodeRabbit Inference Engine (.cursor/rules/review.mdc)
Files:
🧠 Learnings (3)📓 Common learnings
packages/features/calendar-cache/lib/getShouldServeCache.ts (1)Learnt from: vijayraghav-io packages/app-store/office365calendar/lib/CalendarService.ts (7)Learnt from: vijayraghav-io Learnt from: CR Learnt from: Anshumancanrock Learnt from: alishaz-polymath Learnt from: vijayraghav-io Learnt from: hariombalhara Learnt from: vijayraghav-io 🧰 Additional context used📓 Path-based instructions (1)**/*.{js,jsx,ts,tsx}📄 CodeRabbit Inference Engine (.cursor/rules/review.mdc)
Files:
🧠 Learnings (3)📓 Common learnings
packages/features/calendar-cache/lib/getShouldServeCache.ts (1)Learnt from: vijayraghav-io packages/app-store/office365calendar/lib/CalendarService.ts (7)Learnt from: vijayraghav-io Learnt from: CR Learnt from: Anshumancanrock Learnt from: alishaz-polymath Learnt from: vijayraghav-io Learnt from: hariombalhara Learnt from: vijayraghav-io ⏰ Context from checks skipped due to timeout of 90000ms. You can increase the timeout in your CodeRabbit configuration to a maximum of 15 minutes (900000ms). (2)
🔇 Additional comments (13)
✨ Finishing Touches
🪧 TipsChatThere are 3 ways to chat with CodeRabbit:
SupportNeed help? Create a ticket on our support page for assistance with any issues or questions. Note: Be mindful of the bot's finite context window. It's strongly recommended to break down tasks such as reading entire modules into smaller chunks. For a focused discussion, use review comments to chat about specific files and their changes, instead of using the PR comments. CodeRabbit Commands (Invoked using PR comments)
Other keywords and placeholders
CodeRabbit Configuration File (
|
Graphite Automations"Add consumer team as reviewer" took an action on this PR • (07/17/25)1 reviewer was added to this PR based on Keith Williams's automation. "Add community label" took an action on this PR • (07/17/25)1 label was added to this PR based on Keith Williams's automation. |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Actionable comments posted: 6
♻️ Duplicate comments (1)
packages/app-store/office365calendar/lib/CalendarService.ts (1)
976-995
: Apply the same error handling improvement hereThis method has the same silent failure pattern as
upsertSelectedCalendar
. Apply the same error handling improvement for consistency.
🧹 Nitpick comments (5)
.env.example (1)
465-465
: Add missing blank line at end of file.The static analysis tool correctly identified that the file should end with a blank line, which is a common convention in many codebases.
Apply this diff to add the missing blank line:
OFFICE365_WEBHOOK_CLIENT_STATE= +
packages/app-store/office365calendar/api/webhook.ts (1)
51-88
: Consider parallel processing for better performance.Processing notifications sequentially could lead to timeouts with large batches. Consider parallel processing with concurrency limits.
- for (const notification of notifications) { - try { - const { subscriptionId, resource } = notification; - // ... rest of the processing - } catch (error) { - log.error( - "Error processing notification", - safeStringify({ error, subscriptionId: notification.subscriptionId }) - ); - } - } + // Process notifications in parallel with concurrency limit + const CONCURRENCY_LIMIT = 5; + const chunks = []; + for (let i = 0; i < notifications.length; i += CONCURRENCY_LIMIT) { + chunks.push(notifications.slice(i, i + CONCURRENCY_LIMIT)); + } + + for (const chunk of chunks) { + await Promise.allSettled( + chunk.map(async (notification) => { + try { + const { subscriptionId, resource } = notification; + // ... rest of the processing + } catch (error) { + log.error( + "Error processing notification", + safeStringify({ error, subscriptionId: notification.subscriptionId }) + ); + } + }) + ); + }packages/lib/server/repository/selectedCalendar.ts (1)
161-189
: Consider extracting complex OR conditions for better readability.The nested OR conditions make the query hard to understand and maintain. Consider extracting them into helper functions.
+ private static getGoogleCalendarWatchConditions(tomorrowTimestamp: string) { + return { + integration: "google_calendar", + user: { + teams: { + some: { + team: { + features: { + some: { + featureId: "calendar-cache", + }, + }, + }, + }, + }, + }, + OR: [{ googleChannelExpiration: null }, { googleChannelExpiration: { lt: tomorrowTimestamp } }], + }; + } + + private static getOffice365WatchConditions(tomorrowTimestamp: string) { + return { + integration: "office365_calendar", + OR: [ + { office365SubscriptionExpiration: null }, + { office365SubscriptionExpiration: { lt: new Date(parseInt(tomorrowTimestamp)) } }, + ], + }; + } + static async getNextBatchToWatch(limit = 100) { const oneDayInMS = 24 * 60 * 60 * 1000; const tomorrowTimestamp = String(new Date().getTime() + oneDayInMS); const nextBatch = await prisma.selectedCalendar.findMany({ take: limit, where: { OR: [ - // Google Calendar - requires team with calendar-cache feature - { - integration: "google_calendar", - // ... existing conditions - }, - // Office365 Calendar - no team requirement - { - integration: "office365_calendar", - // ... existing conditions - }, + this.getGoogleCalendarWatchConditions(tomorrowTimestamp), + this.getOffice365WatchConditions(tomorrowTimestamp), ], // Common conditions for both calendar types AND: [ // ... existing conditions ], }, }); return nextBatch; }packages/app-store/office365calendar/lib/CalendarService.ts (2)
684-804
: Consider making subscription expiration configurableThe implementation is comprehensive with good subscription reuse logic. However, the 3-day expiration is hardcoded on line 743. Consider making this configurable through an environment variable for flexibility.
Also, ensure that
NEXT_PUBLIC_WEBAPP_URL
is properly validated and ends without a trailing slash to avoid double slashes in the webhook URL.- const expirationDateTime = new Date(Date.now() + 3 * 24 * 60 * 60 * 1000).toISOString(); // 3 Days + const expirationDays = process.env.OFFICE365_WEBHOOK_EXPIRATION_DAYS ? parseInt(process.env.OFFICE365_WEBHOOK_EXPIRATION_DAYS) : 3; + const expirationDateTime = new Date(Date.now() + expirationDays * 24 * 60 * 60 * 1000).toISOString();
960-974
: Consider throwing an error instead of silent failureWhile the userId validation is good, consider throwing an error instead of silently returning when userId is missing. This would make debugging easier and ensure calling code is aware of the failure.
if (!this.credential.userId) { - logger.error("upsertSelectedCalendar failed. userId is missing."); - return; + const error = new Error("upsertSelectedCalendar failed. userId is missing."); + logger.error(error); + throw error; }
📜 Review details
Configuration used: CodeRabbit UI
Review profile: CHILL
Plan: Pro
📒 Files selected for processing (9)
.env.example
(1 hunks)apps/api/v1/test/lib/selected-calendars/_post.test.ts
(2 hunks)packages/app-store/office365calendar/api/index.ts
(1 hunks)packages/app-store/office365calendar/api/webhook.ts
(1 hunks)packages/app-store/office365calendar/lib/CalendarService.ts
(5 hunks)packages/lib/server/repository/selectedCalendar.ts
(4 hunks)packages/prisma/migrations/20250715074701_add_office365_calendar_webhook_support/migration.sql
(1 hunks)packages/prisma/schema.prisma
(2 hunks)turbo.json
(1 hunks)
🧰 Additional context used
🧠 Learnings (1)
packages/app-store/office365calendar/lib/CalendarService.ts (1)
Learnt from: hariombalhara
PR: calcom/cal.com#22547
File: packages/embeds/embed-core/src/lib/eventHandlers/scrollByDistanceEventHandler.ts:11-14
Timestamp: 2025-07-16T11:46:28.738Z
Learning: In Cal.com's embed system, internal events like "__scrollByDistance" are fired by Cal.com's own code, so runtime validation of event data structure is unnecessary since TypeScript type system guarantees type safety for internal events.
🧬 Code Graph Analysis (1)
packages/lib/server/repository/selectedCalendar.ts (2)
packages/prisma/selects/credential.ts (1)
credentialForCalendarServiceSelect
(3-17)packages/platform/libraries/index.ts (1)
credentialForCalendarServiceSelect
(97-97)
🪛 dotenv-linter (3.3.0)
.env.example
[warning] 465-465: [EndingBlankLine] No blank line at the end of the file
🪛 Biome (1.9.4)
packages/app-store/office365calendar/lib/CalendarService.ts
[error] 935-935: Change to an optional chain.
Unsafe fix: Change to an optional chain.
(lint/complexity/useOptionalChain)
⏰ Context from checks skipped due to timeout of 90000ms. You can increase the timeout in your CodeRabbit configuration to a maximum of 15 minutes (900000ms). (2)
- GitHub Check: Install dependencies / Yarn install & cache
- GitHub Check: Security Check
🔇 Additional comments (20)
.env.example (1)
463-465
: LGTM! Well-documented environment variable addition.The new
OFFICE365_WEBHOOK_CLIENT_STATE
environment variable is properly documented with clear instructions for generating a secure random string. This aligns well with the webhook validation pattern used by other calendar integrations in the codebase.turbo.json (1)
378-378
: LGTM! Proper global environment configuration.The
OFFICE365_WEBHOOK_CLIENT_STATE
environment variable is correctly added to theglobalEnv
array in alphabetical order, ensuring it's available across all build and runtime environments.packages/app-store/office365calendar/api/index.ts (1)
3-3
: LGTM! Consistent API export pattern.The new webhook export follows the same pattern as the existing
add
andcallback
exports, properly exposing the webhook API endpoint.apps/api/v1/test/lib/selected-calendars/_post.test.ts (2)
91-93
: LGTM! Test mock data properly updated for Office365 subscription fields.The addition of
office365SubscriptionId
,office365SubscriptionExpiration
, andoffice365SubscriptionClientState
fields with null values correctly reflects the new database schema for Office365 webhook subscription tracking.
136-138
: LGTM! Consistent mock data structure maintained.The Office365 subscription fields are consistently added to both admin and non-admin test scenarios, maintaining proper test coverage.
packages/prisma/schema.prisma (3)
868-872
: LGTM! Office365 subscription fields follow established patterns.The new fields for Office365 subscription tracking are well-structured and consistent with the existing Google Calendar webhook implementation.
893-893
: Appropriate unique constraint for Office365 subscriptions.The unique constraint on
[office365SubscriptionId, eventTypeId]
ensures data integrity by preventing duplicate subscriptions.
901-903
: Well-designed indexes for Office365 webhook management.The composite indexes on
[integration, office365SubscriptionExpiration, error, watchAttempts/unwatchAttempts, maxAttempts]
will optimize queries for managing webhook subscription lifecycle.packages/app-store/office365calendar/api/webhook.ts (1)
15-23
: Ignore origin validation for the GET validation request.Microsoft Graph’s subscription validation GET only provides the validationToken and does not include any header or signature you can check against an IP range—so adding origin validation here isn’t feasible. Your handler is correct as-is for GET; the recommended security control is to validate the OFFICE365_WEBHOOK_CLIENT_STATE in your POST notification handler instead.
Likely an incorrect or invalid review comment.
packages/prisma/migrations/20250715074701_add_office365_calendar_webhook_support/migration.sql (1)
1-13
: Migration correctly implements Office365 webhook support.The SQL migration properly adds the required columns and indexes for Office365 calendar webhook subscription tracking.
packages/lib/server/repository/selectedCalendar.ts (3)
31-36
: Type definition correctly extends support for Office365 subscriptions.The addition of
office365SubscriptionId
filter follows the established pattern forgoogleChannelId
.
309-330
: New method properly implements Office365 subscription lookup.The
findFirstByOffice365SubscriptionId
method correctly filters selected calendars by integration type and includes necessary credential data.
180-187
: Confirm Office365 Calendar Feature Flag OmissionThe Google Calendar integration in packages/lib/server/repository/selectedCalendar.ts includes a
user.teams.some.team.features.some.featureId === "calendar-cache"
check, but the Office365 block at lines 180–187 omits anycalendar-cache
feature filter. Please verify that this difference is intentional and that Office365 calendar caching should not be gated by thecalendar-cache
team feature.• File: packages/lib/server/repository/selectedCalendar.ts
• Lines: ~180–187 (Office365 snippet) vs. ~163–174 (Google snippet)
• Google requirescalendar-cache
feature; Office365 does notpackages/app-store/office365calendar/lib/CalendarService.ts (7)
5-7
: LGTM!The new imports are appropriate for the caching and webhook functionality being added.
Also applies to: 15-18, 26-26
306-358
: Well-structured cache integration!The modified
getAvailability
method properly handles both cached and non-cached scenarios while maintaining backward compatibility with the optionalshouldServeCache
parameter.
360-412
: Good separation of concerns!The extraction of
fetchAvailability
from the originalgetAvailability
method improves code organization and reusability. The implementation maintains the original logic while making it accessible for both cached and non-cached scenarios.
677-677
: Good practice for safe logging!Using
safeStringify
prevents potential circular reference errors when logging response data.
950-958
: Clean adapter implementation!The
getCacheOrFetchAvailability
method provides a clean interface for the caching functionality while maintaining the expected return type.
997-1027
: Well-documented cache initialization!The method clearly explains why initial cache population is necessary for Office365 (unlike Google which sends initial sync webhooks). The error handling is appropriate as cache population failure shouldn't break the subscription creation flow.
1029-1074
: Default date range behavior verified
BothgetTimeMin()
andgetTimeMax()
intentionally default to a sliding two-month cache window when called without arguments (start of the current month and start of the month two months out, respectively), as documented inpackages/features/calendar-cache/lib/datesForCache.ts
. No changes needed.
async function postHandler(req: NextApiRequest, res: NextApiResponse) { | ||
const validationToken = req.query.validationToken; | ||
if (validationToken && typeof validationToken === "string") { | ||
res.setHeader("Content-Type", "text/plain"); | ||
res.status(200).send(validationToken); | ||
return; | ||
} | ||
|
||
const { value: notifications } = req.body; | ||
|
||
if (!notifications || !Array.isArray(notifications)) { | ||
throw new HttpError({ statusCode: 400, message: "Invalid notification payload" }); | ||
} | ||
|
||
const expectedClientState = process.env.OFFICE365_WEBHOOK_CLIENT_STATE; | ||
if (!expectedClientState) { | ||
log.error("OFFICE365_WEBHOOK_CLIENT_STATE not configured"); | ||
throw new HttpError({ statusCode: 500, message: "Webhook not configured" }); | ||
} | ||
|
||
for (const notification of notifications) { | ||
if (notification.clientState !== expectedClientState) { | ||
throw new HttpError({ statusCode: 403, message: "Invalid client state" }); | ||
} | ||
} | ||
|
||
for (const notification of notifications) { | ||
try { | ||
const { subscriptionId, resource } = notification; | ||
|
||
if (!subscriptionId || !resource) { | ||
log.warn("Notification missing required fields"); | ||
continue; | ||
} | ||
|
||
log.debug("Processing notification", { subscriptionId }); | ||
|
||
const selectedCalendar = await SelectedCalendarRepository.findFirstByOffice365SubscriptionId( | ||
subscriptionId | ||
); | ||
|
||
if (!selectedCalendar) { | ||
log.debug("No selected calendar found for subscription", { subscriptionId }); | ||
continue; | ||
} | ||
|
||
const { credential } = selectedCalendar; | ||
if (!credential) { | ||
log.debug("No credential found for selected calendar", { subscriptionId }); | ||
continue; | ||
} | ||
|
||
const { selectedCalendars } = credential; | ||
const credentialForCalendarCache = await getCredentialForCalendarCache({ credentialId: credential.id }); | ||
const calendarServiceForCalendarCache = await getCalendar(credentialForCalendarCache); | ||
await calendarServiceForCalendarCache?.fetchAvailabilityAndSetCache?.(selectedCalendars); | ||
log.debug("Successfully updated calendar cache", { subscriptionId }); | ||
} catch (error) { | ||
log.error( | ||
"Error processing notification", | ||
safeStringify({ error, subscriptionId: notification.subscriptionId }) | ||
); | ||
} | ||
} | ||
|
||
return { message: "ok" }; | ||
} |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Add request size and rate limiting protection.
The webhook endpoint should validate request size and implement rate limiting to prevent abuse.
Consider implementing:
- Request body size validation
- Rate limiting per subscription or IP
- Request timeout handling
- Dead letter queue for failed notifications
🤖 Prompt for AI Agents
In packages/app-store/office365calendar/api/webhook.ts between lines 25 and 91,
the webhook handler lacks protections against large request bodies and excessive
request rates, which can lead to abuse or service degradation. Add request body
size validation to reject overly large payloads early. Implement rate limiting
based on subscriptionId or client IP to control request frequency. Include
request timeout handling to avoid hanging requests. Consider adding a dead
letter queue mechanism to capture and retry or analyze failed notification
processing asynchronously.
for (const notification of notifications) { | ||
if (notification.clientState !== expectedClientState) { | ||
throw new HttpError({ statusCode: 403, message: "Invalid client state" }); | ||
} | ||
} |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Add protection against timing attacks in client state validation.
The client state comparison should use constant-time comparison to prevent timing attacks.
+import crypto from "crypto";
+
for (const notification of notifications) {
- if (notification.clientState !== expectedClientState) {
+ // Use constant-time comparison to prevent timing attacks
+ if (!crypto.timingSafeEqual(
+ Buffer.from(notification.clientState || ''),
+ Buffer.from(expectedClientState)
+ )) {
throw new HttpError({ statusCode: 403, message: "Invalid client state" });
}
}
📝 Committable suggestion
‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.
for (const notification of notifications) { | |
if (notification.clientState !== expectedClientState) { | |
throw new HttpError({ statusCode: 403, message: "Invalid client state" }); | |
} | |
} | |
import crypto from "crypto"; | |
for (const notification of notifications) { | |
// Use constant-time comparison to prevent timing attacks | |
if (!crypto.timingSafeEqual( | |
Buffer.from(notification.clientState || ''), | |
Buffer.from(expectedClientState) | |
)) { | |
throw new HttpError({ statusCode: 403, message: "Invalid client state" }); | |
} | |
} |
🤖 Prompt for AI Agents
In packages/app-store/office365calendar/api/webhook.ts around lines 45 to 49,
the clientState comparison uses a direct equality check which is vulnerable to
timing attacks. Replace the direct comparison with a constant-time comparison
function to securely compare notification.clientState and expectedClientState,
preventing timing attack vulnerabilities.
{ office365SubscriptionExpiration: null }, | ||
{ office365SubscriptionExpiration: { lt: new Date(parseInt(tomorrowTimestamp)) } }, | ||
], |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Fix date parsing inconsistency.
The Office365 condition parses tomorrowTimestamp
as an integer, but it's already a string representation of milliseconds.
- { office365SubscriptionExpiration: { lt: new Date(parseInt(tomorrowTimestamp)) } },
+ { office365SubscriptionExpiration: { lt: new Date(parseInt(tomorrowTimestamp, 10)) } },
📝 Committable suggestion
‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.
{ office365SubscriptionExpiration: null }, | |
{ office365SubscriptionExpiration: { lt: new Date(parseInt(tomorrowTimestamp)) } }, | |
], | |
{ office365SubscriptionExpiration: null }, | |
{ office365SubscriptionExpiration: { lt: new Date(parseInt(tomorrowTimestamp, 10)) } }, | |
], |
🤖 Prompt for AI Agents
In packages/lib/server/repository/selectedCalendar.ts around lines 184 to 186,
the code incorrectly parses tomorrowTimestamp using parseInt before passing it
to the Date constructor, but tomorrowTimestamp is already a string representing
milliseconds. Remove the parseInt call and directly pass tomorrowTimestamp to
the Date constructor to fix the date parsing inconsistency.
…and fixed incorrect unwatch query for global calendar-cache flag
/tip 100 @sidequestdeveloper |
@sidequestdeveloper: You've been awarded a $100 by Cal.com, Inc.! 👉 Complete your Algora onboarding to collect the tip. |
This PR is being marked as stale due to inactivity. |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Could you please address the coderabbit suggestions. They are right.
Please see comment here #21050 (comment). Thanks again for this contribution and apologies for the confusion. |
What does this PR do?
/claim Outlook Cache – Bounty-to-Hire #21050
Visual Demo (For contributors especially)
A visual demonstration is strongly recommended, for both the original and new change (video / image - any one).
Video Demo (if applicable):
Image Demo (if applicable):
Mandatory Tasks (DO NOT REMOVE)
How should this be tested?
Are there environment variables that should be set?
Yes, you must set
OFFICE365_WEBHOOK_CLIENT_STATE
in.env
, this is used to validate webhook notifications from Microsoft Graph about calendar changes.What are the minimal test data to have?
One user with Microsoft Outlook calendar connected and an event created to view availability page.
What is expected (happy path) to have (input and output)?
SelectedCalendar
table should haveoffice365SubscriptionId
populated after cron runNEXT_PUBLIC_LOGGER_LEVEL=2
in.env
Any other important info that could help to test that PR
Feature
table:calendar-cache
totrue
calendar-cache-serve
totrue
/api/calendar-cache/cron
) either manually or have it setup to run automatically and wait for it to complete.Note:
calendar-cache
andcalendar-cache-serve
flags need to be enabled. This was a concious design decision and can be adjusted based on the requirement.