@@ -2,72 +2,85 @@ import { WebSocketMessage } from '@shared/types/websocket-message'
22import { handle_messages } from './message-handler'
33import { DEFAULT_PORT , CLIENT_TYPE } from '@shared/constants/websocket'
44import browser from 'webextension-polyfill'
5- let websocket : WebSocket | null = null
6- let is_reconnecting = false
7- let last_ping_timestamp = Date . now ( )
85
9- browser . storage . onChanged . addListener ( ( changes , areaName ) => {
10- if ( areaName === 'local' && changes [ 'browser-profile-name' ] ) {
11- if ( websocket ) {
12- is_reconnecting = false
13- const old_ws = websocket
14- websocket = null
15- old_ws . close ( )
16- }
17- connect_websocket ( )
6+ // Remote setups (Remote-SSH, WSL, Dev Containers) may forward the server to
7+ // DEFAULT_PORT + n when the local DEFAULT_PORT is already occupied.
8+ const PORT_RANGE_SIZE = 10
9+
10+ const ports_to_check = ( ) : number [ ] => {
11+ const ports = [ ]
12+ for ( let i = 0 ; i < PORT_RANGE_SIZE ; i ++ ) {
13+ ports . push ( DEFAULT_PORT + i )
1814 }
19- } )
15+ return ports
16+ }
2017
21- export const check_server_health = async ( ) : Promise < boolean > => {
22- try {
23- const response = await fetch ( `http://localhost:${ DEFAULT_PORT } /health` )
24- return response . ok
25- } catch {
26- return false
18+ const STALE_AFTER_MS = 20000
19+ const RETRY_AFTER_DROP_MS = 1000 // we had a live connection, retry quickly
20+ const RETRY_IDLE_MS = 5000 // nothing was ever listening, poll slowly
21+
22+ type Connection = {
23+ ws : WebSocket | null
24+ last_ping : number
25+ retry_timer : ReturnType < typeof setTimeout > | null
26+ }
27+
28+ const connections = new Map < number , Connection > ( )
29+
30+ const client_id_to_port = new Map < string , number > ( )
31+
32+ const get_connection = ( port : number ) : Connection => {
33+ let c = connections . get ( port )
34+ if ( ! c ) {
35+ c = { ws : null , last_ping : Date . now ( ) , retry_timer : null }
36+ connections . set ( port , c )
2737 }
38+ return c
39+ }
40+
41+ const schedule_retry = ( port : number , delay_ms : number ) => {
42+ const c = get_connection ( port )
43+ if ( c . retry_timer ) return
44+ c . retry_timer = setTimeout ( ( ) => {
45+ c . retry_timer = null
46+ connect_websocket ( port )
47+ } , delay_ms )
2848}
2949
50+ const is_busy = ( c : Connection ) =>
51+ c . ws ?. readyState === WebSocket . OPEN ||
52+ c . ws ?. readyState === WebSocket . CONNECTING
53+
3054export const check_and_recover_connection = ( ) => {
31- if ( websocket ?. readyState === WebSocket . OPEN ) {
32- if ( Date . now ( ) - last_ping_timestamp > 20000 ) {
33- console . warn (
34- 'WebSocket connection is stale (no recent pings). Force reconnecting...'
35- )
36- is_reconnecting = false
37- websocket . close ( )
55+ for ( const port of ports_to_check ( ) ) {
56+ const c = get_connection ( port )
57+
58+ if ( c . ws ?. readyState === WebSocket . OPEN ) {
59+ if ( Date . now ( ) - c . last_ping > STALE_AFTER_MS ) {
60+ console . warn (
61+ `WebSocket on port ${ port } is stale (no recent pings). Force reconnecting...`
62+ )
63+ c . ws . close ( )
64+ }
65+ continue
3866 }
39- }
4067
41- if (
42- ! is_reconnecting &&
43- websocket ?. readyState !== WebSocket . OPEN &&
44- websocket ?. readyState !== WebSocket . CONNECTING
45- ) {
46- connect_websocket ( )
68+ if ( ! is_busy ( c ) && ! c . retry_timer ) {
69+ connect_websocket ( port )
70+ }
4771 }
4872}
4973
50- export const connect_websocket = async ( ) : Promise < void > => {
51- if (
52- is_reconnecting ||
53- websocket ?. readyState === WebSocket . OPEN ||
54- websocket ?. readyState === WebSocket . CONNECTING
55- ) {
56- return
57- }
74+ export const connect_websocket = async ( port : number ) : Promise < void > => {
75+ const c = get_connection ( port )
76+ if ( is_busy ( c ) ) return
5877
59- is_reconnecting = true
60- last_ping_timestamp = Date . now ( )
78+ if ( c . retry_timer ) {
79+ clearTimeout ( c . retry_timer )
80+ c . retry_timer = null
81+ }
6182
6283 try {
63- const is_healthy = await check_server_health ( )
64- if ( ! is_healthy ) {
65- console . debug ( 'Server health check failed, retrying in 5 seconds...' )
66- is_reconnecting = false
67- setTimeout ( check_and_recover_connection , 5000 )
68- return
69- }
70-
7184 const manifest = browser . runtime . getManifest ( )
7285 const version = manifest . version
7386 const user_agent = navigator . userAgent
@@ -86,61 +99,99 @@ export const connect_websocket = async (): Promise<void> => {
8699 await browser . storage . local . set ( { 'browser-id' : browser_id } )
87100 }
88101
89- let ws_url = `ws://localhost:${ DEFAULT_PORT } ?client_type=${ CLIENT_TYPE . BROWSER } &version=${ version } &user_agent=${ encodeURIComponent ( user_agent ) } &browser_id=${ encodeURIComponent ( browser_id ) } `
102+ if ( is_busy ( c ) ) return
103+
104+ let ws_url = `ws://localhost:${ port } ?client_type=${ CLIENT_TYPE . BROWSER } &version=${ version } &user_agent=${ encodeURIComponent ( user_agent ) } &browser_id=${ encodeURIComponent ( browser_id ) } `
90105 if ( profile_name ) {
91106 ws_url += `&profile_name=${ encodeURIComponent ( profile_name ) } `
92107 }
93108
94109 const ws = new WebSocket ( ws_url )
95- websocket = ws
110+ c . ws = ws
111+ c . last_ping = Date . now ( )
112+ let was_opened = false
96113
97114 ws . onopen = ( ) => {
98- if ( websocket !== ws ) return
99- console . log ( 'Connected with the VS Code!' )
100- is_reconnecting = false
101- last_ping_timestamp = Date . now ( )
115+ if ( c . ws !== ws ) return
116+ was_opened = true
117+ c . last_ping = Date . now ( )
118+ console . log ( `Connected with the VS Code on port ${ port } !` )
119+ check_and_recover_connection ( )
102120 }
103121
104- ws . onmessage = async ( event ) => {
105- if ( websocket !== ws ) return
122+ ws . onmessage = ( event ) => {
123+ if ( c . ws !== ws ) return
106124 const message = JSON . parse ( event . data )
107125 if ( message . action === 'ping' ) {
108- last_ping_timestamp = Date . now ( )
126+ c . last_ping = Date . now ( )
109127 if ( ws . readyState === WebSocket . OPEN ) {
110128 ws . send ( JSON . stringify ( { action : 'pong' } ) )
111129 }
112130 return
113131 }
132+ if ( message . action === 'initialize-chat' && message . client_id ) {
133+ client_id_to_port . set ( message . client_id , port )
134+ }
114135 handle_messages ( message as WebSocketMessage )
115136 }
116137
117138 ws . onclose = ( ) => {
118- console . log ( 'Disconnected from VS Code, attempting to reconnect...' )
119- if ( websocket === ws ) {
120- websocket = null
139+ if ( c . ws !== ws ) return
140+ c . ws = null
141+ if ( was_opened ) {
142+ console . log (
143+ `Disconnected from VS Code on port ${ port } , attempting to reconnect...`
144+ )
121145 }
122- is_reconnecting = false
123- setTimeout ( check_and_recover_connection , 1000 )
146+ schedule_retry ( port , was_opened ? RETRY_AFTER_DROP_MS : RETRY_IDLE_MS )
124147 }
148+ } catch {
149+ c . ws = null
150+ schedule_retry ( port , RETRY_IDLE_MS )
151+ }
152+ }
125153
126- ws . onerror = ( ) => {
127- if ( websocket === ws ) {
128- websocket = null
129- }
130- is_reconnecting = false
131- setTimeout ( check_and_recover_connection , 1000 )
154+ const reset_all_connections = ( ) => {
155+ for ( const port of ports_to_check ( ) ) {
156+ const c = get_connection ( port )
157+ if ( c . retry_timer ) {
158+ clearTimeout ( c . retry_timer )
159+ c . retry_timer = null
132160 }
133- } catch {
134- is_reconnecting = false
135- setTimeout ( check_and_recover_connection , 5000 )
161+ const old_ws = c . ws
162+ c . ws = null
163+ old_ws ?. close ( )
164+ connect_websocket ( port )
136165 }
137166}
138167
168+ browser . storage . onChanged . addListener ( ( changes , areaName ) => {
169+ if ( areaName === 'local' && changes [ 'browser-profile-name' ] ) {
170+ reset_all_connections ( )
171+ }
172+ } )
173+
139174export const send_message_to_server = ( message : any ) : boolean => {
140- if ( websocket ?. readyState === WebSocket . OPEN ) {
141- websocket . send ( JSON . stringify ( message ) )
142- return true
175+ const json = JSON . stringify ( message )
176+ const port : number | undefined = message ?. client_id
177+ ? client_id_to_port . get ( message . client_id )
178+ : undefined
179+
180+ if ( port === undefined && message . action === 'apply-response' ) {
181+ console . warn ( 'Dropping apply-response message: unknown client_id' )
182+ return false
183+ }
184+
185+ const targets =
186+ port !== undefined ? [ get_connection ( port ) ] : [ ...connections . values ( ) ]
187+
188+ let sent = false
189+ for ( const c of targets ) {
190+ if ( c . ws ?. readyState === WebSocket . OPEN ) {
191+ c . ws . send ( json )
192+ sent = true
193+ }
143194 }
144- console . warn ( 'WebSocket not connected, cannot send message' )
145- return false
195+ if ( ! sent ) console . warn ( 'WebSocket not connected, cannot send message' )
196+ return sent
146197}
0 commit comments