@@ -11,6 +11,11 @@ import type { WorkspaceProvider } from "../workspace/workspacesProvider";
1111
1212import type { Deployment , DeploymentWithAuth } from "./types" ;
1313
14+ /**
15+ * Internal state type that allows mutation of user property.
16+ */
17+ type DeploymentWithUser = Deployment & { user ?: User } ;
18+
1419/**
1520 * Manages deployment state for the extension.
1621 *
@@ -28,9 +33,9 @@ export class DeploymentManager implements vscode.Disposable {
2833 private readonly contextManager : ContextManager ;
2934 private readonly logger : Logger ;
3035
31- private currentDeployment : ( Deployment & { user ?: User } ) | null = null ;
32- private authListenerDisposable : vscode . Disposable | undefined ;
33- private crossWindowSyncDisposable : vscode . Disposable | undefined ;
36+ #deployment: DeploymentWithUser | null = null ;
37+ # authListenerDisposable: vscode . Disposable | undefined ;
38+ # crossWindowSyncDisposable: vscode . Disposable | undefined ;
3439
3540 private constructor (
3641 serviceContainer : ServiceContainer ,
@@ -61,14 +66,14 @@ export class DeploymentManager implements vscode.Disposable {
6166 * Get the current deployment state.
6267 */
6368 public getCurrentDeployment ( ) : Deployment | null {
64- return this . currentDeployment ;
69+ return this . #deployment ;
6570 }
6671
6772 /**
6873 * Check if we have an authenticated deployment (with a valid user).
6974 */
7075 public isAuthenticated ( ) : boolean {
71- return this . currentDeployment ?. user !== undefined ;
76+ return this . #deployment ?. user !== undefined ;
7277 }
7378
7479 /**
@@ -78,8 +83,7 @@ export class DeploymentManager implements vscode.Disposable {
7883 public async changeDeployment (
7984 deployment : DeploymentWithAuth & { user : User } ,
8085 ) : Promise < void > {
81- this . setDeployment ( deployment ) ;
82-
86+ this . setDeploymentInternal ( deployment ) ;
8387 await this . persistDeployment ( deployment ) ;
8488 }
8589
@@ -91,85 +95,90 @@ export class DeploymentManager implements vscode.Disposable {
9195 public async setDeploymentAndValidate (
9296 deployment : Deployment & { token ?: string } ,
9397 ) : Promise < void > {
94- this . setDeployment ( { ...deployment } ) ;
95-
98+ this . setDeploymentInternal ( deployment ) ;
9699 await this . tryFetchAndUpgradeUser ( ) ;
97100 }
98101
99- private setDeployment ( deployment : DeploymentWithAuth ) : void {
100- this . currentDeployment = { ...deployment } ;
101- if ( deployment . token === undefined ) {
102- this . client . setHost ( deployment . url ) ;
103- } else {
104- this . client . setCredentials ( deployment . url , deployment . token ) ;
105- }
106- this . registerAuthListener ( deployment . safeHostname ) ;
107- this . updateAuthContexts ( deployment . user ) ;
108- this . refreshWorkspaces ( ) ;
109- }
110-
111102 /**
112103 * Clears the current deployment.
113104 */
114105 public async clearDeployment ( ) : Promise < void > {
115- this . client . setCredentials ( undefined , undefined ) ;
106+ this . #authListenerDisposable?. dispose ( ) ;
107+ this . #authListenerDisposable = undefined ;
108+ this . #deployment = null ;
116109
117- this . authListenerDisposable ?. dispose ( ) ;
118- this . authListenerDisposable = undefined ;
119- this . currentDeployment = null ;
120-
121- this . updateAuthContexts ( undefined ) ;
110+ this . client . setCredentials ( undefined , undefined ) ;
111+ this . updateAuthContexts ( ) ;
122112 this . refreshWorkspaces ( ) ;
113+
123114 await this . secretsManager . setCurrentDeployment ( undefined ) ;
124115 }
125116
126117 public dispose ( ) : void {
127- this . authListenerDisposable ?. dispose ( ) ;
128- this . crossWindowSyncDisposable ?. dispose ( ) ;
118+ this . # authListenerDisposable?. dispose ( ) ;
119+ this . # crossWindowSyncDisposable?. dispose ( ) ;
129120 }
130121
131- private subscribeToCrossWindowChanges ( ) : void {
132- this . crossWindowSyncDisposable =
133- this . secretsManager . onDidChangeCurrentDeployment (
134- async ( { deployment } ) => {
135- if ( this . isAuthenticated ( ) ) {
136- // Ignore if we are already authenticated
137- return ;
138- }
122+ /**
123+ * Internal method to set deployment state with all side effects.
124+ * - Updates client credentials
125+ * - Re-registers auth listener if hostname changed
126+ * - Updates auth contexts
127+ * - Refreshes workspaces
128+ */
129+ private setDeploymentInternal ( deployment : DeploymentWithAuth ) : void {
130+ this . #deployment = { ...deployment } ;
139131
140- if ( deployment ) {
141- this . logger . info ( "Deployment changed from another window" ) ;
142- const auth = await this . secretsManager . getSessionAuth (
143- deployment . safeHostname ,
144- ) ;
145- await this . setDeploymentAndValidate ( {
146- ...deployment ,
147- token : auth ?. token ,
148- } ) ;
149- }
150- } ,
151- ) ;
132+ // Update client credentials
133+ if ( deployment . token !== undefined ) {
134+ this . client . setCredentials ( deployment . url , deployment . token ) ;
135+ } else {
136+ this . client . setHost ( deployment . url ) ;
137+ }
138+
139+ this . registerAuthListener ( ) ;
140+ this . updateAuthContexts ( ) ;
141+ this . refreshWorkspaces ( ) ;
142+ }
143+
144+ /**
145+ * Upgrade the current deployment with a user.
146+ * Use this when the user has been fetched after initial deployment setup.
147+ */
148+ private upgradeWithUser ( user : User ) : void {
149+ if ( ! this . #deployment) {
150+ return ;
151+ }
152+
153+ this . #deployment. user = user ;
154+ this . updateAuthContexts ( ) ;
155+ this . refreshWorkspaces ( ) ;
152156 }
153157
154158 /**
155- * Register auth listener for the given deployment hostname .
159+ * Register auth listener for the current deployment.
156160 * Updates credentials when they change (token refresh, cross-window sync).
157161 */
158- private registerAuthListener ( safeHostname : string ) : void {
159- this . authListenerDisposable ?. dispose ( ) ;
162+ private registerAuthListener ( ) : void {
163+ if ( ! this . #deployment) {
164+ return ;
165+ }
160166
167+ // Capture hostname at registration time for the guard clause
168+ const safeHostname = this . #deployment. safeHostname ;
169+
170+ this . #authListenerDisposable?. dispose ( ) ;
161171 this . logger . debug ( "Registering auth listener for hostname" , safeHostname ) ;
162- this . authListenerDisposable = this . secretsManager . onDidChangeSessionAuth (
172+ this . # authListenerDisposable = this . secretsManager . onDidChangeSessionAuth (
163173 safeHostname ,
164174 async ( auth ) => {
165- if ( this . currentDeployment ?. safeHostname !== safeHostname ) {
175+ if ( this . #deployment ?. safeHostname !== safeHostname ) {
166176 return ;
167177 }
178+
168179 if ( auth ) {
169180 this . client . setCredentials ( auth . url , auth . token ) ;
170-
171- // If we don't have a user yet, try to fetch one
172- if ( ! this . currentDeployment ?. user ) {
181+ if ( ! this . isAuthenticated ( ) ) {
173182 await this . tryFetchAndUpgradeUser ( ) ;
174183 }
175184 } else {
@@ -179,33 +188,54 @@ export class DeploymentManager implements vscode.Disposable {
179188 ) ;
180189 }
181190
191+ private subscribeToCrossWindowChanges ( ) : void {
192+ this . #crossWindowSyncDisposable =
193+ this . secretsManager . onDidChangeCurrentDeployment (
194+ async ( { deployment } ) => {
195+ if ( this . isAuthenticated ( ) ) {
196+ // Ignore if we are already authenticated
197+ return ;
198+ }
199+
200+ if ( deployment ) {
201+ this . logger . info ( "Deployment changed from another window" ) ;
202+ const auth = await this . secretsManager . getSessionAuth (
203+ deployment . safeHostname ,
204+ ) ;
205+ await this . setDeploymentAndValidate ( {
206+ ...deployment ,
207+ token : auth ?. token ,
208+ } ) ;
209+ }
210+ } ,
211+ ) ;
212+ }
213+
182214 /**
183215 * Try to fetch the authenticated user and upgrade the deployment state.
184216 */
185217 private async tryFetchAndUpgradeUser ( ) : Promise < void > {
186- if ( ! this . currentDeployment || this . currentDeployment . user ) {
218+ if ( ! this . #deployment || this . isAuthenticated ( ) ) {
187219 return ;
188220 }
189221
190- const safeHostname = this . currentDeployment . safeHostname ;
222+ const safeHostname = this . #deployment . safeHostname ;
191223
192224 try {
193225 const user = await this . client . getAuthenticatedUser ( ) ;
194226
195227 // Re-validate deployment hasn't changed during await
196- if ( this . currentDeployment ?. safeHostname !== safeHostname ) {
228+ if ( this . #deployment ?. safeHostname !== safeHostname ) {
197229 this . logger . debug (
198230 "Deployment changed during user fetch, discarding result" ,
199231 ) ;
200232 return ;
201233 }
202234
203- this . currentDeployment . user = user ;
204- this . updateAuthContexts ( user ) ;
205- this . refreshWorkspaces ( ) ;
235+ this . upgradeWithUser ( user ) ;
206236
207237 // Persist with user
208- await this . persistDeployment ( this . currentDeployment ) ;
238+ await this . persistDeployment ( this . #deployment ) ;
209239 } catch ( e ) {
210240 this . logger . warn ( "Failed to fetch user:" , e ) ;
211241 }
@@ -214,7 +244,8 @@ export class DeploymentManager implements vscode.Disposable {
214244 /**
215245 * Update authentication-related contexts.
216246 */
217- private updateAuthContexts ( user : User | undefined ) : void {
247+ private updateAuthContexts ( ) : void {
248+ const user = this . #deployment?. user ;
218249 this . contextManager . set ( "coder.authenticated" , Boolean ( user ) ) ;
219250 const isOwner = user ?. roles . some ( ( r ) => r . name === "owner" ) ?? false ;
220251 this . contextManager . set ( "coder.isOwner" , isOwner ) ;
@@ -232,9 +263,7 @@ export class DeploymentManager implements vscode.Disposable {
232263 /**
233264 * Persist deployment to storage for cross-window sync.
234265 */
235- private async persistDeployment (
236- deployment : DeploymentWithAuth ,
237- ) : Promise < void > {
266+ private async persistDeployment ( deployment : Deployment ) : Promise < void > {
238267 await this . secretsManager . setCurrentDeployment ( deployment ) ;
239268 await this . mementoManager . addToUrlHistory ( deployment . url ) ;
240269 }
0 commit comments