Skip to content

Commit d5b2ff3

Browse files
committed
use built-in global ws
1 parent 7154af6 commit d5b2ff3

File tree

8 files changed

+69
-77
lines changed

8 files changed

+69
-77
lines changed

common/api-review/ai.api.md

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -433,6 +433,10 @@ export class GenerativeModel extends AIModel {
433433
toolConfig?: ToolConfig;
434434
// (undocumented)
435435
tools?: Tool[];
436+
// Warning: (ae-forgotten-export) The symbol "WebSocketHandler" needs to be exported by the entry point index.d.ts
437+
//
438+
// (undocumented)
439+
ws: WebSocketHandler;
436440
}
437441

438442
// @public

package.json

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -159,6 +159,7 @@
159159
"typescript": "5.5.4",
160160
"watch": "1.0.2",
161161
"webpack": "5.98.0",
162+
"ws": "8.18.3",
162163
"yargs": "17.7.2"
163164
}
164165
}

packages/ai/package.json

Lines changed: 1 addition & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -63,8 +63,7 @@
6363
"rollup": "2.79.2",
6464
"rollup-plugin-replace": "2.2.0",
6565
"rollup-plugin-typescript2": "0.36.0",
66-
"typescript": "5.5.4",
67-
"ws": "8.18.3"
66+
"typescript": "5.5.4"
6867
},
6968
"repository": {
7069
"directory": "packages/ai",

packages/ai/src/platform/browser/websocket.test.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -270,4 +270,4 @@ describe('BrowserWebSocketHandler', () => {
270270
expect(mockWebSocket.readyState).to.equal(MockBrowserWebSocket.CLOSED);
271271
});
272272
});
273-
});
273+
});

packages/ai/src/platform/browser/websocket.ts

Lines changed: 16 additions & 19 deletions
Original file line numberDiff line numberDiff line change
@@ -20,16 +20,16 @@ import { AIErrorCode } from '../../types';
2020
import { WebSocketHandler } from '../websocket';
2121

2222
export function createWebSocketHandler(): WebSocketHandler {
23-
if (typeof WebSocket !== 'undefined') {
24-
return new BrowserWebSocketHandler();
25-
} else {
23+
if (typeof WebSocket === 'undefined') {
2624
throw new AIError(
2725
AIErrorCode.UNSUPPORTED,
2826
'The WebSocket API is not available in this browser-like environment. ' +
2927
'The "Live" feature is not supported here. It is supported in ' +
30-
'standard browser windows, Web Workers with WebSocket support, and Node >= 22.'
28+
'modern browser windows, Web Workers with WebSocket support, and Node >= 22.'
3129
);
3230
}
31+
32+
return new BrowserWebSocketHandler();
3333
}
3434

3535
/**
@@ -43,17 +43,7 @@ export class BrowserWebSocketHandler implements WebSocketHandler {
4343

4444
connect(url: string): Promise<void> {
4545
return new Promise((resolve, reject) => {
46-
try {
47-
this.ws = new WebSocket(url);
48-
} catch (e) {
49-
return reject(
50-
new AIError(
51-
AIErrorCode.ERROR,
52-
`Internal Error: Invalid WebSocket URL: ${url}`
53-
)
54-
);
55-
}
56-
46+
this.ws = new WebSocket(url);
5747
this.ws.addEventListener('open', () => resolve(), { once: true });
5848
this.ws.addEventListener(
5949
'error',
@@ -136,15 +126,22 @@ export class BrowserWebSocketHandler implements WebSocketHandler {
136126

137127
close(code?: number, reason?: string): Promise<void> {
138128
return new Promise(resolve => {
129+
if (!this.ws) {
130+
return resolve();
131+
}
132+
133+
this.ws.addEventListener('close', () => resolve(), { once: true });
134+
// Calling 'close' during these states results in an error.
139135
if (
140-
!this.ws ||
141136
this.ws.readyState === WebSocket.CLOSED ||
142-
this.ws.readyState === WebSocket.CLOSING
137+
this.ws.readyState === WebSocket.CONNECTING
143138
) {
144139
return resolve();
145140
}
146-
this.ws.addEventListener('close', () => resolve(), { once: true });
147-
this.ws.close(code, reason);
141+
142+
if (this.ws.readyState !== WebSocket.CLOSING) {
143+
this.ws.close(code, reason);
144+
}
148145
});
149146
}
150147
}

packages/ai/src/platform/node/websocket.test.ts

Lines changed: 5 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -23,15 +23,14 @@ import { TextEncoder } from 'util';
2323
import { MockWebSocketServer } from '../../../test-utils/mock-websocket-server';
2424
import { WebSocketHandler } from '../websocket';
2525
import { NodeWebSocketHandler } from './websocket';
26-
import { AIError } from '../../errors';
2726

2827
use(sinonChai);
2928
use(chaiAsPromised);
3029

3130
const TEST_PORT = 9003;
3231
const TEST_URL = `ws://localhost:${TEST_PORT}`;
3332

34-
describe('NodeWebSocketHandler (Integration Tests)', () => {
33+
describe('NodeWebSocketHandler', () => {
3534
let server: MockWebSocketServer;
3635
let handler: WebSocketHandler;
3736

@@ -66,12 +65,9 @@ describe('NodeWebSocketHandler (Integration Tests)', () => {
6665
expect(server.clients.size).to.equal(1);
6766
});
6867

69-
it('should reject if the connection fails (e.g., wrong port)', async () => {
70-
const wrongPortUrl = `ws://localhost:${TEST_PORT + 1}`;
71-
await expect(handler.connect(wrongPortUrl)).to.be.rejectedWith(
72-
AIError,
73-
/Failed to establish WebSocket connection/
74-
);
68+
it('should reject if the connection fails', async () => {
69+
const wrongPortUrl = `ws://wrongUrl:9000`;
70+
await expect(handler.connect(wrongPortUrl)).to.be.rejected;
7571
});
7672
});
7773

@@ -144,4 +140,4 @@ describe('NodeWebSocketHandler (Integration Tests)', () => {
144140
await expect(consumerPromise).to.be.fulfilled;
145141
});
146142
});
147-
});
143+
});

packages/ai/src/platform/node/websocket.ts

Lines changed: 40 additions & 45 deletions
Original file line numberDiff line numberDiff line change
@@ -15,7 +15,6 @@
1515
* limitations under the License.
1616
*/
1717

18-
// import { WebSocket, MessageEvent } from 'ws'; // External dependency on native Node module
1918
import { AIError } from '../../errors';
2019
import { AIErrorCode } from '../../types';
2120
import { WebSocketHandler } from '../websocket';
@@ -27,41 +26,41 @@ export function createWebSocketHandler(): WebSocketHandler {
2726
throw new AIError(
2827
AIErrorCode.UNSUPPORTED,
2928
`The "Live" feature is being used in a Node environment, but the ` +
30-
`runtime version is ${process.versions.node}. This feature requires Node >= 22` +
29+
`runtime version is ${process.versions.node}. This feature requires Node >= 22 ` +
3130
`for native WebSocket support.`
3231
);
32+
} else if (typeof WebSocket === 'undefined') {
33+
throw new AIError(
34+
AIErrorCode.UNSUPPORTED,
35+
`The "Live" feature is being used in a Node environment that does not offer the ` +
36+
`'WebSocket' API in the global scope.`
37+
);
3338
}
39+
3440
return new NodeWebSocketHandler();
3541
} else {
3642
throw new AIError(
3743
AIErrorCode.UNSUPPORTED,
3844
'The "Live" feature is not supported in this Node-like environment. It is supported in ' +
39-
'standard browser windows, Web Workers with WebSocket support, and Node >= 22.'
45+
'modern browser windows, Web Workers with WebSocket support, and Node >= 22.'
4046
);
4147
}
4248
}
4349

4450
/**
4551
* A WebSocketHandler implementation for Node >= 22.
4652
*
53+
* Node 22 is the minimum version that offers the built-in global `WebSocket` API.
54+
*
4755
* @internal
4856
*/
4957
export class NodeWebSocketHandler implements WebSocketHandler {
50-
private ws?: import('ws').WebSocket;
58+
private ws?: WebSocket;
5159

5260
async connect(url: string): Promise<void> {
5361
return new Promise(async (resolve, reject) => {
54-
const { WebSocket } = await import('ws');
55-
try {
56-
this.ws = new WebSocket(url);
57-
} catch (e) {
58-
return reject(
59-
new AIError(
60-
AIErrorCode.ERROR,
61-
`Internal Error: Invalid WebSocket URL: ${url}`
62-
)
63-
);
64-
}
62+
this.ws = new WebSocket(url);
63+
this.ws.binaryType = 'blob';
6564
this.ws!.addEventListener('open', () => resolve(), { once: true });
6665
this.ws!.addEventListener(
6766
'error',
@@ -78,7 +77,7 @@ export class NodeWebSocketHandler implements WebSocketHandler {
7877
}
7978

8079
send(data: string | ArrayBuffer): void {
81-
if (!this.ws || this.ws.readyState !== this.ws.OPEN) {
80+
if (!this.ws || this.ws.readyState !== WebSocket.OPEN) {
8281
throw new AIError(AIErrorCode.REQUEST_ERROR, 'WebSocket is not open.');
8382
}
8483
this.ws.send(data);
@@ -96,36 +95,25 @@ export class NodeWebSocketHandler implements WebSocketHandler {
9695
let resolvePromise: (() => void) | null = null;
9796
let isClosed = false;
9897

99-
const messageListener = (event: import('ws').MessageEvent): void => {
100-
let textData: string;
101-
102-
if (typeof event.data === 'string') {
103-
textData = event.data;
104-
} else if (
105-
event.data instanceof Buffer ||
106-
event.data instanceof ArrayBuffer ||
107-
event.data instanceof Uint8Array
108-
) {
109-
const decoder = new TextDecoder();
110-
textData = decoder.decode(event.data);
98+
const messageListener = async (event: MessageEvent): Promise<void> => {
99+
if (event.data instanceof Blob) {
100+
try {
101+
const obj = JSON.parse(await event.data.text()) as unknown;
102+
messageQueue.push(obj);
103+
if (resolvePromise) {
104+
resolvePromise();
105+
resolvePromise = null;
106+
}
107+
} catch (e) {
108+
console.warn('Failed to parse WebSocket message to JSON:', e);
109+
}
111110
} else {
112111
throw new AIError(
113112
AIErrorCode.PARSE_FAILED,
114113
`Failed to parse WebSocket response to JSON. ` +
115-
`Expected data to be string, Buffer, ArrayBuffer, or Uint8Array, but was ${typeof event.data}.`
114+
`Expected data to be a Blob, but was ${typeof event.data}.`
116115
);
117116
}
118-
119-
try {
120-
const parsedObject = JSON.parse(textData);
121-
messageQueue.push(parsedObject);
122-
if (resolvePromise) {
123-
resolvePromise();
124-
resolvePromise = null;
125-
}
126-
} catch (e) {
127-
console.warn('Failed to parse WebSocket message to JSON:', textData, e);
128-
}
129117
};
130118

131119
const closeListener = (): void => {
@@ -155,15 +143,22 @@ export class NodeWebSocketHandler implements WebSocketHandler {
155143

156144
close(code?: number, reason?: string): Promise<void> {
157145
return new Promise(resolve => {
146+
if (!this.ws) {
147+
return resolve();
148+
}
149+
150+
this.ws.addEventListener('close', () => resolve(), { once: true });
151+
// Calling 'close' during these states results in an error.
158152
if (
159-
!this.ws ||
160-
this.ws.readyState === this.ws.CLOSED ||
161-
this.ws.readyState === this.ws.CLOSING
153+
this.ws.readyState === WebSocket.CLOSED ||
154+
this.ws.readyState === WebSocket.CONNECTING
162155
) {
163156
return resolve();
164157
}
165-
this.ws.addEventListener('close', () => resolve(), { once: true });
166-
this.ws.close(code, reason);
158+
159+
if (this.ws.readyState !== WebSocket.CLOSING) {
160+
this.ws.close(code, reason);
161+
}
167162
});
168163
}
169164
}

packages/ai/test-utils/mock-websocket-server.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -52,7 +52,7 @@ export class MockWebSocketServer {
5252
broadcast(message: string | Buffer): void {
5353
for (const client of this.clients) {
5454
if (client.readyState === WebSocket.OPEN) {
55-
client.send(message);
55+
client.send(message, { binary: true });
5656
}
5757
}
5858
}

0 commit comments

Comments
 (0)