1+ import * as vscode from "vscode" ;
2+
13import { CoderApi } from "../api/coderApi" ;
24import {
35 CONFIG_CHANGE_DEBOUNCE_MS ,
@@ -11,7 +13,6 @@ import { type Logger } from "../logging/logger";
1113import { type OAuthSessionManager } from "../oauth/sessionManager" ;
1214import { getAuthConfigWatchSettings } from "../settings/authConfig" ;
1315import { type TelemetryService } from "../telemetry/service" ;
14- import { type WorkspaceProvider } from "../workspace/workspacesProvider" ;
1516
1617import {
1718 DeploymentSchema ,
@@ -20,7 +21,24 @@ import {
2021} from "./types" ;
2122
2223import type { User } from "coder/site/src/api/typesGenerated" ;
23- import type * as vscode from "vscode" ;
24+
25+ import type {
26+ WorkspaceSessionSnapshot ,
27+ WorkspaceSessionState ,
28+ } from "../workspace/session" ;
29+
30+ type DeploymentSessionSnapshot =
31+ | {
32+ readonly kind : "signedOut" ;
33+ readonly revision : number ;
34+ readonly deployment : Deployment | null ;
35+ }
36+ | {
37+ readonly kind : "signedIn" ;
38+ readonly revision : number ;
39+ readonly deployment : Deployment ;
40+ readonly user : User ;
41+ } ;
2442
2543/**
2644 * Manages deployment state for the extension.
@@ -31,19 +49,25 @@ import type * as vscode from "vscode";
3149 * - OAuth session management
3250 * - Auth listener registration
3351 * - Context updates (coder.authenticated, coder.isOwner)
34- * - Workspace provider refresh
3552 * - Cross-window sync handling
3653 */
37- export class DeploymentManager implements vscode . Disposable {
54+ export class DeploymentManager
55+ implements vscode . Disposable , WorkspaceSessionState
56+ {
3857 private readonly secretsManager : SecretsManager ;
3958 private readonly mementoManager : MementoManager ;
4059 private readonly contextManager : ContextManager ;
4160 private readonly logger : Logger ;
4261 private readonly telemetryService : TelemetryService ;
4362
44- #deployment: Deployment | null = null ;
45- #authedUser: User | null = null ;
46- #authStateVersion = 0 ;
63+ #session: DeploymentSessionSnapshot = {
64+ kind : "signedOut" ,
65+ revision : 0 ,
66+ deployment : null ,
67+ } ;
68+ readonly #onDidChangeWorkspaceSession =
69+ new vscode . EventEmitter < WorkspaceSessionSnapshot > ( ) ;
70+ public readonly onDidChange = this . #onDidChangeWorkspaceSession. event ;
4771 #disposed = false ;
4872 #authListenerDisposable: vscode . Disposable | undefined ;
4973 #authConfigDisposable: vscode . Disposable | undefined ;
@@ -55,7 +79,6 @@ export class DeploymentManager implements vscode.Disposable {
5579 serviceContainer : ServiceContainer ,
5680 private readonly client : CoderApi ,
5781 private readonly oauthSessionManager : OAuthSessionManager ,
58- private readonly workspaceProviders : WorkspaceProvider [ ] ,
5982 ) {
6083 this . secretsManager = serviceContainer . getSecretsManager ( ) ;
6184 this . mementoManager = serviceContainer . getMementoManager ( ) ;
@@ -68,13 +91,11 @@ export class DeploymentManager implements vscode.Disposable {
6891 serviceContainer : ServiceContainer ,
6992 client : CoderApi ,
7093 oauthSessionManager : OAuthSessionManager ,
71- workspaceProviders : WorkspaceProvider [ ] ,
7294 ) : DeploymentManager {
7395 const manager = new DeploymentManager (
7496 serviceContainer ,
7597 client ,
7698 oauthSessionManager ,
77- workspaceProviders ,
7899 ) ;
79100 manager . subscribeToAuthConfigChanges ( ) ;
80101 manager . subscribeToCrossWindowChanges ( ) ;
@@ -85,22 +106,31 @@ export class DeploymentManager implements vscode.Disposable {
85106 * Get the current deployment state.
86107 */
87108 public getCurrentDeployment ( ) : Deployment | null {
88- return this . #deployment;
109+ return this . #session . deployment ;
89110 }
90111
91112 public getCurrentUserId ( ) : string | undefined {
92- return this . #authedUser?. id ;
113+ return this . #session. kind === "signedIn"
114+ ? this . #session. user . id
115+ : undefined ;
93116 }
94117
95- public getAuthStateVersion ( ) : number {
96- return this . #authStateVersion;
118+ public getSnapshot ( ) : WorkspaceSessionSnapshot {
119+ if ( this . #session. kind === "signedIn" ) {
120+ return {
121+ kind : "signedIn" ,
122+ revision : this . #session. revision ,
123+ userId : this . #session. user . id ,
124+ } ;
125+ }
126+ return { kind : "signedOut" , revision : this . #session. revision } ;
97127 }
98128
99129 /**
100130 * Check if we have an authenticated deployment (does not guarantee that the current auth data is valid).
101131 */
102132 public isAuthenticated ( ) : boolean {
103- return this . contextManager . get ( "coder.authenticated" ) ;
133+ return this . #session . kind === "signedIn" ;
104134 }
105135
106136 /**
@@ -112,8 +142,7 @@ export class DeploymentManager implements vscode.Disposable {
112142 public async verifyAndApplyDeployment (
113143 deployment : Deployment & { token ?: string } ,
114144 ) : Promise < boolean > {
115- const deploymentBefore = this . #deployment;
116- const authStateVersionBefore = this . #authStateVersion;
145+ const sessionBefore = this . #session;
117146 const token =
118147 deployment . token ??
119148 ( await this . secretsManager . getSessionAuth ( deployment . safeHostname ) )
@@ -122,9 +151,7 @@ export class DeploymentManager implements vscode.Disposable {
122151
123152 try {
124153 const user = await tempClient . getAuthenticatedUser ( ) ;
125- if (
126- this . #hasStateChangedSince( deploymentBefore , authStateVersionBefore )
127- ) {
154+ if ( this . #hasStateChangedSince( sessionBefore ) ) {
128155 return false ;
129156 }
130157 await this . setDeployment ( { ...deployment , token, user } ) ;
@@ -138,15 +165,11 @@ export class DeploymentManager implements vscode.Disposable {
138165 }
139166
140167 /** True if disposal, login, or a deployment switch raced our await. */
141- #hasStateChangedSince(
142- deploymentBefore : Deployment | null ,
143- authStateVersionBefore : number ,
144- ) : boolean {
168+ #hasStateChangedSince( sessionBefore : DeploymentSessionSnapshot ) : boolean {
145169 return (
146170 this . #disposed ||
147171 this . isAuthenticated ( ) ||
148- this . #deployment !== deploymentBefore ||
149- this . #authStateVersion !== authStateVersionBefore
172+ this . #session !== sessionBefore
150173 ) ;
151174 }
152175
@@ -162,33 +185,21 @@ export class DeploymentManager implements vscode.Disposable {
162185 user : deployment . user . username ,
163186 } ) ;
164187 const deploymentWithoutAuth = DeploymentSchema . parse ( deployment ) ;
165- const ourRef = this . commitDeploymentState (
166- deploymentWithoutAuth ,
167- deployment . user ,
168- ) ;
169- const authStateVersion = this . #authStateVersion;
170188 this . telemetryService . setDeploymentUrl ( deployment . url ) ;
171-
172- // Updates client credentials
173189 if ( deployment . token === undefined ) {
174190 this . client . setHost ( deployment . url ) ;
175191 } else {
176192 this . client . setCredentials ( deployment . url , deployment . token ) ;
177193 }
178194
179- // Register auth listener before setDeployment so background token refresh
180- // can update client credentials via the listener
195+ const ourRef = this . setSignedIn ( deploymentWithoutAuth , deployment . user ) ;
196+ // Register before OAuth setup so background token refresh can update client credentials.
181197 this . registerAuthListener ( ) ;
182- // Contexts must be set before refresh (providers check isAuthenticated)
183198 this . updateAuthContexts ( deployment . user ) ;
184- this . refreshWorkspaces ( ) ;
185199
186200 await this . oauthSessionManager . setDeployment ( deploymentWithoutAuth ) ;
187201 // Bail if a concurrent write took over during the await.
188- if (
189- this . #deployment !== ourRef ||
190- this . #authStateVersion !== authStateVersion
191- ) {
202+ if ( this . #session !== ourRef ) {
192203 return ;
193204 }
194205 await this . persistDeployment ( deploymentWithoutAuth ) ;
@@ -198,11 +209,14 @@ export class DeploymentManager implements vscode.Disposable {
198209 * Clears the current deployment.
199210 */
200211 public async clearDeployment ( ) : Promise < void > {
201- this . logger . debug ( "Clearing deployment" , this . #deployment?. safeHostname ) ;
202- this . suspendSession ( ) ;
212+ this . logger . debug (
213+ "Clearing deployment" ,
214+ this . #session. deployment ?. safeHostname ,
215+ ) ;
203216 this . #authListenerDisposable?. dispose ( ) ;
204217 this . #authListenerDisposable = undefined ;
205- this . commitDeploymentState ( null , null ) ;
218+ this . setSignedOut ( null ) ;
219+ this . clearSessionSideEffects ( ) ;
206220 this . telemetryService . setDeploymentUrl ( "" ) ;
207221
208222 await this . secretsManager . setCurrentDeployment ( undefined ) ;
@@ -213,27 +227,22 @@ export class DeploymentManager implements vscode.Disposable {
213227 * Auth listener remains active so recovery can happen automatically if tokens update.
214228 */
215229 public suspendSession ( ) : void {
216- this . commitDeploymentState ( this . #deployment, null ) ;
230+ this . setSignedOut ( this . #session. deployment ) ;
231+ this . clearSessionSideEffects ( ) ;
232+ }
233+
234+ private clearSessionSideEffects ( ) : void {
217235 this . oauthSessionManager . clearDeployment ( ) ;
218236 this . client . setCredentials ( undefined , undefined ) ;
219237 this . updateAuthContexts ( undefined ) ;
220- this . clearWorkspaces ( ) ;
221- }
222-
223- /**
224- * Clear all workspace providers without fetching.
225- */
226- private clearWorkspaces ( ) : void {
227- for ( const provider of this . workspaceProviders ) {
228- provider . clear ( ) ;
229- }
230238 }
231239
232240 public dispose ( ) : void {
233241 this . #disposed = true ;
234242 this . #authListenerDisposable?. dispose ( ) ;
235243 this . #authConfigDisposable?. dispose ( ) ;
236244 this . #crossWindowSyncDisposable?. dispose ( ) ;
245+ this . #onDidChangeWorkspaceSession. dispose ( ) ;
237246 }
238247
239248 /**
@@ -242,19 +251,19 @@ export class DeploymentManager implements vscode.Disposable {
242251 * Also handles recovery from suspended session state.
243252 */
244253 private registerAuthListener ( ) : void {
245- if ( ! this . #deployment) {
254+ if ( ! this . #session . deployment ) {
246255 return ;
247256 }
248257
249258 // Capture hostname at registration time for the guard clause
250- const safeHostname = this . #deployment. safeHostname ;
259+ const safeHostname = this . #session . deployment . safeHostname ;
251260
252261 this . #authListenerDisposable?. dispose ( ) ;
253262 this . logger . debug ( "Registering auth listener for hostname" , safeHostname ) ;
254263 this . #authListenerDisposable = this . secretsManager . onDidChangeSessionAuth (
255264 safeHostname ,
256265 async ( auth ) => {
257- if ( this . #deployment?. safeHostname !== safeHostname ) {
266+ if ( this . #session . deployment ?. safeHostname !== safeHostname ) {
258267 return ;
259268 }
260269
@@ -285,8 +294,7 @@ export class DeploymentManager implements vscode.Disposable {
285294 private async verifyAndUpdateAuthenticatedSession (
286295 deployment : Deployment & { token : string } ,
287296 ) : Promise < void > {
288- const deploymentBefore = this . #deployment;
289- const authStateVersionBefore = this . #authStateVersion;
297+ const sessionBefore = this . #session;
290298 const tempClient = CoderApi . create (
291299 deployment . url ,
292300 deployment . token ,
@@ -295,11 +303,7 @@ export class DeploymentManager implements vscode.Disposable {
295303
296304 try {
297305 const user = await tempClient . getAuthenticatedUser ( ) ;
298- if (
299- this . #disposed ||
300- this . #deployment !== deploymentBefore ||
301- this . #authStateVersion !== authStateVersionBefore
302- ) {
306+ if ( this . #disposed || this . #session !== sessionBefore ) {
303307 return ;
304308 }
305309 await this . setDeployment ( { ...deployment , user } ) ;
@@ -310,14 +314,30 @@ export class DeploymentManager implements vscode.Disposable {
310314 }
311315 }
312316
313- private commitDeploymentState (
317+ private setSignedIn (
318+ deployment : Deployment ,
319+ user : User ,
320+ ) : DeploymentSessionSnapshot {
321+ this . #session = {
322+ kind : "signedIn" ,
323+ revision : this . #session. revision + 1 ,
324+ deployment,
325+ user,
326+ } ;
327+ this . #onDidChangeWorkspaceSession. fire ( this . getSnapshot ( ) ) ;
328+ return this . #session;
329+ }
330+
331+ private setSignedOut (
314332 deployment : Deployment | null ,
315- authedUser : User | null ,
316- ) : Deployment | null {
317- this . #deployment = deployment ;
318- this . #authedUser = authedUser ;
319- this . #authStateVersion += 1 ;
320- return this . #deployment;
333+ ) : DeploymentSessionSnapshot {
334+ this . #session = {
335+ kind : "signedOut" ,
336+ revision : this . #session. revision + 1 ,
337+ deployment,
338+ } ;
339+ this . #onDidChangeWorkspaceSession. fire ( this . getSnapshot ( ) ) ;
340+ return this . #session;
321341 }
322342
323343 private subscribeToAuthConfigChanges ( ) : void {
@@ -343,7 +363,7 @@ export class DeploymentManager implements vscode.Disposable {
343363 try {
344364 do {
345365 this . #recoveryPending = false ;
346- const snapshot = this . #deployment;
366+ const snapshot = this . #session . deployment ;
347367 if ( this . #disposed || ! snapshot || this . isAuthenticated ( ) ) {
348368 return ;
349369 }
@@ -387,15 +407,6 @@ export class DeploymentManager implements vscode.Disposable {
387407 this . contextManager . set ( "coder.isOwner" , isOwner ) ;
388408 }
389409
390- /**
391- * Refresh all workspace providers asynchronously.
392- */
393- private refreshWorkspaces ( ) : void {
394- for ( const provider of this . workspaceProviders ) {
395- void provider . fetchAndRefresh ( ) ;
396- }
397- }
398-
399410 /**
400411 * Persist deployment to storage for cross-window sync.
401412 */
0 commit comments