@@ -60,6 +60,9 @@ export interface WorkspaceProviderOptions {
6060 readonly refreshIntervalMs ?: number ;
6161}
6262
63+ // Bounds fetch() retries when the session keeps changing mid-request.
64+ const MAX_FETCH_ATTEMPTS = 3 ;
65+
6366/**
6467 * Polls workspaces using the provided REST client and renders them in a tree.
6568 *
@@ -119,7 +122,6 @@ export class WorkspaceProvider
119122 this . fetching = false ;
120123 }
121124
122- // Keep polling only after a clean fetch while still signed in and visible.
123125 if (
124126 ! hadError &&
125127 ! this . disposed &&
@@ -143,65 +145,75 @@ export class WorkspaceProvider
143145 * signed out, and throws if the query fails.
144146 */
145147 private async fetch ( ) : Promise < WorkspaceTreeItem [ ] > {
146- const session = this . sessionState . getSnapshot ( ) ;
147- if ( session . kind !== "signedIn" ) {
148- return [ ] ;
149- }
148+ for ( let attempt = 0 ; attempt < MAX_FETCH_ATTEMPTS ; attempt ++ ) {
149+ if ( this . disposed ) {
150+ return [ ] ;
151+ }
152+ const session = this . sessionState . getSnapshot ( ) ;
153+ if ( session . kind !== "signedIn" ) {
154+ return [ ] ;
155+ }
150156
151- const resp = await this . client . getWorkspaces ( {
152- q : this . getWorkspacesQuery ,
153- } ) ;
157+ const resp = await this . client . getWorkspaces ( {
158+ q : this . getWorkspacesQuery ,
159+ } ) ;
154160
155- // If the session changed while the request was in flight, this result is
156- // stale. Drop it and fetch again for the current session.
157- const latest = this . sessionState . getSnapshot ( ) ;
158- if ( latest . kind !== "signedIn" || latest . revision !== session . revision ) {
159- return this . fetch ( ) ;
160- }
161+ // Session changed mid-request; this result is stale, so retry.
162+ const latest = this . sessionState . getSnapshot ( ) ;
163+ if ( latest . kind !== "signedIn" || latest . revision !== session . revision ) {
164+ continue ;
165+ }
161166
162- const workspaces = this . filterWorkspaces ( resp . workspaces , session ) ;
163- const oldWatcherIds = [ ...this . agentWatchers . keys ( ) ] ;
164- const reusedWatcherIds : string [ ] = [ ] ;
165-
166- // TODO: I think it might make more sense for the tree items to contain
167- // their own watchers, rather than recreate the tree items every time and
168- // have this separate map held outside the tree.
169- if ( this . config . showMetadata ) {
170- const agents = extractAllAgents ( workspaces ) ;
171- for ( const agent of agents ) {
172- // If we have an existing watcher, re-use it.
173- const oldWatcher = this . agentWatchers . get ( agent . id ) ;
174- if ( oldWatcher ) {
175- reusedWatcherIds . push ( agent . id ) ;
176- } else {
177- // Otherwise create a new watcher.
167+ const workspaces = this . filterWorkspaces ( resp . workspaces , session ) ;
168+ const oldWatcherIds = [ ...this . agentWatchers . keys ( ) ] ;
169+ const reusedWatcherIds : string [ ] = [ ] ;
170+
171+ // TODO: I think it might make more sense for the tree items to contain
172+ // their own watchers, rather than recreate the tree items every time
173+ // and have this separate map held outside the tree.
174+ if ( this . config . showMetadata ) {
175+ const agents = extractAllAgents ( workspaces ) ;
176+ for ( const agent of agents ) {
177+ // If we have an existing watcher, re-use it.
178+ const oldWatcher = this . agentWatchers . get ( agent . id ) ;
179+ if ( oldWatcher ) {
180+ reusedWatcherIds . push ( agent . id ) ;
181+ continue ;
182+ }
178183 const watcher = await createAgentMetadataWatcher (
179184 agent . id ,
180185 this . client ,
181186 ) ;
187+ // dispose() may have cleared the map mid-create; don't leak
188+ // this watcher.
189+ if ( this . disposed ) {
190+ watcher . dispose ( ) ;
191+ return [ ] ;
192+ }
182193 watcher . onChange ( ( ) => this . refreshTree ( ) ) ;
183194 this . agentWatchers . set ( agent . id , watcher ) ;
184195 }
185196 }
186- }
187197
188- // Dispose of watchers we ended up not reusing.
189- for ( const id of oldWatcherIds ) {
190- if ( ! reusedWatcherIds . includes ( id ) ) {
191- this . agentWatchers . get ( id ) ?. dispose ( ) ;
192- this . agentWatchers . delete ( id ) ;
198+ // Dispose of watchers we ended up not reusing.
199+ for ( const id of oldWatcherIds ) {
200+ if ( ! reusedWatcherIds . includes ( id ) ) {
201+ this . agentWatchers . get ( id ) ?. dispose ( ) ;
202+ this . agentWatchers . delete ( id ) ;
203+ }
193204 }
194- }
195205
196- // Create tree items for each workspace
197- return workspaces . map (
198- ( workspace : Workspace ) =>
199- new WorkspaceTreeItem (
200- workspace ,
201- this . config . showOwner ,
202- this . config . showMetadata ,
203- ) ,
204- ) ;
206+ return workspaces . map (
207+ ( workspace : Workspace ) =>
208+ new WorkspaceTreeItem (
209+ workspace ,
210+ this . config . showOwner ,
211+ this . config . showMetadata ,
212+ ) ,
213+ ) ;
214+ }
215+ // Session changed on every attempt; the next refresh will catch up.
216+ return [ ] ;
205217 }
206218
207219 private filterWorkspaces (
@@ -244,10 +256,7 @@ export class WorkspaceProvider
244256 }
245257 }
246258
247- /**
248- * Schedule a refresh if one is not already scheduled or underway and a
249- * timeout length was provided.
250- */
259+ /** Schedule the next poll, unless one is pending or no interval is set. */
251260 private maybeScheduleRefresh ( ) {
252261 if ( this . options . refreshIntervalMs && ! this . timeout ) {
253262 this . timeout = setTimeout ( ( ) => {
@@ -370,6 +379,7 @@ export class WorkspaceProvider
370379 this . disposed = true ;
371380 this . clearState ( ) ;
372381 this . sessionChangeDisposable . dispose ( ) ;
382+ this . _onDidChangeTreeData . dispose ( ) ;
373383 }
374384}
375385
0 commit comments