Async import of the appStore packages
calcom/cal.com#8087Golden Comments (2/2 found)
| # | Comment | Severity | Status | FriendlyReviewer Detail |
|---|---|---|---|---|
| #1 | Consider adding try-catch around the await to handle import failures gracefully |
LOW | ✓ Found | "The await appStore[...] for the dynamic import has no error handling; a failing module import rejects getCalendar and can abort whole Promise.all batches in callers. Wrap in try/catch and return null on failure" (packages/app-store/_utils/getCalendar.ts L15) |
| #2 | The code uses forEach with async callbacks, which causes asynchronous operations (e.g., calendar/video event deletions, payment refunds) to run concurrently without being awaited. This 'fire-and-forget' behavior leads to unhandled promise rejections, race conditions, and incomplete cleanup, as surrounding try-catch blocks cannot properly handle errors from these unawaited promises. Replace forEach with for...of loops or Promise.all() with map() to ensure proper sequential execution and error handling. |
HIGH | ✓ Found | "The forEach(async ...) loop is fire-and-forget; calendar/video deletions may not complete (or may never run in serverless) and rejections go unhandled. Collect and await the promises with Promise.all" (packages/trpc/server/routers/viewer/bookings.tsx L553) |
Supplementary Findings (16 findings)
| Finding | File | Severity | Legitimate? |
|---|---|---|---|
getCalendarCredentials remains synchronous: it eagerly fires all dynamic imports and returns calendar as a Promise, causing unhandled rejections and defeating lazy loading |
CalendarManager.ts:28 | HIGH | ✓ Real regression: lazy loading defeated, unhandled rejections possible |
Unguarded await appStore[...] can reject on module-load failure and bypass the Daily-video fallback in createMeeting |
videoClient.ts:26 | HIGH | ✓ Real: the Daily fallback is silently skipped on import failure |
| The dynamic import await has no try/catch; a failed import throws out of deletePayment instead of returning false | deletePayment.ts:16 | HIGH | ✓ Real error-contract break introduced by the conversion |
Optional chaining only guards apps, so a missing apps[appId] throws a TypeError when accessing .price/.currency |
handlePayment.ts:35 | HIGH | ✓ Real potential TypeError on a missing payment app |
In the recurring-event cancel path, async push into apiDeletes races with the subsequent Promise.all(prismaPromises.concat(apiDeletes)), so some deletions can be missed |
handleCancelBooking.ts:460 | HIGH | ✓ Real race: some calendar deletions silently dropped |
The paid-cancel path does await apiDeletes (a bare array), which resolves immediately without waiting for the contained promises; should be await Promise.all(apiDeletes) |
handleCancelBooking.ts:617 | HIGH | ✓ Real bug: deletions are not actually awaited |
The forEach(async ...) makes calendar/video cleanup fire-and-forget; the wrapping try/catch no longer catches errors inside async callbacks (they become unhandled rejections) |
vital/reschedule.ts:125 | HIGH | ✓ Real: error logging lost, rejections unhandled |
Same fire-and-forget pattern in the wipemycalother reschedule: forEach(async ...) calendar/video cleanup with a try/catch that can no longer catch errors inside async callbacks |
wipemycalother/reschedule.ts:125 | HIGH | ✓ Real: error logging lost, rejections unhandled |
| When the payment app is unavailable, the paid-cancel handler returns null, abandoning cancellation and skipping cleanup/webhook while reporting success | handleCancelBooking.ts:589 | MEDIUM | ✓ Real silent-abandon path on a missing payment app |
| The rejectBooking mutation returns null when the payment app is missing, leaving the booking PENDING and never sending declined emails, breaking the response contract | bookings.tsx:970 | MEDIUM | ✓ Real: booking stuck PENDING, no declined emails |
The lastAttendee condition length < 0 is never true, so seat-cancellation never cleans up calendar/video for the final attendee; should be === 0 |
handleCancelBooking.ts:223 | MEDIUM | ✓ Real dead condition; pre-existing, now in scope |
The key derivation calendarType.split("_").join("") plus as keyof typeof appStore silently maps unknown credential types to undefined, with no startup verification that every type resolves |
getCalendar.ts:15 | LOW | ✓ Borderline: silent misrouting, a dev-time guard would help |
Type-unsafety of new CalendarService(credential) relies on structural compatibility across a union of constructors |
getCalendar.ts:22 | LOW | ✓ Borderline: typing improvement, no runtime impact |
| Logs the CalendarService class at info level on every lookup; should be debug or removed | getCalendar.ts:20 | LOW | ✓ Real log-noise nit |
| The performance mark is placed after awaiting all calendar module loads, so the import cost is excluded from the measured metric | CalendarManager.ts:142 | LOW | ✓ Borderline: measurement-correctness nit |
Sequential module resolution in the for loop serializes cold-load latency across multiple credentials; consider resolving imports with Promise.all |
videoClient.ts:24 | LOW | ✓ Borderline: performance suggestion, non-blocking |
Honest Analysis
FriendlyReviewer's full review
As posted on the PR — the complete output from FriendlyReviewer.
Hi there 👋
| 🌥️ Tech | 🌤️ Feat |
|---|---|
| 10 high, 3 medium, 5 low | 1 mitigated |
This MR converts the app-store registry from static namespace imports to async dynamic imports and makes the calendar/video/payment factories async. The mechanical conversion is largely correct, but it introduces several regressions: dynamic imports are awaited without error handling in key paths, causing unhandled rejections; multiple forEach(async ...) loops are fire-and-forget and now race with request completion, which can silently drop calendar/video deletions; getCalendarCredentials was left synchronous, defeating lazy-loading and returning Promise-typed calendars; and a few pre-existing logic bugs (lastAttendee condition, paid-cancel early return) are now in scope. These issues should be addressed before merge.
These might need a close look
- 🔴 packages/app-store/_utils/getCalendar.ts (L15)
Theawait appStore[...]for the dynamic import has no error handling; a failing module import rejectsgetCalendarand can abort wholePromise.allbatches in callers. Wrap in try/catch and return null on failure. - 🔴 packages/core/CalendarManager.ts (L28)
getCalendarCredentials remains synchronous, so it eagerly fires all dynamic imports and returnscalendaras a Promise, causing unhandled rejections and defeating lazy loading. Make it async and await the calendar resolution. - 🔴 packages/core/videoClient.ts (L26)
Unguardedawait appStore[...]can reject on module-load failure and bypass the Daily-video fallback in createMeeting; wrap it or the call sites in try/catch so the fallback runs. - 🔴 packages/lib/payment/deletePayment.ts (L16)
The dynamic import await has no try/catch; a failed import throws out of deletePayment instead of returning false. Add try/catch and match the error contract. - 🔴 packages/lib/payment/handlePayment.ts (L35)
Optional chaining only guardsapps, so a missingapps[appId]throws a TypeError when accessing.price/.currency. Useapps?.[appId]?.priceor resolve through the legacy helpers. - 🔴 packages/trpc/server/routers/viewer/bookings.tsx (L553)
TheforEach(async ...)loop is fire-and-forget; calendar/video deletions may not complete (or may never run in serverless) and rejections go unhandled. Collect and await the promises with Promise.all. - 🔴 packages/features/bookings/lib/handleCancelBooking.ts (L460)
In the recurring-event cancel path, async push intoapiDeletesraces with the subsequentPromise.all(prismaPromises.concat(apiDeletes))so some deletions can be missed. Convert to collecting promises and awaiting. - 🔴 packages/features/bookings/lib/handleCancelBooking.ts (L617)
The paid-cancel path doesawait apiDeletes(a bare array) which resolves immediately without waiting for the contained promises; should beawait Promise.all(apiDeletes). - 🔴 packages/app-store/vital/lib/reschedule.ts (L125)
TheforEach(async ...)makes calendar/video cleanup fire-and-forget and the wrapping try/catch no longer catches errors inside async callbacks (they become unhandled rejections); rewrite withfor...of/Promise.allto await deletions and restore error logging. - 🔴 packages/app-store/wipemycalother/lib/reschedule.ts (L125)
TheforEach(async ...)makes calendar/video cleanup fire-and-forget and the wrapping try/catch no longer catches errors inside async callbacks (they become unhandled rejections); rewrite withfor...of/Promise.allto await deletions and restore error logging.
Worth checking
- 🟡 packages/features/bookings/lib/handleCancelBooking.ts (L589)
When the payment app is unavailable, the paid-cancel handler returns null, abandoning cancellation and skipping cleanup/webhook while reporting success; should throw so the client gets a failure. - 🟡 packages/trpc/server/routers/viewer/bookings.tsx (L970)
The rejectBooking mutation returns null when the payment app is missing, leaving the booking PENDING and never sending declined emails, breaking the response contract; should throw a TRPCError. - 🟡 packages/features/bookings/lib/handleCancelBooking.ts (L223)
ThelastAttendeeconditionlength < 0is never true, so seat-cancellation never cleans up calendar/video for the final attendee; should be=== 0.
Small things (take or leave)
- 🔵 packages/app-store/_utils/getCalendar.ts (L15)
The key derivationcalendarType.split("_").join("")combined withas keyof typeof appStoresilently maps unknown credential types to undefined, and there is no startup verification that every credential type resolves. Add a dev-time assertion orkey in appStorecheck. - 🔵 packages/app-store/_utils/getCalendar.ts (L22)
Type-unsafety ofnew CalendarService(credential)relies on structural compatibility across a union of constructors; consider typing the registry to enforce a shared construct signature. - 🔵 packages/app-store/_utils/getCalendar.ts (L20)
Logs the CalendarService class at info level on every lookup; should be debug or removed. - 🔵 packages/core/CalendarManager.ts (L142)
The performance mark is placed after awaiting all calendar module loads, so the import cost is excluded from the measured metric. Move the mark before the Promise.all. - 🔵 packages/core/videoClient.ts (L24)
Sequential module resolution in the for loop serializes cold-load latency across multiple credentials; consider resolving imports with Promise.all.
✅ Feature-level checklist
Partially covered
- ⚠️ Convert static app-store imports to async dynamic imports (lazy loading).
The registry now uses dynamicimport()and the factories await it, butgetCalendarCredentialsremains synchronous and eagerly loads all modules, and several fire-and-forget async call sites mean the async behavior is not reliably lazy/awaited. Error handling around dynamic imports is also incomplete.
FriendlyReviewer found both golden comments of this PR — recall 100% — and on both the underlying finding is a first-class one rather than a near-miss. Golden #1 maps exactly to the unguarded
await appStore[...]at getCalendar.ts L15, with the review adding the caller-side consequence the reference implies (a failing import aborts wholePromise.allbatches). Golden #2 maps to theforEach(async ...)at bookings.tsx L553, where the review goes beyond the reference comment by spelling out the serverless angle — deletions that may never run at all.The supplementary set (16) is dominated by genuine conversion regressions: the synchronous
getCalendarCredentials(CalendarManager.ts L28) defeats the entire lazy-loading goal of the PR, the unguarded awaits in videoClient and deletePayment break their error contracts, and the two handleCancelBooking items (L460 race, L617 bare-array await) are real unawaited-async bugs. These are the same failure family as the goldens, which explains the clean recall. The low-severity tail is weaker: getCalendar.ts L20 (info-level logging) and L22 (constructor typing) are nits that would not change a merge decision, and handleCancelBooking.ts L223 (lastAttendee) is pre-existing code pulled into scope by the verdict rather than by this PR.The verdict labels 10 findings HIGH, which overstates the material risk: most of these regressions only fire when a module import fails — a rare event in production — or in narrow cancel/refund paths, while the highest-impact consequence (deletions silently dropped on cancel) is genuinely severe. One gap is worth noting: the golden's example list includes payment refunds, and the review examines the payment helpers (deletePayment, handlePayment) but never the refund flow itself. Overall a strong recall performance, with severity inflation and some scope drift as the honest counterweights.