@@ -3,15 +3,26 @@ import { Workspace, WorkspaceAgent } from "coder/site/src/api/typesGenerated"
33import EventSource from "eventsource"
44import * as path from "path"
55import * as vscode from "vscode"
6- import { AgentMetadataEvent , AgentMetadataEventSchemaArray , extractAgents } from "./api-helper"
6+ import {
7+ AgentMetadataEvent ,
8+ AgentMetadataEventSchemaArray ,
9+ extractAllAgents ,
10+ extractAgents ,
11+ errToStr ,
12+ } from "./api-helper"
713import { Storage } from "./storage"
814
915export enum WorkspaceQuery {
1016 Mine = "owner:me" ,
1117 All = "" ,
1218}
1319
14- type AgentWatcher = { dispose : ( ) => void ; metadata ?: AgentMetadataEvent [ ] }
20+ type AgentWatcher = {
21+ onChange : vscode . EventEmitter < null > [ "event" ]
22+ dispose : ( ) => void
23+ metadata ?: AgentMetadataEvent [ ]
24+ error ?: unknown
25+ }
1526
1627export class WorkspaceProvider implements vscode . TreeDataProvider < vscode . TreeItem > {
1728 private workspaces : WorkspaceTreeItem [ ] = [ ]
@@ -39,9 +50,6 @@ export class WorkspaceProvider implements vscode.TreeDataProvider<vscode.TreeIte
3950 }
4051 this . fetching = true
4152
42- // TODO: It would be better to reuse these.
43- Object . values ( this . agentWatchers ) . forEach ( ( watcher ) => watcher . dispose ( ) )
44-
4553 // It is possible we called fetchAndRefresh() manually (through the button
4654 // for example), in which case we might still have a pending refresh that
4755 // needs to be cleared.
@@ -93,12 +101,38 @@ export class WorkspaceProvider implements vscode.TreeDataProvider<vscode.TreeIte
93101 return this . fetch ( )
94102 }
95103
96- return resp . workspaces . map ( ( workspace ) => {
97- const showMetadata = this . getWorkspacesQuery === WorkspaceQuery . Mine
98- if ( showMetadata ) {
99- const agents = extractAgents ( workspace )
100- agents . forEach ( ( agent ) => this . monitorMetadata ( agent . id , url , token2 ) ) // monitor metadata for all agents
104+ const oldWatcherIds = Object . keys ( this . agentWatchers )
105+ const reusedWatcherIds : string [ ] = [ ]
106+
107+ // TODO: I think it might make more sense for the tree items to contain
108+ // their own watchers, rather than recreate the tree items every time and
109+ // have this separate map held outside the tree.
110+ const showMetadata = this . getWorkspacesQuery === WorkspaceQuery . Mine
111+ if ( showMetadata ) {
112+ const agents = extractAllAgents ( resp . workspaces )
113+ agents . forEach ( ( agent ) => {
114+ // If we have an existing watcher, re-use it.
115+ if ( this . agentWatchers [ agent . id ] ) {
116+ reusedWatcherIds . push ( agent . id )
117+ return this . agentWatchers [ agent . id ]
118+ }
119+ // Otherwise create a new watcher.
120+ const watcher = monitorMetadata ( agent . id , url , token2 )
121+ watcher . onChange ( ( ) => this . refresh ( ) )
122+ this . agentWatchers [ agent . id ] = watcher
123+ return watcher
124+ } )
125+ }
126+
127+ // Dispose of watchers we ended up not reusing.
128+ oldWatcherIds . forEach ( ( id ) => {
129+ if ( ! reusedWatcherIds . includes ( id ) ) {
130+ this . agentWatchers [ id ] . dispose ( )
131+ delete this . agentWatchers [ id ]
101132 }
133+ } )
134+
135+ return resp . workspaces . map ( ( workspace ) => {
102136 return new WorkspaceTreeItem ( workspace , this . getWorkspacesQuery === WorkspaceQuery . All , showMetadata )
103137 } )
104138 }
@@ -157,61 +191,69 @@ export class WorkspaceProvider implements vscode.TreeDataProvider<vscode.TreeIte
157191 )
158192 return Promise . resolve ( agentTreeItems )
159193 } else if ( element instanceof AgentTreeItem ) {
160- const savedMetadata = this . agentWatchers [ element . agent . id ] ?. metadata || [ ]
194+ const watcher = this . agentWatchers [ element . agent . id ]
195+ if ( watcher ?. error ) {
196+ return Promise . resolve ( [ new ErrorTreeItem ( watcher . error ) ] )
197+ }
198+ const savedMetadata = watcher ?. metadata || [ ]
161199 return Promise . resolve ( savedMetadata . map ( ( metadata ) => new AgentMetadataTreeItem ( metadata ) ) )
162200 }
163201
164202 return Promise . resolve ( [ ] )
165203 }
166204 return Promise . resolve ( this . workspaces )
167205 }
206+ }
168207
169- // monitorMetadata opens an SSE endpoint to monitor metadata on the specified
170- // agent and registers a disposer that can be used to stop the watch.
171- monitorMetadata ( agentId : WorkspaceAgent [ "id" ] , url : string , token : string ) : void {
172- const agentMetadataURL = new URL ( `${ url } /api/v2/workspaceagents/${ agentId } /watch-metadata` )
173- const agentMetadataEventSource = new EventSource ( agentMetadataURL . toString ( ) , {
174- headers : {
175- "Coder-Session-Token" : token ,
176- } ,
177- } )
178-
179- let disposed = false
180- const watcher : AgentWatcher = {
181- dispose : ( ) => {
182- if ( ! disposed ) {
183- delete this . agentWatchers [ agentId ]
184- agentMetadataEventSource . close ( )
185- disposed = true
186- }
187- } ,
188- }
208+ // monitorMetadata opens an SSE endpoint to monitor metadata on the specified
209+ // agent and registers a watcher that can be disposed to stop the watch and
210+ // emits an event when the metadata changes.
211+ function monitorMetadata ( agentId : WorkspaceAgent [ "id" ] , url : string , token : string ) : AgentWatcher {
212+ const metadataUrl = new URL ( `${ url } /api/v2/workspaceagents/${ agentId } /watch-metadata` )
213+ const eventSource = new EventSource ( metadataUrl . toString ( ) , {
214+ headers : {
215+ "Coder-Session-Token" : token ,
216+ } ,
217+ } )
218+
219+ let disposed = false
220+ const onChange = new vscode . EventEmitter < null > ( )
221+ const watcher : AgentWatcher = {
222+ onChange : onChange . event ,
223+ dispose : ( ) => {
224+ if ( ! disposed ) {
225+ eventSource . close ( )
226+ disposed = true
227+ }
228+ } ,
229+ }
189230
190- this . agentWatchers [ agentId ] = watcher
231+ eventSource . addEventListener ( "data" , ( event ) => {
232+ try {
233+ const dataEvent = JSON . parse ( event . data )
234+ const metadata = AgentMetadataEventSchemaArray . parse ( dataEvent )
191235
192- agentMetadataEventSource . addEventListener ( "data" , ( event ) => {
193- try {
194- const dataEvent = JSON . parse ( event . data )
195- const agentMetadata = AgentMetadataEventSchemaArray . parse ( dataEvent )
236+ // Overwrite metadata if it changed.
237+ if ( JSON . stringify ( watcher . metadata ) !== JSON . stringify ( metadata ) ) {
238+ watcher . metadata = metadata
239+ onChange . fire ( null )
240+ }
241+ } catch ( error ) {
242+ watcher . error = error
243+ onChange . fire ( null )
244+ }
245+ } )
196246
197- if ( agentMetadata . length === 0 ) {
198- watcher . dispose ( )
199- }
247+ return watcher
248+ }
200249
201- // Overwrite metadata if it changed.
202- if ( JSON . stringify ( watcher . metadata ) !== JSON . stringify ( agentMetadata ) ) {
203- watcher . metadata = agentMetadata
204- this . refresh ( )
205- }
206- } catch ( error ) {
207- watcher . dispose ( )
208- }
209- } )
250+ class ErrorTreeItem extends vscode . TreeItem {
251+ constructor ( error : unknown ) {
252+ super ( "Failed to query metadata: " + errToStr ( error , "no error provided" ) , vscode . TreeItemCollapsibleState . None )
253+ this . contextValue = "coderAgentMetadata"
210254 }
211255}
212256
213- type CoderTreeItemType = "coderWorkspaceSingleAgent" | "coderWorkspaceMultipleAgents" | "coderAgent"
214-
215257class AgentMetadataTreeItem extends vscode . TreeItem {
216258 constructor ( metadataEvent : AgentMetadataEvent ) {
217259 const label =
@@ -225,6 +267,8 @@ class AgentMetadataTreeItem extends vscode.TreeItem {
225267 }
226268}
227269
270+ type CoderOpenableTreeItemType = "coderWorkspaceSingleAgent" | "coderWorkspaceMultipleAgents" | "coderAgent"
271+
228272export class OpenableTreeItem extends vscode . TreeItem {
229273 constructor (
230274 label : string ,
@@ -236,7 +280,7 @@ export class OpenableTreeItem extends vscode.TreeItem {
236280 public readonly workspaceAgent : string | undefined ,
237281 public readonly workspaceFolderPath : string | undefined ,
238282
239- contextValue : CoderTreeItemType ,
283+ contextValue : CoderOpenableTreeItemType ,
240284 ) {
241285 super ( label , collapsibleState )
242286 this . contextValue = contextValue
0 commit comments