perf: optimize video adapter imports to avoid loading entire app store (#23435)

* 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 <keithwillcode@gmail.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 <keithwillcode@gmail.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 <keithwillcode@gmail.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 <keithwillcode@gmail.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 <keithwillcode@gmail.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 <keithwillcode@gmail.com>

---------

Co-authored-by: Devin AI <158243242+devin-ai-integration[bot]@users.noreply.github.com>
This commit is contained in:
Keith Williams
2025-08-29 14:21:05 +09:00
committed by GitHub
co-authored by Devin AI
parent bfacd8a780
commit 191db5104a
5 changed files with 159 additions and 65 deletions
@@ -51,6 +51,22 @@ vi.mock("@calcom/app-store/calendar.services.generated", () => ({
},
}));
const mockVideoAdapterRegistry: Record<string, any> = {};
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,
},
});
});
+35
View File
@@ -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")}`));
@@ -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"),
};
+8 -9
View File
@@ -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<V
const appName = cred.type.split("_").join(""); // Transform `zoom_video` to `zoomvideo`;
log.silly("Getting video adapter for", safeStringify({ appName, cred: getPiiFreeCredential(cred) }));
const appStore = await import("@calcom/app-store").then((m) => 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`);
}
}
@@ -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",