Skip to content

Commit ca016a6

Browse files
committed
Add WebSocket ping-pong heartbeat mechanism to monitor connection health and terminate stale clients
- it's been reported that sometimes we see duplicates in 'Connected Browsers' quick pick. Restarting the connected browser extension fixes it. - [Pasted text] - Property 'terminate' does not exist on type 'WebSocket' - [Changes] (old browser extension would not return pong) - check version and respect pong on >=[Fragment] 
1 parent 54d76f7 commit ca016a6

5 files changed

Lines changed: 93 additions & 11 deletions

File tree

‎apps/browser/src/background/websocket.ts‎

Lines changed: 9 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -27,8 +27,11 @@ export const check_and_recover_connection = () => {
2727
}
2828
}
2929

30-
if (!is_reconnecting && websocket?.readyState !== WebSocket.OPEN &&
31-
websocket?.readyState !== WebSocket.CONNECTING) {
30+
if (
31+
!is_reconnecting &&
32+
websocket?.readyState !== WebSocket.OPEN &&
33+
websocket?.readyState !== WebSocket.CONNECTING
34+
) {
3235
connect_websocket()
3336
}
3437
}
@@ -70,8 +73,11 @@ export const connect_websocket = async (): Promise<void> => {
7073

7174
websocket.onmessage = async (event) => {
7275
const message = JSON.parse(event.data)
73-
if (message.action === 'ping') {
76+
if (message.action == 'ping') {
7477
last_ping_timestamp = Date.now()
78+
if (websocket?.readyState == WebSocket.OPEN) {
79+
websocket.send(JSON.stringify({ action: 'pong' }))
80+
}
7581
return
7682
}
7783
console.debug(message)

‎apps/browser/src/manifest.json‎

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,7 @@
44
"name": "Autofill for Code Web Chat",
55
"short_name": "CWC",
66
"description": "Got tired with copy and paste? Enable prompt autofill!",
7-
"version": "2026.7.1",
7+
"version": "2026.7.2",
88
"homepage_url": "https://github.com/robertpiosik/CodeWebChat",
99
"icons": {
1010
"16": "icons/icon-16.png",

‎apps/editor/src/services/websocket-manager.ts‎

Lines changed: 12 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,7 @@ import * as http from 'http'
55
import * as path from 'path'
66
import * as net from 'net'
77
import {
8+
ApplyResponseMessage,
89
ConnectedBrowser,
910
InitializeChatMessage,
1011
WebSocketMessage
@@ -184,6 +185,14 @@ export class WebSocketManager {
184185
this.client.on('message', async (data) => {
185186
try {
186187
const message = JSON.parse(data.toString()) as WebSocketMessage
188+
189+
if (message.action === 'ping') {
190+
if (this.client?.readyState === WebSocket.WebSocket.OPEN) {
191+
this.client.send(JSON.stringify({ action: 'pong' }))
192+
}
193+
return
194+
}
195+
187196
Logger.info({
188197
function_name: '_connect_as_client',
189198
message: 'Incoming WS message',
@@ -200,9 +209,10 @@ export class WebSocketManager {
200209
message.action == 'apply-response' ||
201210
(message.action as any) == 'apply-chat-response' // Backward compatibility 20.07.26
202211
) {
212+
const apply_msg = message as ApplyResponseMessage
203213
vscode.commands.executeCommand('codeWebChat.applyResponse', {
204-
raw_instructions: message.raw_instructions,
205-
url: message.url
214+
raw_instructions: apply_msg.raw_instructions,
215+
url: apply_msg.url
206216
} as ApplyResponseCommandArgs)
207217
}
208218
} catch (error) {

‎apps/editor/src/services/websocket-server-process.ts‎

Lines changed: 61 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -5,16 +5,31 @@ const WebSocket = require('ws')
55

66
import { DEFAULT_PORT, SECURITY_TOKENS } from '@shared/constants/websocket'
77

8+
const is_version_gte = (v: string, target: string) => {
9+
if (v === 'unknown') return false
10+
const vParts = v.split('.').map(Number)
11+
const tParts = target.split('.').map(Number)
12+
for (let i = 0; i < Math.max(vParts.length, tParts.length); i++) {
13+
const p1 = vParts[i] || 0
14+
const p2 = tParts[i] || 0
15+
if (p1 > p2) return true
16+
if (p1 < p2) return false
17+
}
18+
return true
19+
}
20+
821
interface BrowserClient {
922
ws: WebSocket
1023
version: string
1124
id: number
1225
user_agent: string
26+
is_alive: boolean
1327
}
1428

1529
interface VSCodeClient {
1630
ws: WebSocket
1731
client_id: number
32+
is_alive: boolean
1833
}
1934

2035
class WebSocketServer {
@@ -88,7 +103,7 @@ class WebSocketServer {
88103

89104
this.connections.add(ws)
90105

91-
ws.on('message', (message: any) => this._handle_message(message))
106+
ws.on('message', (message: any) => this._handle_message(message, ws))
92107
ws.on('close', () => this._handle_disconnection(ws, is_browser_client))
93108
}
94109

@@ -99,7 +114,13 @@ class WebSocketServer {
99114
this.browser_client_counter++
100115
const id = this.browser_client_counter
101116

102-
const client: BrowserClient = { ws, version, id, user_agent }
117+
const client: BrowserClient = {
118+
ws,
119+
version,
120+
id,
121+
user_agent,
122+
is_alive: true
123+
}
103124
this.browser_clients.set(id, client)
104125

105126
this._notify_vscode_clients()
@@ -108,7 +129,7 @@ class WebSocketServer {
108129

109130
private _handle_vscode_connection(ws: WebSocket) {
110131
const client_id = this._generate_client_id()
111-
this.vscode_clients.set(client_id, { ws, client_id })
132+
this.vscode_clients.set(client_id, { ws, client_id, is_alive: true })
112133

113134
ws.send(
114135
JSON.stringify({
@@ -136,9 +157,30 @@ class WebSocketServer {
136157
}
137158
}
138159

139-
private _handle_message(message: any) {
160+
private _handle_message(message: any, ws: any) {
140161
const msg_string = message.toString()
141-
const msg_data = JSON.parse(msg_string)
162+
let msg_data
163+
try {
164+
msg_data = JSON.parse(msg_string)
165+
} catch {
166+
return
167+
}
168+
169+
if (msg_data.action === 'pong') {
170+
for (const client of this.browser_clients.values()) {
171+
if (client.ws === ws) {
172+
client.is_alive = true
173+
return
174+
}
175+
}
176+
for (const client of this.vscode_clients.values()) {
177+
if (client.ws === ws) {
178+
client.is_alive = true
179+
return
180+
}
181+
}
182+
return
183+
}
142184

143185
if (msg_data.action == 'initialize-chat') {
144186
if (msg_data.target_browser_id) {
@@ -231,12 +273,26 @@ class WebSocketServer {
231273
const ping_message = JSON.stringify({ action: 'ping' })
232274

233275
for (const client of this.browser_clients.values()) {
276+
const expects_pong = is_version_gte(client.version, '2026.7.2')
277+
278+
if (expects_pong) {
279+
if (!client.is_alive) {
280+
;(client.ws as any).terminate()
281+
continue
282+
}
283+
client.is_alive = false
284+
}
234285
if (client.ws.readyState === WebSocket.OPEN) {
235286
client.ws.send(ping_message)
236287
}
237288
}
238289

239290
for (const client of this.vscode_clients.values()) {
291+
if (!client.is_alive) {
292+
;(client.ws as any).terminate()
293+
continue
294+
}
295+
client.is_alive = false
240296
if (client.ws.readyState == WebSocket.OPEN) {
241297
client.ws.send(ping_message)
242298
}

‎packages/shared/src/types/websocket-message.ts‎

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -47,8 +47,18 @@ export type ClientIdAssignmentMessage = {
4747
client_id: number
4848
}
4949

50+
export type PingMessage = {
51+
action: 'ping'
52+
}
53+
54+
export type PongMessage = {
55+
action: 'pong'
56+
}
57+
5058
export type WebSocketMessage =
5159
| InitializeChatMessage
5260
| BrowserConnectionStatusMessage
5361
| ClientIdAssignmentMessage
5462
| ApplyResponseMessage
63+
| PingMessage
64+
| PongMessage

0 commit comments

Comments
 (0)