icebear icebear0828 commited on
Commit
ea2be52
·
unverified ·
1 Parent(s): 257bec0

fix: dynamic import `ws` to prevent Electron ESM startup crash (#88)

Browse files

The `ws` package uses CJS `require('events')` internally. When esbuild
bundles the server as ESM for Electron, these dynamic requires fail at
startup with "Dynamic require of 'events' is not supported".

Switch to lazy dynamic `import("ws")` so the CJS dependency chain is
only resolved at first WebSocket use, not at module load time.

Also lazy-import `https-proxy-agent` for the same reason.

Closes #87

Co-authored-by: icebear0828 <icebear0828@users.noreply.github.com>

Files changed (1) hide show
  1. src/proxy/ws-transport.ts +28 -8
src/proxy/ws-transport.ts CHANGED
@@ -7,12 +7,27 @@
7
  * regardless of whether HTTP SSE or WebSocket was used.
8
  *
9
  * Used when `previous_response_id` is present — HTTP SSE does not support it.
 
 
 
 
 
10
  */
11
 
12
- import WebSocket from "ws";
13
- import { HttpsProxyAgent } from "https-proxy-agent";
14
  import type { CodexInputItem } from "./codex-api.js";
15
 
 
 
 
 
 
 
 
 
 
 
 
 
16
  /** Flat WebSocket message format expected by the Codex backend. */
17
  export interface WsCreateRequest {
18
  type: "response.create";
@@ -42,24 +57,29 @@ export interface WsCreateRequest {
42
  * The SSE format matches what parseStream() expects:
43
  * event: <type>\ndata: <json>\n\n
44
  */
45
- export function createWebSocketResponse(
46
  wsUrl: string,
47
  headers: Record<string, string>,
48
  request: WsCreateRequest,
49
  signal?: AbortSignal,
50
  proxyUrl?: string | null,
51
  ): Promise<Response> {
 
 
 
 
 
 
 
 
 
52
  return new Promise<Response>((resolve, reject) => {
53
  if (signal?.aborted) {
54
  reject(new Error("Aborted before WebSocket connect"));
55
  return;
56
  }
57
 
58
- const wsOpts: WebSocket.ClientOptions = { headers };
59
- if (proxyUrl) {
60
- wsOpts.agent = new HttpsProxyAgent(proxyUrl);
61
- }
62
- const ws = new WebSocket(wsUrl, wsOpts);
63
  const encoder = new TextEncoder();
64
  let controller: ReadableStreamDefaultController<Uint8Array> | null = null;
65
  let streamClosed = false;
 
7
  * regardless of whether HTTP SSE or WebSocket was used.
8
  *
9
  * Used when `previous_response_id` is present — HTTP SSE does not support it.
10
+ *
11
+ * The `ws` package is loaded lazily via dynamic import to avoid
12
+ * "Dynamic require of 'events' is not supported" errors when the
13
+ * backend is bundled as ESM for Electron (esbuild cannot convert
14
+ * ws's CJS require chain to ESM statics).
15
  */
16
 
 
 
17
  import type { CodexInputItem } from "./codex-api.js";
18
 
19
+ /** Cached ws module — loaded once on first use. */
20
+ let _WS: typeof import("ws").default | undefined;
21
+
22
+ /** Lazily load the `ws` package. */
23
+ async function getWS(): Promise<typeof import("ws").default> {
24
+ if (!_WS) {
25
+ const mod = await import("ws");
26
+ _WS = mod.default;
27
+ }
28
+ return _WS;
29
+ }
30
+
31
  /** Flat WebSocket message format expected by the Codex backend. */
32
  export interface WsCreateRequest {
33
  type: "response.create";
 
57
  * The SSE format matches what parseStream() expects:
58
  * event: <type>\ndata: <json>\n\n
59
  */
60
+ export async function createWebSocketResponse(
61
  wsUrl: string,
62
  headers: Record<string, string>,
63
  request: WsCreateRequest,
64
  signal?: AbortSignal,
65
  proxyUrl?: string | null,
66
  ): Promise<Response> {
67
+ const WS = await getWS();
68
+
69
+ // Lazy-import proxy agent only when needed
70
+ const wsOpts: ConstructorParameters<typeof WS>[2] = { headers };
71
+ if (proxyUrl) {
72
+ const { HttpsProxyAgent } = await import("https-proxy-agent");
73
+ wsOpts.agent = new HttpsProxyAgent(proxyUrl);
74
+ }
75
+
76
  return new Promise<Response>((resolve, reject) => {
77
  if (signal?.aborted) {
78
  reject(new Error("Aborted before WebSocket connect"));
79
  return;
80
  }
81
 
82
+ const ws = new WS(wsUrl, wsOpts);
 
 
 
 
83
  const encoder = new TextEncoder();
84
  let controller: ReadableStreamDefaultController<Uint8Array> | null = null;
85
  let streamClosed = false;