@@ -3,15 +3,26 @@ import { Workspace, WorkspaceAgent } from "coder/site/src/api/typesGenerated"
3
3
import EventSource from "eventsource"
4
4
import * as path from "path"
5
5
import * 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"
7
13
import { Storage } from "./storage"
8
14
9
15
export enum WorkspaceQuery {
10
16
Mine = "owner:me" ,
11
17
All = "" ,
12
18
}
13
19
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
+ }
15
26
16
27
export class WorkspaceProvider implements vscode . TreeDataProvider < vscode . TreeItem > {
17
28
private workspaces : WorkspaceTreeItem [ ] = [ ]
@@ -39,9 +50,6 @@ export class WorkspaceProvider implements vscode.TreeDataProvider<vscode.TreeIte
39
50
}
40
51
this . fetching = true
41
52
42
- // TODO: It would be better to reuse these.
43
- Object . values ( this . agentWatchers ) . forEach ( ( watcher ) => watcher . dispose ( ) )
44
-
45
53
// It is possible we called fetchAndRefresh() manually (through the button
46
54
// for example), in which case we might still have a pending refresh that
47
55
// needs to be cleared.
@@ -93,12 +101,37 @@ export class WorkspaceProvider implements vscode.TreeDataProvider<vscode.TreeIte
93
101
return this . fetch ( )
94
102
}
95
103
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 ( )
101
131
}
132
+ } )
133
+
134
+ return resp . workspaces . map ( ( workspace ) => {
102
135
return new WorkspaceTreeItem ( workspace , this . getWorkspacesQuery === WorkspaceQuery . All , showMetadata )
103
136
} )
104
137
}
@@ -157,61 +190,71 @@ export class WorkspaceProvider implements vscode.TreeDataProvider<vscode.TreeIte
157
190
)
158
191
return Promise . resolve ( agentTreeItems )
159
192
} else if ( element instanceof AgentTreeItem ) {
160
- const savedMetadata = this . agentWatchers [ element . agent . id ] ?. metadata || [ ]
193
+ const watcher = this . agentWatchers [ element . agent . id ]
194
+ if ( watcher ?. error ) {
195
+ return Promise . resolve ( [ new ErrorTreeItem ( watcher . error ) ] )
196
+ }
197
+ const savedMetadata = watcher ?. metadata || [ ]
161
198
return Promise . resolve ( savedMetadata . map ( ( metadata ) => new AgentMetadataTreeItem ( metadata ) ) )
162
199
}
163
200
164
201
return Promise . resolve ( [ ] )
165
202
}
166
203
return Promise . resolve ( this . workspaces )
167
204
}
205
+ }
168
206
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
- } )
207
+ // monitorMetadata opens an SSE endpoint to monitor metadata on the specified
208
+ // agent and registers a watcher that can be disposed to stop the watch and
209
+ // emits an event when the metadata changes.
210
+ function monitorMetadata ( agentId : WorkspaceAgent [ "id" ] , url : string , token : string ) : AgentWatcher {
211
+ const metadataUrl = new URL ( `${ url } /api/v2/workspaceagents/${ agentId } /watch-metadata` )
212
+ const eventSource = new EventSource ( metadataUrl . toString ( ) , {
213
+ headers : {
214
+ "Coder-Session-Token" : token ,
215
+ } ,
216
+ } )
217
+
218
+ let disposed = false
219
+ const onChange = new vscode . EventEmitter < null > ( )
220
+ const watcher : AgentWatcher = {
221
+ onChange : onChange . event ,
222
+ dispose : ( ) => {
223
+ if ( ! disposed ) {
224
+ eventSource . close ( )
225
+ disposed = true
226
+ }
227
+ } ,
228
+ }
178
229
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
- }
230
+ eventSource . addEventListener ( "data" , ( event ) => {
231
+ try {
232
+ const dataEvent = JSON . parse ( event . data )
233
+ const metadata = AgentMetadataEventSchemaArray . parse ( dataEvent )
189
234
190
- this . agentWatchers [ agentId ] = watcher
235
+ // Overwrite metadata if it changed.
236
+ if ( JSON . stringify ( watcher . metadata ) !== JSON . stringify ( metadata ) ) {
237
+ watcher . metadata = metadata
238
+ onChange . fire ( null )
239
+ }
240
+ } catch ( error ) {
241
+ watcher . error = error
242
+ onChange . fire ( null )
243
+ }
244
+ } )
191
245
192
- agentMetadataEventSource . addEventListener ( "data" , ( event ) => {
193
- try {
194
- const dataEvent = JSON . parse ( event . data )
195
- const agentMetadata = AgentMetadataEventSchemaArray . parse ( dataEvent )
246
+ return watcher
247
+ }
196
248
197
- if ( agentMetadata . length === 0 ) {
198
- watcher . dispose ( )
199
- }
249
+ type CoderTreeItemType = "coderWorkspaceSingleAgent" | "coderWorkspaceMultipleAgents" | "coderAgent"
200
250
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
- } )
251
+ class ErrorTreeItem extends vscode . TreeItem {
252
+ constructor ( error : unknown ) {
253
+ super ( "Failed to query metadata: " + errToStr ( error , "no error provided" ) , vscode . TreeItemCollapsibleState . None )
254
+ this . contextValue = "coderAgentMetadata"
210
255
}
211
256
}
212
257
213
- type CoderTreeItemType = "coderWorkspaceSingleAgent" | "coderWorkspaceMultipleAgents" | "coderAgent"
214
-
215
258
class AgentMetadataTreeItem extends vscode . TreeItem {
216
259
constructor ( metadataEvent : AgentMetadataEvent ) {
217
260
const label =
0 commit comments