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:
co-authored by
Devin AI
parent
bfacd8a780
commit
191db5104a
@@ -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,
|
||||
},
|
||||
});
|
||||
});
|
||||
|
||||
@@ -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"),
|
||||
};
|
||||
@@ -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",
|
||||
|
||||
Reference in New Issue
Block a user