From 5e35e6a95ffbabef643be51cea758f1ff580a714 Mon Sep 17 00:00:00 2001 From: Peter Steinberger Date: Fri, 27 Mar 2026 01:04:48 +0000 Subject: [PATCH] fix: lazy-load zca-js at the zalouser runtime boundary --- extensions/zalouser/src/zalo-js.ts | 6 ++--- extensions/zalouser/src/zca-client.test.ts | 28 ++++++++++++++++++++++ extensions/zalouser/src/zca-client.ts | 20 +++++++++++++--- 3 files changed, 48 insertions(+), 6 deletions(-) create mode 100644 extensions/zalouser/src/zca-client.test.ts diff --git a/extensions/zalouser/src/zalo-js.ts b/extensions/zalouser/src/zalo-js.ts index e8e6c3659f6..965413e3bde 100644 --- a/extensions/zalouser/src/zalo-js.ts +++ b/extensions/zalouser/src/zalo-js.ts @@ -26,7 +26,7 @@ import { type LoginQRCallbackEvent, type Message, type User, - Zalo, + createZalo, } from "./zca-client.js"; import { LoginQRCallbackEventType, ThreadType } from "./zca-constants.js"; @@ -619,7 +619,7 @@ async function ensureApi( if (!stored) { throw new Error(`No saved Zalo session for profile \"${profile}\"`); } - const zalo = new Zalo({ + const zalo = await createZalo({ logging: false, selfListen: false, }); @@ -1293,7 +1293,7 @@ export async function startZaloQrLogin(params: { let capturedCredentials: Omit | null = null; try { - const zalo = new Zalo({ logging: false, selfListen: false }); + const zalo = await createZalo({ logging: false, selfListen: false }); const api = await zalo.loginQR(undefined, (event: LoginQRCallbackEvent) => { const current = activeQrLogins.get(profile); if (!current || current.id !== login.id) { diff --git a/extensions/zalouser/src/zca-client.test.ts b/extensions/zalouser/src/zca-client.test.ts new file mode 100644 index 00000000000..a1fc7e610e1 --- /dev/null +++ b/extensions/zalouser/src/zca-client.test.ts @@ -0,0 +1,28 @@ +import { beforeEach, describe, expect, it, vi } from "vitest"; + +describe("zca-client runtime loading", () => { + beforeEach(() => { + vi.resetModules(); + vi.clearAllMocks(); + }); + + it("does not import zca-js until a session is created", async () => { + const runtimeFactory = vi.fn(() => ({ + Zalo: class MockZalo { + constructor(public readonly options?: { logging?: boolean; selfListen?: boolean }) {} + }, + })); + + vi.doMock("zca-js", runtimeFactory); + + const zcaClient = await import("./zca-client.js"); + expect(runtimeFactory).not.toHaveBeenCalled(); + + const client = await zcaClient.createZalo({ logging: false, selfListen: true }); + + expect(runtimeFactory).toHaveBeenCalledTimes(1); + expect(client).toMatchObject({ + options: { logging: false, selfListen: true }, + }); + }); +}); diff --git a/extensions/zalouser/src/zca-client.ts b/extensions/zalouser/src/zca-client.ts index bae0472fc09..2af9bf67d59 100644 --- a/extensions/zalouser/src/zca-client.ts +++ b/extensions/zalouser/src/zca-client.ts @@ -1,4 +1,3 @@ -import * as zcaJsRuntime from "zca-js"; import { LoginQRCallbackEventType, Reactions, @@ -7,9 +6,18 @@ import { type Style, } from "./zca-constants.js"; -const zcaJs = zcaJsRuntime as unknown as { +type ZcaJsRuntime = { Zalo: unknown; }; +let zcaJsRuntimePromise: Promise | null = null; + +async function loadZcaJsRuntime(): Promise { + // Keep zca-js behind a runtime boundary so bundled metadata/contracts can load + // without resolving its optional WebSocket dependency tree. + zcaJsRuntimePromise ??= import("zca-js").then((mod) => mod as unknown as ZcaJsRuntime); + return await zcaJsRuntimePromise; +} + export { LoginQRCallbackEventType, Reactions, TextStyle, ThreadType }; export type { Style }; @@ -242,4 +250,10 @@ type ZaloCtor = new (options?: { logging?: boolean; selfListen?: boolean }) => { ): Promise; }; -export const Zalo = zcaJs.Zalo as unknown as ZaloCtor; +export async function createZalo( + options?: ConstructorParameters[0], +): Promise> { + const zcaJs = await loadZcaJsRuntime(); + const Zalo = zcaJs.Zalo as unknown as ZaloCtor; + return new Zalo(options); +}