From 191db5104ae2ad5dd8a6f13e8603c7463fbebf66 Mon Sep 17 00:00:00 2001 From: Keith Williams Date: Fri, 29 Aug 2025 02:21:05 -0300 Subject: [PATCH] perf: optimize video adapter imports to avoid loading entire app store (#23435) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * perf: optimize video adapter imports to avoid loading entire app store - Creates VideoApiAdapterMap with lazy imports for 12 video services - Updates getVideoAdapters function to use VideoApiAdapterMap instead of dynamic app store imports - Preserves zoom app name parsing logic (zoom_video → zoomvideo) - Follows same optimization pattern as calendar, analytics, and payment services - Reduces bundle size by avoiding import of 100+ apps when only video functionality needed Affected files: - packages/app-store-cli/src/build.ts: Added video service generation logic - packages/lib/videoClient.ts: Updated to use VideoApiAdapterMap - packages/features/bookings/lib/handleCancelBooking.ts: Updated FAKE_DAILY_CREDENTIAL import - packages/lib/EventManager.ts: Updated FAKE_DAILY_CREDENTIAL import - packages/trpc/server/routers/viewer/calVideo/getMeetingInformation.handler.ts: Updated to use VideoApiAdapterMap - apps/web/lib/video/[uid]/getServerSideProps.ts: Updated daily video function imports - packages/app-store/video.services.generated.ts: Generated video adapter map with re-exports Co-Authored-By: keith@cal.com * fix: add missing re-exports to video.services.generated.ts - Updates build.ts to include FAKE_DAILY_CREDENTIAL and other daily video function re-exports - Fixes type errors in EventManager.ts and other files importing from video.services.generated - Ensures video adapter refactoring maintains all existing functionality Co-Authored-By: keith@cal.com * fix: update video adapter test mocks to work with VideoApiAdapterMap - Creates global mockVideoAdapterRegistry for dynamic video adapter mocks - Uses Proxy in vi.mock for VideoApiAdapterMap to return registered mocks - Updates mockVideoApp and mockErrorOnVideoMeetingCreation to register mocks - Fixes unit test failures in booking scenario tests - Ensures video meeting operations (createMeeting, updateMeeting, deleteMeeting) work correctly Co-Authored-By: keith@cal.com * fix: remove re-exports from video.services.generated.ts and revert imports - Remove re-export block from video.services.generated.ts as requested - Revert imports back to pull directly from dailyvideo/lib/VideoApiAdapter - Update build.ts to not generate the re-exports - Maintains all existing functionality while addressing GitHub feedback Co-Authored-By: keith@cal.com * fix: rename video.services.generated.ts to video.adapters.generated.ts - Updates build.ts to generate video.adapters.generated.ts instead of video.services.generated.ts - Updates all import statements to use new filename - Removes unnecessary mock exports from bookingScenario.ts (FAKE_DAILY_CREDENTIAL, etc.) - Addresses GitHub comments from @keithwillcode on PR #23435 The terminology change from 'services map' to 'adapters map' better reflects the actual content (video adapters, not services) and maintains consistency with the established refactoring pattern. Co-Authored-By: keith@cal.com * refactor: rename videoServices to videoAdapters and simplify return statement - Rename variable from videoServices to videoAdapters for consistency - Remove unnecessary const variable and return directly in same line - Addresses GitHub comments from @keithwillcode on PR #23435 Co-Authored-By: keith@cal.com --------- Co-authored-by: Devin AI <158243242+devin-ai-integration[bot]@users.noreply.github.com> --- .../utils/bookingScenario/bookingScenario.ts | 139 +++++++++++------- packages/app-store-cli/src/build.ts | 35 +++++ .../app-store/video.adapters.generated.ts | 19 +++ packages/lib/videoClient.ts | 17 +-- .../calVideo/getMeetingInformation.handler.ts | 14 +- 5 files changed, 159 insertions(+), 65 deletions(-) create mode 100644 packages/app-store/video.adapters.generated.ts diff --git a/apps/web/test/utils/bookingScenario/bookingScenario.ts b/apps/web/test/utils/bookingScenario/bookingScenario.ts index 13008b8301..396642618d 100644 --- a/apps/web/test/utils/bookingScenario/bookingScenario.ts +++ b/apps/web/test/utils/bookingScenario/bookingScenario.ts @@ -51,6 +51,22 @@ vi.mock("@calcom/app-store/calendar.services.generated", () => ({ }, })); +const mockVideoAdapterRegistry: Record = {}; + +vi.mock("@calcom/app-store/video.adapters.generated", () => ({ + VideoApiAdapterMap: new Proxy( + {}, + { + get(target, prop) { + if (typeof prop === "string" && mockVideoAdapterRegistry[prop]) { + return mockVideoAdapterRegistry[prop]; + } + return Promise.resolve({ default: vi.fn() }); + }, + } + ), +})); + // We don't need to test it. Also, it causes Formbricks error when imported vi.mock("@calcom/lib/raqb/findTeamMembersMatchingAttributeLogic", () => ({ default: {}, @@ -1995,6 +2011,61 @@ export function mockVideoApp({ const updateMeetingCalls: any[] = []; // eslint-disable-next-line @typescript-eslint/no-explicit-any const deleteMeetingCalls: any[] = []; + + const mockVideoAdapter = (credential: any) => { + return { + // eslint-disable-next-line @typescript-eslint/no-explicit-any + createMeeting: (...rest: any[]) => { + if (creationCrash) { + throw new Error("MockVideoApiAdapter.createMeeting fake error"); + } + createMeetingCalls.push({ + credential, + args: rest, + }); + + return Promise.resolve({ + type: appStoreMetadata[metadataLookupKey as keyof typeof appStoreMetadata].type, + ...videoMeetingData, + }); + }, + // eslint-disable-next-line @typescript-eslint/no-explicit-any + updateMeeting: async (...rest: any[]) => { + if (updationCrash) { + throw new Error("MockVideoApiAdapter.updateMeeting fake error"); + } + const [bookingRef, calEvent] = rest; + updateMeetingCalls.push({ + credential, + args: rest, + }); + if (!bookingRef.type) { + throw new Error("bookingRef.type is not defined"); + } + if (!calEvent.organizer) { + throw new Error("calEvent.organizer is not defined"); + } + log.silly("MockVideoApiAdapter.updateMeeting", JSON.stringify({ bookingRef, calEvent })); + return Promise.resolve({ + type: appStoreMetadata[metadataLookupKey as keyof typeof appStoreMetadata].type, + ...videoMeetingData, + }); + }, + // eslint-disable-next-line @typescript-eslint/no-explicit-any + deleteMeeting: async (...rest: any[]) => { + log.silly("MockVideoApiAdapter.deleteMeeting", JSON.stringify(rest)); + deleteMeetingCalls.push({ + credential, + args: rest, + }); + }, + }; + }; + + mockVideoAdapterRegistry[appStoreLookupKey] = Promise.resolve({ + default: mockVideoAdapter, + }); + // eslint-disable-next-line @typescript-eslint/ban-ts-comment //@ts-ignore appStoreMock.default[appStoreLookupKey as keyof typeof appStoreMock.default].mockImplementation(() => { @@ -2003,59 +2074,12 @@ export function mockVideoApp({ lib: { // eslint-disable-next-line @typescript-eslint/ban-ts-comment //@ts-ignore - VideoApiAdapter: (credential) => { - return { - // eslint-disable-next-line @typescript-eslint/no-explicit-any - createMeeting: (...rest: any[]) => { - if (creationCrash) { - throw new Error("MockVideoApiAdapter.createMeeting fake error"); - } - createMeetingCalls.push({ - credential, - args: rest, - }); - - return Promise.resolve({ - type: appStoreMetadata[metadataLookupKey as keyof typeof appStoreMetadata].type, - ...videoMeetingData, - }); - }, - // eslint-disable-next-line @typescript-eslint/no-explicit-any - updateMeeting: async (...rest: any[]) => { - if (updationCrash) { - throw new Error("MockVideoApiAdapter.updateMeeting fake error"); - } - const [bookingRef, calEvent] = rest; - updateMeetingCalls.push({ - credential, - args: rest, - }); - if (!bookingRef.type) { - throw new Error("bookingRef.type is not defined"); - } - if (!calEvent.organizer) { - throw new Error("calEvent.organizer is not defined"); - } - log.silly("MockVideoApiAdapter.updateMeeting", JSON.stringify({ bookingRef, calEvent })); - return Promise.resolve({ - type: appStoreMetadata[metadataLookupKey as keyof typeof appStoreMetadata].type, - ...videoMeetingData, - }); - }, - // eslint-disable-next-line @typescript-eslint/no-explicit-any - deleteMeeting: async (...rest: any[]) => { - log.silly("MockVideoApiAdapter.deleteMeeting", JSON.stringify(rest)); - deleteMeetingCalls.push({ - credential, - args: rest, - }); - }, - }; - }, + VideoApiAdapter: mockVideoAdapter, }, }); }); }); + return { createMeetingCalls, updateMeetingCalls, @@ -2121,6 +2145,17 @@ export function mockErrorOnVideoMeetingCreation({ appStoreLookupKey?: string; }) { appStoreLookupKey = appStoreLookupKey || metadataLookupKey; + + const mockErrorAdapter = () => ({ + createMeeting: () => { + throw new MockError("Error creating Video meeting"); + }, + }); + + mockVideoAdapterRegistry[appStoreLookupKey] = Promise.resolve({ + default: mockErrorAdapter, + }); + // eslint-disable-next-line @typescript-eslint/ban-ts-comment //@ts-ignore appStoreMock.default[appStoreLookupKey].mockImplementation(() => { @@ -2129,11 +2164,7 @@ export function mockErrorOnVideoMeetingCreation({ lib: { // eslint-disable-next-line @typescript-eslint/ban-ts-comment //@ts-ignore - VideoApiAdapter: () => ({ - createMeeting: () => { - throw new MockError("Error creating Video meeting"); - }, - }), + VideoApiAdapter: mockErrorAdapter, }, }); }); diff --git a/packages/app-store-cli/src/build.ts b/packages/app-store-cli/src/build.ts index f6b8b988e1..e52cc73439 100644 --- a/packages/app-store-cli/src/build.ts +++ b/packages/app-store-cli/src/build.ts @@ -433,6 +433,40 @@ function generateFiles() { paymentOutput.push(...paymentServices); + const videoOutput = []; + const videoAdapters = getExportedObject( + "VideoApiAdapterMap", + { + importConfig: { + fileToBeImported: "lib/VideoApiAdapter.ts", + importName: "default", + }, + lazyImport: true, + }, + (app: App) => { + return fs.existsSync(path.join(APP_STORE_PATH, app.path, "lib/VideoApiAdapter.ts")); + } + ); + + const videoExportLineIndex = videoAdapters.findIndex((line) => + line.startsWith("export const VideoApiAdapterMap") + ); + if (videoExportLineIndex !== -1) { + const exportLine = videoAdapters[videoExportLineIndex]; + const objectContent = videoAdapters.slice(videoExportLineIndex + 1, -1); + + videoOutput.push( + exportLine.replace( + "export const VideoApiAdapterMap = {", + "export const VideoApiAdapterMap = process.env.NEXT_PUBLIC_IS_E2E ? {} : {" + ), + ...objectContent, + "};" + ); + } else { + videoOutput.push(...videoAdapters); + } + const banner = `/** This file is autogenerated using the command \`yarn app-store:build --watch\`. Don't modify this file manually. @@ -449,6 +483,7 @@ function generateFiles() { ["crm.apps.generated.ts", crmOutput], ["calendar.services.generated.ts", calendarOutput], ["payment.services.generated.ts", paymentOutput], + ["video.adapters.generated.ts", videoOutput], ]; filesToGenerate.forEach(([fileName, output]) => { fs.writeFileSync(`${APP_STORE_PATH}/${fileName}`, formatOutput(`${banner}${output.join("\n")}`)); diff --git a/packages/app-store/video.adapters.generated.ts b/packages/app-store/video.adapters.generated.ts new file mode 100644 index 0000000000..693e69168e --- /dev/null +++ b/packages/app-store/video.adapters.generated.ts @@ -0,0 +1,19 @@ +/** + This file is autogenerated using the command `yarn app-store:build --watch`. + Don't modify this file manually. +**/ +export const VideoApiAdapterMap = process.env.NEXT_PUBLIC_IS_E2E + ? {} + : { + dailyvideo: import("./dailyvideo/lib/VideoApiAdapter"), + huddle01video: import("./huddle01video/lib/VideoApiAdapter"), + jelly: import("./jelly/lib/VideoApiAdapter"), + jitsivideo: import("./jitsivideo/lib/VideoApiAdapter"), + nextcloudtalk: import("./nextcloudtalk/lib/VideoApiAdapter"), + office365video: import("./office365video/lib/VideoApiAdapter"), + shimmervideo: import("./shimmervideo/lib/VideoApiAdapter"), + sylapsvideo: import("./sylapsvideo/lib/VideoApiAdapter"), + tandemvideo: import("./tandemvideo/lib/VideoApiAdapter"), + webex: import("./webex/lib/VideoApiAdapter"), + zoomvideo: import("./zoomvideo/lib/VideoApiAdapter"), + }; diff --git a/packages/lib/videoClient.ts b/packages/lib/videoClient.ts index 3c474e4311..4025e0d0a9 100644 --- a/packages/lib/videoClient.ts +++ b/packages/lib/videoClient.ts @@ -3,6 +3,7 @@ import { v5 as uuidv5 } from "uuid"; import { getDailyAppKeys } from "@calcom/app-store/dailyvideo/lib/getDailyAppKeys"; import { DailyLocationType } from "@calcom/app-store/locations"; +import { VideoApiAdapterMap } from "@calcom/app-store/video.adapters.generated"; import { sendBrokenIntegrationEmail } from "@calcom/emails"; import { getUid } from "@calcom/lib/CalEventParser"; import logger from "@calcom/lib/logger"; @@ -27,23 +28,21 @@ const getVideoAdapters = async (withCredentials: CredentialPayload[]): Promise m.default); - const appImportFn = appStore[appName as keyof typeof appStore]; + const videoAdapterImport = VideoApiAdapterMap[appName as keyof typeof VideoApiAdapterMap]; - // Static Link Video Apps don't exist in packages/app-store/index.ts(it's manually maintained at the moment) and they aren't needed there anyway. - const app = appImportFn ? await appImportFn() : null; - - if (!app) { + if (!videoAdapterImport) { log.error(`Couldn't get adapter for ${appName}`); continue; } - if ("lib" in app && "VideoApiAdapter" in app.lib) { - const makeVideoApiAdapter = app.lib.VideoApiAdapter as VideoApiAdapterFactory; + const videoAdapterModule = await videoAdapterImport; + const makeVideoApiAdapter = videoAdapterModule.default as VideoApiAdapterFactory; + + if (makeVideoApiAdapter) { const videoAdapter = makeVideoApiAdapter(cred); videoAdapters.push(videoAdapter); } else { - log.error(`App ${appName} doesn't have 'lib.VideoApiAdapter' defined`); + log.error(`App ${appName} doesn't have a default VideoApiAdapter export`); } } diff --git a/packages/trpc/server/routers/viewer/calVideo/getMeetingInformation.handler.ts b/packages/trpc/server/routers/viewer/calVideo/getMeetingInformation.handler.ts index de12ba635b..9a70196ba5 100644 --- a/packages/trpc/server/routers/viewer/calVideo/getMeetingInformation.handler.ts +++ b/packages/trpc/server/routers/viewer/calVideo/getMeetingInformation.handler.ts @@ -1,4 +1,4 @@ -import DailyVideoApiAdapter from "@calcom/app-store/dailyvideo/lib/VideoApiAdapter"; +import { VideoApiAdapterMap } from "@calcom/app-store/video.adapters.generated"; import type { TrpcSessionUser } from "@calcom/trpc/server/types"; import { TRPCError } from "@trpc/server"; @@ -16,7 +16,17 @@ export const getMeetingInformationHandler = async ({ ctx: _ctx, input }: GetMeet const { roomName } = input; try { - const videoApiAdapter = DailyVideoApiAdapter(); + const dailyVideoAdapterImport = VideoApiAdapterMap["dailyvideo"]; + if (!dailyVideoAdapterImport) { + throw new TRPCError({ + code: "BAD_REQUEST", + message: "Daily video adapter not available", + }); + } + + const dailyVideoAdapterModule = await dailyVideoAdapterImport; + const videoApiAdapter = dailyVideoAdapterModule.default(); + if (!videoApiAdapter || !videoApiAdapter.getMeetingInformation) { throw new TRPCError({ code: "BAD_REQUEST",