Skip to content

Commit 191db51

Browse files
perf: optimize video adapter imports to avoid loading entire app store (calcom#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 calcom#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 calcom#23435 Co-Authored-By: keith@cal.com <keithwillcode@gmail.com> --------- Co-authored-by: Devin AI <158243242+devin-ai-integration[bot]@users.noreply.github.com>
1 parent bfacd8a commit 191db51

5 files changed

Lines changed: 159 additions & 65 deletions

File tree

apps/web/test/utils/bookingScenario/bookingScenario.ts

Lines changed: 85 additions & 54 deletions
Original file line numberDiff line numberDiff line change
@@ -51,6 +51,22 @@ vi.mock("@calcom/app-store/calendar.services.generated", () => ({
5151
},
5252
}));
5353

54+
const mockVideoAdapterRegistry: Record<string, any> = {};
55+
56+
vi.mock("@calcom/app-store/video.adapters.generated", () => ({
57+
VideoApiAdapterMap: new Proxy(
58+
{},
59+
{
60+
get(target, prop) {
61+
if (typeof prop === "string" && mockVideoAdapterRegistry[prop]) {
62+
return mockVideoAdapterRegistry[prop];
63+
}
64+
return Promise.resolve({ default: vi.fn() });
65+
},
66+
}
67+
),
68+
}));
69+
5470
// We don't need to test it. Also, it causes Formbricks error when imported
5571
vi.mock("@calcom/lib/raqb/findTeamMembersMatchingAttributeLogic", () => ({
5672
default: {},
@@ -1995,6 +2011,61 @@ export function mockVideoApp({
19952011
const updateMeetingCalls: any[] = [];
19962012
// eslint-disable-next-line @typescript-eslint/no-explicit-any
19972013
const deleteMeetingCalls: any[] = [];
2014+
2015+
const mockVideoAdapter = (credential: any) => {
2016+
return {
2017+
// eslint-disable-next-line @typescript-eslint/no-explicit-any
2018+
createMeeting: (...rest: any[]) => {
2019+
if (creationCrash) {
2020+
throw new Error("MockVideoApiAdapter.createMeeting fake error");
2021+
}
2022+
createMeetingCalls.push({
2023+
credential,
2024+
args: rest,
2025+
});
2026+
2027+
return Promise.resolve({
2028+
type: appStoreMetadata[metadataLookupKey as keyof typeof appStoreMetadata].type,
2029+
...videoMeetingData,
2030+
});
2031+
},
2032+
// eslint-disable-next-line @typescript-eslint/no-explicit-any
2033+
updateMeeting: async (...rest: any[]) => {
2034+
if (updationCrash) {
2035+
throw new Error("MockVideoApiAdapter.updateMeeting fake error");
2036+
}
2037+
const [bookingRef, calEvent] = rest;
2038+
updateMeetingCalls.push({
2039+
credential,
2040+
args: rest,
2041+
});
2042+
if (!bookingRef.type) {
2043+
throw new Error("bookingRef.type is not defined");
2044+
}
2045+
if (!calEvent.organizer) {
2046+
throw new Error("calEvent.organizer is not defined");
2047+
}
2048+
log.silly("MockVideoApiAdapter.updateMeeting", JSON.stringify({ bookingRef, calEvent }));
2049+
return Promise.resolve({
2050+
type: appStoreMetadata[metadataLookupKey as keyof typeof appStoreMetadata].type,
2051+
...videoMeetingData,
2052+
});
2053+
},
2054+
// eslint-disable-next-line @typescript-eslint/no-explicit-any
2055+
deleteMeeting: async (...rest: any[]) => {
2056+
log.silly("MockVideoApiAdapter.deleteMeeting", JSON.stringify(rest));
2057+
deleteMeetingCalls.push({
2058+
credential,
2059+
args: rest,
2060+
});
2061+
},
2062+
};
2063+
};
2064+
2065+
mockVideoAdapterRegistry[appStoreLookupKey] = Promise.resolve({
2066+
default: mockVideoAdapter,
2067+
});
2068+
19982069
// eslint-disable-next-line @typescript-eslint/ban-ts-comment
19992070
//@ts-ignore
20002071
appStoreMock.default[appStoreLookupKey as keyof typeof appStoreMock.default].mockImplementation(() => {
@@ -2003,59 +2074,12 @@ export function mockVideoApp({
20032074
lib: {
20042075
// eslint-disable-next-line @typescript-eslint/ban-ts-comment
20052076
//@ts-ignore
2006-
VideoApiAdapter: (credential) => {
2007-
return {
2008-
// eslint-disable-next-line @typescript-eslint/no-explicit-any
2009-
createMeeting: (...rest: any[]) => {
2010-
if (creationCrash) {
2011-
throw new Error("MockVideoApiAdapter.createMeeting fake error");
2012-
}
2013-
createMeetingCalls.push({
2014-
credential,
2015-
args: rest,
2016-
});
2017-
2018-
return Promise.resolve({
2019-
type: appStoreMetadata[metadataLookupKey as keyof typeof appStoreMetadata].type,
2020-
...videoMeetingData,
2021-
});
2022-
},
2023-
// eslint-disable-next-line @typescript-eslint/no-explicit-any
2024-
updateMeeting: async (...rest: any[]) => {
2025-
if (updationCrash) {
2026-
throw new Error("MockVideoApiAdapter.updateMeeting fake error");
2027-
}
2028-
const [bookingRef, calEvent] = rest;
2029-
updateMeetingCalls.push({
2030-
credential,
2031-
args: rest,
2032-
});
2033-
if (!bookingRef.type) {
2034-
throw new Error("bookingRef.type is not defined");
2035-
}
2036-
if (!calEvent.organizer) {
2037-
throw new Error("calEvent.organizer is not defined");
2038-
}
2039-
log.silly("MockVideoApiAdapter.updateMeeting", JSON.stringify({ bookingRef, calEvent }));
2040-
return Promise.resolve({
2041-
type: appStoreMetadata[metadataLookupKey as keyof typeof appStoreMetadata].type,
2042-
...videoMeetingData,
2043-
});
2044-
},
2045-
// eslint-disable-next-line @typescript-eslint/no-explicit-any
2046-
deleteMeeting: async (...rest: any[]) => {
2047-
log.silly("MockVideoApiAdapter.deleteMeeting", JSON.stringify(rest));
2048-
deleteMeetingCalls.push({
2049-
credential,
2050-
args: rest,
2051-
});
2052-
},
2053-
};
2054-
},
2077+
VideoApiAdapter: mockVideoAdapter,
20552078
},
20562079
});
20572080
});
20582081
});
2082+
20592083
return {
20602084
createMeetingCalls,
20612085
updateMeetingCalls,
@@ -2121,6 +2145,17 @@ export function mockErrorOnVideoMeetingCreation({
21212145
appStoreLookupKey?: string;
21222146
}) {
21232147
appStoreLookupKey = appStoreLookupKey || metadataLookupKey;
2148+
2149+
const mockErrorAdapter = () => ({
2150+
createMeeting: () => {
2151+
throw new MockError("Error creating Video meeting");
2152+
},
2153+
});
2154+
2155+
mockVideoAdapterRegistry[appStoreLookupKey] = Promise.resolve({
2156+
default: mockErrorAdapter,
2157+
});
2158+
21242159
// eslint-disable-next-line @typescript-eslint/ban-ts-comment
21252160
//@ts-ignore
21262161
appStoreMock.default[appStoreLookupKey].mockImplementation(() => {
@@ -2129,11 +2164,7 @@ export function mockErrorOnVideoMeetingCreation({
21292164
lib: {
21302165
// eslint-disable-next-line @typescript-eslint/ban-ts-comment
21312166
//@ts-ignore
2132-
VideoApiAdapter: () => ({
2133-
createMeeting: () => {
2134-
throw new MockError("Error creating Video meeting");
2135-
},
2136-
}),
2167+
VideoApiAdapter: mockErrorAdapter,
21372168
},
21382169
});
21392170
});

packages/app-store-cli/src/build.ts

Lines changed: 35 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -433,6 +433,40 @@ function generateFiles() {
433433

434434
paymentOutput.push(...paymentServices);
435435

436+
const videoOutput = [];
437+
const videoAdapters = getExportedObject(
438+
"VideoApiAdapterMap",
439+
{
440+
importConfig: {
441+
fileToBeImported: "lib/VideoApiAdapter.ts",
442+
importName: "default",
443+
},
444+
lazyImport: true,
445+
},
446+
(app: App) => {
447+
return fs.existsSync(path.join(APP_STORE_PATH, app.path, "lib/VideoApiAdapter.ts"));
448+
}
449+
);
450+
451+
const videoExportLineIndex = videoAdapters.findIndex((line) =>
452+
line.startsWith("export const VideoApiAdapterMap")
453+
);
454+
if (videoExportLineIndex !== -1) {
455+
const exportLine = videoAdapters[videoExportLineIndex];
456+
const objectContent = videoAdapters.slice(videoExportLineIndex + 1, -1);
457+
458+
videoOutput.push(
459+
exportLine.replace(
460+
"export const VideoApiAdapterMap = {",
461+
"export const VideoApiAdapterMap = process.env.NEXT_PUBLIC_IS_E2E ? {} : {"
462+
),
463+
...objectContent,
464+
"};"
465+
);
466+
} else {
467+
videoOutput.push(...videoAdapters);
468+
}
469+
436470
const banner = `/**
437471
This file is autogenerated using the command \`yarn app-store:build --watch\`.
438472
Don't modify this file manually.
@@ -449,6 +483,7 @@ function generateFiles() {
449483
["crm.apps.generated.ts", crmOutput],
450484
["calendar.services.generated.ts", calendarOutput],
451485
["payment.services.generated.ts", paymentOutput],
486+
["video.adapters.generated.ts", videoOutput],
452487
];
453488
filesToGenerate.forEach(([fileName, output]) => {
454489
fs.writeFileSync(`${APP_STORE_PATH}/${fileName}`, formatOutput(`${banner}${output.join("\n")}`));
Lines changed: 19 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,19 @@
1+
/**
2+
This file is autogenerated using the command `yarn app-store:build --watch`.
3+
Don't modify this file manually.
4+
**/
5+
export const VideoApiAdapterMap = process.env.NEXT_PUBLIC_IS_E2E
6+
? {}
7+
: {
8+
dailyvideo: import("./dailyvideo/lib/VideoApiAdapter"),
9+
huddle01video: import("./huddle01video/lib/VideoApiAdapter"),
10+
jelly: import("./jelly/lib/VideoApiAdapter"),
11+
jitsivideo: import("./jitsivideo/lib/VideoApiAdapter"),
12+
nextcloudtalk: import("./nextcloudtalk/lib/VideoApiAdapter"),
13+
office365video: import("./office365video/lib/VideoApiAdapter"),
14+
shimmervideo: import("./shimmervideo/lib/VideoApiAdapter"),
15+
sylapsvideo: import("./sylapsvideo/lib/VideoApiAdapter"),
16+
tandemvideo: import("./tandemvideo/lib/VideoApiAdapter"),
17+
webex: import("./webex/lib/VideoApiAdapter"),
18+
zoomvideo: import("./zoomvideo/lib/VideoApiAdapter"),
19+
};

packages/lib/videoClient.ts

Lines changed: 8 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,7 @@ import { v5 as uuidv5 } from "uuid";
33

44
import { getDailyAppKeys } from "@calcom/app-store/dailyvideo/lib/getDailyAppKeys";
55
import { DailyLocationType } from "@calcom/app-store/locations";
6+
import { VideoApiAdapterMap } from "@calcom/app-store/video.adapters.generated";
67
import { sendBrokenIntegrationEmail } from "@calcom/emails";
78
import { getUid } from "@calcom/lib/CalEventParser";
89
import logger from "@calcom/lib/logger";
@@ -27,23 +28,21 @@ const getVideoAdapters = async (withCredentials: CredentialPayload[]): Promise<V
2728
const appName = cred.type.split("_").join(""); // Transform `zoom_video` to `zoomvideo`;
2829
log.silly("Getting video adapter for", safeStringify({ appName, cred: getPiiFreeCredential(cred) }));
2930

30-
const appStore = await import("@calcom/app-store").then((m) => m.default);
31-
const appImportFn = appStore[appName as keyof typeof appStore];
31+
const videoAdapterImport = VideoApiAdapterMap[appName as keyof typeof VideoApiAdapterMap];
3232

33-
// 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.
34-
const app = appImportFn ? await appImportFn() : null;
35-
36-
if (!app) {
33+
if (!videoAdapterImport) {
3734
log.error(`Couldn't get adapter for ${appName}`);
3835
continue;
3936
}
4037

41-
if ("lib" in app && "VideoApiAdapter" in app.lib) {
42-
const makeVideoApiAdapter = app.lib.VideoApiAdapter as VideoApiAdapterFactory;
38+
const videoAdapterModule = await videoAdapterImport;
39+
const makeVideoApiAdapter = videoAdapterModule.default as VideoApiAdapterFactory;
40+
41+
if (makeVideoApiAdapter) {
4342
const videoAdapter = makeVideoApiAdapter(cred);
4443
videoAdapters.push(videoAdapter);
4544
} else {
46-
log.error(`App ${appName} doesn't have 'lib.VideoApiAdapter' defined`);
45+
log.error(`App ${appName} doesn't have a default VideoApiAdapter export`);
4746
}
4847
}
4948

packages/trpc/server/routers/viewer/calVideo/getMeetingInformation.handler.ts

Lines changed: 12 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
import DailyVideoApiAdapter from "@calcom/app-store/dailyvideo/lib/VideoApiAdapter";
1+
import { VideoApiAdapterMap } from "@calcom/app-store/video.adapters.generated";
22
import type { TrpcSessionUser } from "@calcom/trpc/server/types";
33

44
import { TRPCError } from "@trpc/server";
@@ -16,7 +16,17 @@ export const getMeetingInformationHandler = async ({ ctx: _ctx, input }: GetMeet
1616
const { roomName } = input;
1717

1818
try {
19-
const videoApiAdapter = DailyVideoApiAdapter();
19+
const dailyVideoAdapterImport = VideoApiAdapterMap["dailyvideo"];
20+
if (!dailyVideoAdapterImport) {
21+
throw new TRPCError({
22+
code: "BAD_REQUEST",
23+
message: "Daily video adapter not available",
24+
});
25+
}
26+
27+
const dailyVideoAdapterModule = await dailyVideoAdapterImport;
28+
const videoApiAdapter = dailyVideoAdapterModule.default();
29+
2030
if (!videoApiAdapter || !videoApiAdapter.getMeetingInformation) {
2131
throw new TRPCError({
2232
code: "BAD_REQUEST",

0 commit comments

Comments
 (0)