@@ -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,38 @@ 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 ( )
131
+ delete this . agentWatchers [ id ]
101
132
}
133
+ } )
134
+
135
+ return resp . workspaces . map ( ( workspace ) => {
102
136
return new WorkspaceTreeItem ( workspace , this . getWorkspacesQuery === WorkspaceQuery . All , showMetadata )
103
137
} )
104
138
}
@@ -157,61 +191,69 @@ export class WorkspaceProvider implements vscode.TreeDataProvider<vscode.TreeIte
157
191
)
158
192
return Promise . resolve ( agentTreeItems )
159
193
} 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 || [ ]
161
199
return Promise . resolve ( savedMetadata . map ( ( metadata ) => new AgentMetadataTreeItem ( metadata ) ) )
162
200
}
163
201
164
202
return Promise . resolve ( [ ] )
165
203
}
166
204
return Promise . resolve ( this . workspaces )
167
205
}
206
+ }
168
207
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
+ }
189
230
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 )
191
235
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
+ } )
196
246
197
- if ( agentMetadata . length === 0 ) {
198
- watcher . dispose ( )
199
- }
247
+ return watcher
248
+ }
200
249
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"
210
254
}
211
255
}
212
256
213
- type CoderTreeItemType = "coderWorkspaceSingleAgent" | "coderWorkspaceMultipleAgents" | "coderAgent"
214
-
215
257
class AgentMetadataTreeItem extends vscode . TreeItem {
216
258
constructor ( metadataEvent : AgentMetadataEvent ) {
217
259
const label =
@@ -225,6 +267,8 @@ class AgentMetadataTreeItem extends vscode.TreeItem {
225
267
}
226
268
}
227
269
270
+ type CoderOpenableTreeItemType = "coderWorkspaceSingleAgent" | "coderWorkspaceMultipleAgents" | "coderAgent"
271
+
228
272
export class OpenableTreeItem extends vscode . TreeItem {
229
273
constructor (
230
274
label : string ,
@@ -236,7 +280,7 @@ export class OpenableTreeItem extends vscode.TreeItem {
236
280
public readonly workspaceAgent : string | undefined ,
237
281
public readonly workspaceFolderPath : string | undefined ,
238
282
239
- contextValue : CoderTreeItemType ,
283
+ contextValue : CoderOpenableTreeItemType ,
240
284
) {
241
285
super ( label , collapsibleState )
242
286
this . contextValue = contextValue
0 commit comments