@@ -31,7 +31,7 @@ import {
3131 HttpClientLogLevel ,
3232} from "../logging/types" ;
3333import { sizeOf } from "../logging/utils" ;
34- import { HttpStatusCode } from "../websocket/codes" ;
34+ import { HttpStatusCode , WebSocketCloseCode } from "../websocket/codes" ;
3535import {
3636 type UnidirectionalStream ,
3737 type CloseEvent ,
@@ -55,7 +55,7 @@ const coderSessionTokenHeader = "Coder-Session-Token";
5555 * Unified API class that includes both REST API methods from the base Api class
5656 * and WebSocket methods for real-time functionality.
5757 */
58- export class CoderApi extends Api {
58+ export class CoderApi extends Api implements vscode . Disposable {
5959 private readonly reconnectingSockets = new Set <
6060 ReconnectingWebSocket < unknown >
6161 > ( ) ;
@@ -102,11 +102,27 @@ export class CoderApi extends Api {
102102
103103 if ( currentHost !== host ) {
104104 for ( const socket of this . reconnectingSockets ) {
105- socket . reconnect ( ) ;
105+ if ( host ) {
106+ socket . reconnect ( ) ;
107+ } else {
108+ // No host means logout - suspend sockets (can resume when host is set again)
109+ socket . suspend ( WebSocketCloseCode . NORMAL , "Host cleared" ) ;
110+ }
106111 }
107112 }
108113 } ;
109114
115+ /**
116+ * Permanently dispose all WebSocket connections.
117+ * This clears handlers and prevents reconnection.
118+ */
119+ dispose ( ) : void {
120+ for ( const socket of this . reconnectingSockets ) {
121+ socket . close ( ) ;
122+ }
123+ this . reconnectingSockets . clear ( ) ;
124+ }
125+
110126 watchInboxNotifications = async (
111127 watchTemplates : string [ ] ,
112128 watchTargets : string [ ] ,
@@ -125,7 +141,7 @@ export class CoderApi extends Api {
125141 } ;
126142
127143 watchWorkspace = async ( workspace : Workspace , options ?: ClientOptions ) => {
128- return this . createWebSocketWithFallback < ServerSentEvent > ( {
144+ return this . createWebSocketWithFallback ( {
129145 apiRoute : `/api/v2/workspaces/${ workspace . id } /watch-ws` ,
130146 fallbackApiRoute : `/api/v2/workspaces/${ workspace . id } /watch` ,
131147 options,
@@ -137,7 +153,7 @@ export class CoderApi extends Api {
137153 agentId : WorkspaceAgent [ "id" ] ,
138154 options ?: ClientOptions ,
139155 ) => {
140- return this . createWebSocketWithFallback < ServerSentEvent > ( {
156+ return this . createWebSocketWithFallback ( {
141157 apiRoute : `/api/v2/workspaceagents/${ agentId } /watch-metadata-ws` ,
142158 fallbackApiRoute : `/api/v2/workspaceagents/${ agentId } /watch-metadata` ,
143159 options,
@@ -290,43 +306,43 @@ export class CoderApi extends Api {
290306 *
291307 * Note: The fallback on SSE ignores all passed client options except the headers.
292308 */
293- private async createWebSocketWithFallback < TData = unknown > ( configs : {
309+ private async createWebSocketWithFallback ( configs : {
294310 apiRoute : string ;
295311 fallbackApiRoute : string ;
296312 searchParams ?: Record < string , string > | URLSearchParams ;
297313 options ?: ClientOptions ;
298314 enableRetry ?: boolean ;
299- } ) : Promise < UnidirectionalStream < TData > > {
300- let webSocket : UnidirectionalStream < TData > ;
315+ } ) : Promise < UnidirectionalStream < ServerSentEvent > > {
316+ let webSocket : UnidirectionalStream < ServerSentEvent > ;
301317 try {
302- webSocket = await this . createWebSocket < TData > ( {
318+ webSocket = await this . createWebSocket < ServerSentEvent > ( {
303319 apiRoute : configs . apiRoute ,
304320 searchParams : configs . searchParams ,
305321 options : configs . options ,
306322 enableRetry : configs . enableRetry ,
307323 } ) ;
308324 } catch {
309325 // Failed to create WebSocket, use SSE fallback
310- return this . createSseFallback < TData > (
326+ return this . createSseFallback (
311327 configs . fallbackApiRoute ,
312328 configs . searchParams ,
313329 configs . options ?. headers ,
314330 ) ;
315331 }
316332
317333 return this . waitForConnection ( webSocket , ( ) =>
318- this . createSseFallback < TData > (
334+ this . createSseFallback (
319335 configs . fallbackApiRoute ,
320336 configs . searchParams ,
321337 configs . options ?. headers ,
322338 ) ,
323339 ) ;
324340 }
325341
326- private waitForConnection < TData > (
327- connection : UnidirectionalStream < TData > ,
328- onNotFound ?: ( ) => Promise < UnidirectionalStream < TData > > ,
329- ) : Promise < UnidirectionalStream < TData > > {
342+ private waitForConnection (
343+ connection : UnidirectionalStream < ServerSentEvent > ,
344+ onNotFound ?: ( ) => Promise < UnidirectionalStream < ServerSentEvent > > ,
345+ ) : Promise < UnidirectionalStream < ServerSentEvent > > {
330346 return new Promise ( ( resolve , reject ) => {
331347 const cleanup = ( ) => {
332348 connection . removeEventListener ( "open" , handleOpen ) ;
@@ -345,7 +361,12 @@ export class CoderApi extends Api {
345361 event . error ?. message ?. includes ( String ( HttpStatusCode . NOT_FOUND ) ) ;
346362
347363 if ( is404 && onNotFound ) {
348- connection . close ( ) ;
364+ if ( connection instanceof ReconnectingWebSocket ) {
365+ // We can attempt this again if we change the host
366+ connection . suspend ( ) ;
367+ } else {
368+ connection . close ( ) ;
369+ }
349370 onNotFound ( ) . then ( resolve ) . catch ( reject ) ;
350371 } else {
351372 reject ( event . error || new Error ( event . message ) ) ;
@@ -360,11 +381,11 @@ export class CoderApi extends Api {
360381 /**
361382 * Create SSE fallback connection
362383 */
363- private async createSseFallback < TData = unknown > (
384+ private async createSseFallback (
364385 apiRoute : string ,
365386 searchParams ?: Record < string , string > | URLSearchParams ,
366387 optionsHeaders ?: Record < string , string > ,
367- ) : Promise < UnidirectionalStream < TData > > {
388+ ) : Promise < UnidirectionalStream < ServerSentEvent > > {
368389 this . output . warn ( `WebSocket failed, using SSE fallback: ${ apiRoute } ` ) ;
369390
370391 const baseUrlRaw = this . getAxiosInstance ( ) . defaults . baseURL ;
0 commit comments