Skip to content

Commit 49fbe38

Browse files
committed
Reuse agent metadata watchers
This has two effects: 1. Lower overhead from establishing the watchers over and over. 2. No more flashing in the tree as the watchers get recreated and we wait for the first update.
1 parent 5ab2eb5 commit 49fbe38

File tree

2 files changed

+109
-52
lines changed

2 files changed

+109
-52
lines changed

src/api-helper.ts

Lines changed: 17 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,12 +1,26 @@
11
import { Workspace, WorkspaceAgent } from "coder/site/src/api/typesGenerated"
22
import { z } from "zod"
33

4+
export function errToStr(error: unknown, def: string) {
5+
if (error instanceof Error && error.message) {
6+
return error.message
7+
}
8+
if (typeof error === "string" && error.trim().length > 0) {
9+
return error
10+
}
11+
return def
12+
}
13+
14+
export function extractAllAgents(workspaces: Workspace[]): WorkspaceAgent[] {
15+
return workspaces.reduce((acc, workspace) => {
16+
return acc.concat(extractAgents(workspace))
17+
}, [] as WorkspaceAgent[])
18+
}
19+
420
export function extractAgents(workspace: Workspace): WorkspaceAgent[] {
5-
const agents = workspace.latest_build.resources.reduce((acc, resource) => {
21+
return workspace.latest_build.resources.reduce((acc, resource) => {
622
return acc.concat(resource.agents || [])
723
}, [] as WorkspaceAgent[])
8-
9-
return agents
1024
}
1125

1226
export const AgentMetadataEventSchema = z.object({

src/workspacesProvider.ts

Lines changed: 92 additions & 49 deletions
Original file line numberDiff line numberDiff line change
@@ -3,15 +3,26 @@ import { Workspace, WorkspaceAgent } from "coder/site/src/api/typesGenerated"
33
import EventSource from "eventsource"
44
import * as path from "path"
55
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"
713
import { Storage } from "./storage"
814

915
export 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

1627
export 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,37 @@ 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()
101131
}
132+
})
133+
134+
return resp.workspaces.map((workspace) => {
102135
return new WorkspaceTreeItem(workspace, this.getWorkspacesQuery === WorkspaceQuery.All, showMetadata)
103136
})
104137
}
@@ -157,61 +190,71 @@ export class WorkspaceProvider implements vscode.TreeDataProvider<vscode.TreeIte
157190
)
158191
return Promise.resolve(agentTreeItems)
159192
} 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 || []
161198
return Promise.resolve(savedMetadata.map((metadata) => new AgentMetadataTreeItem(metadata)))
162199
}
163200

164201
return Promise.resolve([])
165202
}
166203
return Promise.resolve(this.workspaces)
167204
}
205+
}
168206

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+
}
178229

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)
189234

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+
})
191245

192-
agentMetadataEventSource.addEventListener("data", (event) => {
193-
try {
194-
const dataEvent = JSON.parse(event.data)
195-
const agentMetadata = AgentMetadataEventSchemaArray.parse(dataEvent)
246+
return watcher
247+
}
196248

197-
if (agentMetadata.length === 0) {
198-
watcher.dispose()
199-
}
249+
type CoderTreeItemType = "coderWorkspaceSingleAgent" | "coderWorkspaceMultipleAgents" | "coderAgent"
200250

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"
210255
}
211256
}
212257

213-
type CoderTreeItemType = "coderWorkspaceSingleAgent" | "coderWorkspaceMultipleAgents" | "coderAgent"
214-
215258
class AgentMetadataTreeItem extends vscode.TreeItem {
216259
constructor(metadataEvent: AgentMetadataEvent) {
217260
const label =

0 commit comments

Comments
 (0)