diff --git a/src/workspacesProvider.ts b/src/workspacesProvider.ts index f485b9d5..fba0f65a 100644 --- a/src/workspacesProvider.ts +++ b/src/workspacesProvider.ts @@ -14,6 +14,7 @@ export enum WorkspaceQuery { export class WorkspaceProvider implements vscode.TreeDataProvider<vscode.TreeItem> { private workspaces: WorkspaceTreeItem[] = [] private agentWatchers: Record<WorkspaceAgent["id"], { dispose: () => void; metadata?: AgentMetadataEvent[] }> = {} + private fetching = false constructor( private readonly getWorkspacesQuery: WorkspaceQuery, @@ -22,26 +23,63 @@ export class WorkspaceProvider implements vscode.TreeDataProvider<vscode.TreeIte this.fetchAndRefresh() } - // fetchAndRefrehsh fetches new workspaces then re-renders the entire tree. + // fetchAndRefresh fetches new workspaces then re-renders the entire tree. + // Trying to call this while already refreshing is a no-op and will return + // immediately. async fetchAndRefresh() { - const token = await this.storage.getSessionToken() - const workspacesTreeItem: WorkspaceTreeItem[] = [] + if (this.fetching) { + return + } + this.fetching = true + Object.values(this.agentWatchers).forEach((watcher) => watcher.dispose()) - // If the URL is set then we are logged in. - if (this.storage.getURL()) { - const resp = await getWorkspaces({ q: this.getWorkspacesQuery }) - resp.workspaces.forEach((workspace) => { - const showMetadata = this.getWorkspacesQuery === WorkspaceQuery.Mine - if (showMetadata && token) { - const agents = extractAgents(workspace) - agents.forEach((agent) => this.monitorMetadata(agent.id, token)) // monitor metadata for all agents - } - const treeItem = new WorkspaceTreeItem(workspace, this.getWorkspacesQuery === WorkspaceQuery.All, showMetadata) - workspacesTreeItem.push(treeItem) - }) + + try { + this.workspaces = await this.fetch() + } catch (error) { + this.workspaces = [] } - this.workspaces = workspacesTreeItem + this.refresh() + this.fetching = false + } + + /** + * Fetch workspaces and turn them into tree items. Throw an error if not + * logged in or the query fails. + */ + async fetch(): Promise<WorkspaceTreeItem[]> { + // Assume that no URL or no token means we are not logged in. + const url = this.storage.getURL() + const token = await this.storage.getSessionToken() + if (!url || !token) { + throw new Error("not logged in") + } + + const resp = await getWorkspaces({ q: this.getWorkspacesQuery }) + + // We could have logged out while waiting for the query, or logged into a + // different deployment. + const url2 = this.storage.getURL() + const token2 = await this.storage.getSessionToken() + if (!url2 || !token2) { + throw new Error("not logged in") + } else if (url !== url2) { + // In this case we need to fetch from the new deployment instead. + // TODO: It would be better to cancel this fetch when that happens, + // because this means we have to wait for the old fetch to finish before + // finally getting workspaces for the new one. + return this.fetch() + } + + return resp.workspaces.map((workspace) => { + const showMetadata = this.getWorkspacesQuery === WorkspaceQuery.Mine + if (showMetadata) { + const agents = extractAgents(workspace) + agents.forEach((agent) => this.monitorMetadata(agent.id, url, token2)) // monitor metadata for all agents + } + return new WorkspaceTreeItem(workspace, this.getWorkspacesQuery === WorkspaceQuery.All, showMetadata) + }) } private _onDidChangeTreeData: vscode.EventEmitter<vscode.TreeItem | undefined | null | void> = @@ -78,8 +116,8 @@ export class WorkspaceProvider implements vscode.TreeDataProvider<vscode.TreeIte // monitorMetadata opens an SSE endpoint to monitor metadata on the specified // agent and registers a disposer that can be used to stop the watch. - monitorMetadata(agentId: WorkspaceAgent["id"], token: string): void { - const agentMetadataURL = new URL(`${this.storage.getURL()}/api/v2/workspaceagents/${agentId}/watch-metadata`) + monitorMetadata(agentId: WorkspaceAgent["id"], url: string, token: string): void { + const agentMetadataURL = new URL(`${url}/api/v2/workspaceagents/${agentId}/watch-metadata`) const agentMetadataEventSource = new EventSource(agentMetadataURL.toString(), { headers: { "Coder-Session-Token": token,