baa-conductor


baa-conductor / packages / codex-app-server / src
im_wower  ·  2026-03-22

transport.ts

  1declare const WebSocket:
  2  | undefined
  3  | (new (url: string) => CodexAppServerWebSocket);
  4
  5export interface CodexAppServerTransportHandlers {
  6  onClose(error?: Error): void;
  7  onMessage(message: string): void;
  8}
  9
 10export interface CodexAppServerTransport {
 11  close(): Promise<void>;
 12  connect(handlers: CodexAppServerTransportHandlers): Promise<void>;
 13  send(message: string): Promise<void>;
 14}
 15
 16export interface CodexAppServerWebSocketMessageEvent {
 17  data: unknown;
 18}
 19
 20export interface CodexAppServerWebSocketCloseEvent {
 21  code?: number;
 22  reason?: string;
 23}
 24
 25export interface CodexAppServerWebSocket {
 26  onclose: ((event: CodexAppServerWebSocketCloseEvent) => void) | null;
 27  onerror: ((event: unknown) => void) | null;
 28  onmessage: ((event: CodexAppServerWebSocketMessageEvent) => void) | null;
 29  onopen: (() => void) | null;
 30  close(code?: number, reason?: string): void;
 31  send(data: string): void;
 32}
 33
 34export type CodexAppServerWebSocketFactory = (url: string) => CodexAppServerWebSocket;
 35
 36export interface CodexAppServerWebSocketTransportConfig {
 37  url: string;
 38  closeCode?: number;
 39  closeReason?: string;
 40  createSocket?: CodexAppServerWebSocketFactory;
 41}
 42
 43function toError(cause: unknown, fallback: string): Error {
 44  if (cause instanceof Error) {
 45    return cause;
 46  }
 47
 48  if (typeof cause === "string" && cause !== "") {
 49    return new Error(cause);
 50  }
 51
 52  return new Error(fallback);
 53}
 54
 55function resolveDefaultWebSocketFactory(): CodexAppServerWebSocketFactory {
 56  if (typeof WebSocket !== "function") {
 57    throw new Error(
 58      "Global WebSocket is unavailable. Pass createSocket to createCodexAppServerWebSocketTransport."
 59    );
 60  }
 61
 62  return (url: string) => new WebSocket(url);
 63}
 64
 65export function createCodexAppServerWebSocketTransport(
 66  config: CodexAppServerWebSocketTransportConfig
 67): CodexAppServerTransport {
 68  let handlers: CodexAppServerTransportHandlers | null = null;
 69  let socket: CodexAppServerWebSocket | null = null;
 70  let state: "idle" | "connecting" | "open" | "closed" = "idle";
 71
 72  return {
 73    async connect(nextHandlers: CodexAppServerTransportHandlers): Promise<void> {
 74      if (state === "open") {
 75        handlers = nextHandlers;
 76        return;
 77      }
 78
 79      if (state === "connecting") {
 80        throw new Error("Codex app-server transport is already connecting.");
 81      }
 82
 83      if (state === "closed") {
 84        throw new Error("Codex app-server transport is already closed.");
 85      }
 86
 87      handlers = nextHandlers;
 88      state = "connecting";
 89
 90      const createSocket = config.createSocket ?? resolveDefaultWebSocketFactory();
 91      const activeSocket = createSocket(config.url);
 92      socket = activeSocket;
 93
 94      await new Promise<void>((resolve, reject) => {
 95        let settled = false;
 96
 97        activeSocket.onopen = () => {
 98          if (settled) {
 99            return;
100          }
101
102          settled = true;
103          state = "open";
104          resolve();
105        };
106
107        activeSocket.onmessage = (event) => {
108          const message =
109            typeof event.data === "string"
110              ? event.data
111              : event.data === undefined || event.data === null
112                ? ""
113                : String(event.data);
114
115          handlers?.onMessage(message);
116        };
117
118        activeSocket.onerror = (event) => {
119          const error = toError(event, `Failed to connect to Codex app-server at ${config.url}.`);
120
121          if (!settled) {
122            settled = true;
123            state = "closed";
124            socket = null;
125            reject(error);
126            return;
127          }
128
129          state = "closed";
130          socket = null;
131          handlers?.onClose(error);
132        };
133
134        activeSocket.onclose = (event) => {
135          const code = event.code ?? 1000;
136          const reason = event.reason ? `: ${event.reason}` : "";
137          const error = new Error(`Codex app-server WebSocket closed (${code})${reason}`);
138
139          if (!settled) {
140            settled = true;
141            state = "closed";
142            socket = null;
143            reject(error);
144            return;
145          }
146
147          state = "closed";
148          socket = null;
149          handlers?.onClose(error);
150        };
151      });
152    },
153
154    async send(message: string): Promise<void> {
155      if (state !== "open" || socket === null) {
156        throw new Error("Codex app-server transport is not connected.");
157      }
158
159      socket.send(message);
160    },
161
162    async close(): Promise<void> {
163      if (socket === null) {
164        state = "closed";
165        handlers = null;
166        return;
167      }
168
169      const activeSocket = socket;
170      socket = null;
171      state = "closed";
172      handlers = null;
173      activeSocket.close(config.closeCode ?? 1000, config.closeReason ?? "client closing");
174    }
175  };
176}