Book a Demo!
CoCalc Logo Icon
StoreFeaturesDocsShareSupportNewsAboutPoliciesSign UpSign In
microsoft
GitHub Repository: microsoft/vscode
Path: blob/main/src/vs/platform/agentHost/browser/webSocketClientTransport.ts
13394 views
1
/*---------------------------------------------------------------------------------------------
2
* Copyright (c) Microsoft Corporation. All rights reserved.
3
* Licensed under the MIT License. See License.txt in the project root for license information.
4
*--------------------------------------------------------------------------------------------*/
5
6
// WebSocket client transport for connecting to remote agent host processes.
7
// Uses plain JSON serialization — URIs are string-typed in the protocol.
8
9
import { Emitter } from '../../../base/common/event.js';
10
import { Disposable } from '../../../base/common/lifecycle.js';
11
import { connectionTokenQueryName } from '../../../base/common/network.js';
12
import type { AhpServerNotification, JsonRpcResponse, ProtocolMessage } from '../common/state/sessionProtocol.js';
13
import type { IClientTransport } from '../common/state/sessionTransport.js';
14
import { MALFORMED_FRAMES_FORCE_CLOSE_THRESHOLD, MALFORMED_FRAMES_LOG_CAP } from '../common/transportConstants.js';
15
16
// ---- Client transport -------------------------------------------------------
17
18
/**
19
* A WebSocket client transport that connects to a remote agent host server.
20
* Uses the native browser WebSocket API (available in Electron renderer).
21
* Implements {@link IClientTransport} with JSON serialization and URI revival.
22
*/
23
export class WebSocketClientTransport extends Disposable implements IClientTransport {
24
25
private readonly _onMessage = this._register(new Emitter<ProtocolMessage>());
26
readonly onMessage = this._onMessage.event;
27
28
private readonly _onClose = this._register(new Emitter<void>());
29
readonly onClose = this._onClose.event;
30
31
private readonly _onOpen = this._register(new Emitter<void>());
32
readonly onOpen = this._onOpen.event;
33
34
private _ws: WebSocket | undefined;
35
private _malformedFrames = 0;
36
37
/** Guards against firing onClose more than once. */
38
private _closeFired = false;
39
40
get isOpen(): boolean {
41
return this._ws?.readyState === WebSocket.OPEN;
42
}
43
44
constructor(
45
private readonly _address: string,
46
private readonly _connectionToken?: string,
47
) {
48
// TODO: @osortega remove console.logs
49
super();
50
}
51
52
/**
53
* Initiate the WebSocket connection. Resolves when the connection
54
* is open, or rejects on error/timeout.
55
*/
56
connect(): Promise<void> {
57
return new Promise<void>((resolve, reject) => {
58
if (this._store.isDisposed) {
59
reject(new Error('Transport is disposed'));
60
return;
61
}
62
63
let url = this._address.startsWith('ws://') || this._address.startsWith('wss://')
64
? this._address
65
: `ws://${this._address}`;
66
67
if (this._connectionToken) {
68
const separator = url.includes('?') ? '&' : '?';
69
url += `${separator}${connectionTokenQueryName}=${encodeURIComponent(this._connectionToken)}`;
70
}
71
72
const ws = new WebSocket(url);
73
this._ws = ws;
74
75
const onOpen = () => {
76
cleanup();
77
this._onOpen.fire();
78
resolve();
79
};
80
81
const onError = () => {
82
cleanup();
83
reject(new Error(`WebSocket connection failed: ${this._address}`));
84
};
85
86
const onClose = () => {
87
cleanup();
88
reject(new Error(`WebSocket closed before connection was established: ${this._address}`));
89
};
90
91
const cleanup = () => {
92
ws.removeEventListener('open', onOpen);
93
ws.removeEventListener('error', onError);
94
ws.removeEventListener('close', onClose);
95
};
96
97
ws.addEventListener('open', onOpen);
98
ws.addEventListener('error', onError);
99
ws.addEventListener('close', onClose);
100
101
// Wire up long-lived listeners after connection
102
ws.addEventListener('message', (event: MessageEvent) => {
103
if (typeof event.data !== 'string') {
104
this._malformedFrames++;
105
if (this._malformedFrames <= MALFORMED_FRAMES_LOG_CAP) {
106
const dataType = event.data instanceof ArrayBuffer ? 'ArrayBuffer' : event.data instanceof Blob ? 'Blob' : typeof event.data;
107
const byteLen = event.data instanceof ArrayBuffer ? event.data.byteLength : event.data instanceof Blob ? event.data.size : 0;
108
console.warn(
109
`[WebSocketClientTransport] Non-string frame #${this._malformedFrames} (type=${dataType}, bytes=${byteLen})`
110
);
111
}
112
if (this._malformedFrames > MALFORMED_FRAMES_FORCE_CLOSE_THRESHOLD) {
113
console.warn(
114
`[WebSocketClientTransport] Malformed frame threshold exceeded; forcing close of ${this._address}.`
115
);
116
this._ws?.close(4002, 'malformed-frames');
117
}
118
return;
119
}
120
const text = event.data;
121
let message: ProtocolMessage;
122
try {
123
message = JSON.parse(text) as ProtocolMessage;
124
} catch (err) {
125
this._malformedFrames++;
126
if (this._malformedFrames <= MALFORMED_FRAMES_LOG_CAP) {
127
const preview = text.length > 80 ? text.slice(0, 80) + '…' : text;
128
console.warn(
129
`[WebSocketClientTransport] Malformed frame #${this._malformedFrames} (len=${text.length}): ${preview}`,
130
err instanceof Error ? err.message : String(err)
131
);
132
}
133
if (this._malformedFrames > MALFORMED_FRAMES_FORCE_CLOSE_THRESHOLD) {
134
console.warn(
135
`[WebSocketClientTransport] Malformed frame threshold exceeded; forcing close of ${this._address}.`
136
);
137
this._ws?.close(4002, 'malformed-frames');
138
}
139
return;
140
}
141
this._onMessage.fire(message);
142
});
143
144
ws.addEventListener('close', () => {
145
if (!this._closeFired) {
146
this._closeFired = true;
147
this._onClose.fire();
148
}
149
});
150
151
ws.addEventListener('error', () => {
152
// Error always precedes close - closing is handled in the close handler.
153
// Only fire if close hasn't already been fired (e.g. from send failure).
154
if (!this._closeFired) {
155
this._closeFired = true;
156
this._onClose.fire();
157
}
158
});
159
});
160
}
161
162
/**
163
* Send a message to the remote end. Returns `true` if the message was
164
* sent, `false` if it was dropped (socket not open). On failure, the
165
* transport is force-closed so reconnection is triggered immediately
166
* rather than silently losing messages.
167
*/
168
send(message: ProtocolMessage | AhpServerNotification | JsonRpcResponse): boolean {
169
if (this._ws?.readyState === WebSocket.OPEN) {
170
this._ws.send(JSON.stringify(message));
171
return true;
172
}
173
console.warn(
174
`[WebSocketClientTransport] Message dropped: readyState=${this._ws?.readyState ?? 'no-socket'}`
175
);
176
// Force-close and fire onClose exactly once to trigger reconnection
177
this._ws?.close(4001, 'send-on-dead-socket');
178
if (!this._closeFired) {
179
this._closeFired = true;
180
this._onClose.fire();
181
}
182
return false;
183
}
184
185
override dispose(): void {
186
this._ws?.close();
187
super.dispose();
188
}
189
}
190
191