Skip to content

Commit adfc6d0

Browse files
committed
refactor: simplify workspace session state
1 parent e22ec87 commit adfc6d0

6 files changed

Lines changed: 890 additions & 310 deletions

File tree

src/deployment/deploymentManager.ts

Lines changed: 93 additions & 82 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,5 @@
1+
import * as vscode from "vscode";
2+
13
import { CoderApi } from "../api/coderApi";
24
import {
35
CONFIG_CHANGE_DEBOUNCE_MS,
@@ -11,7 +13,6 @@ import { type Logger } from "../logging/logger";
1113
import { type OAuthSessionManager } from "../oauth/sessionManager";
1214
import { getAuthConfigWatchSettings } from "../settings/authConfig";
1315
import { type TelemetryService } from "../telemetry/service";
14-
import { type WorkspaceProvider } from "../workspace/workspacesProvider";
1516

1617
import {
1718
DeploymentSchema,
@@ -20,7 +21,24 @@ import {
2021
} from "./types";
2122

2223
import 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

Comments
 (0)