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}