Skip to content

Conversation

shouples
Copy link
Contributor

@shouples shouples commented May 20, 2025

Summary of Changes

ℹ️ Click for background info

Back in the early days of this extension's development, I was annoyed that most extension examples showed the ExtensionContext being passed around to various functions in order to call things like globalState/workspaceState/secrets, so I put together the StorageManager singleton class as an easy way of working with extension state.

Fast forward a bit and we added some basic get/set logic for the ExtensionContext, which was then used in various places when we had to access the package.json info, static root, etc.

Eventually, we started noticing that StorageManager was missing some methods and event listeners, and it was lacking compared to interacting with the Memento (globalState/workspaceState) and SecretStorage (secrets) interfaces directly, so some parts of the codebase just started using getExtensionContext().___.

On top of that, we started adding the whole ResourceLoader layer to incorporate some caching. At this point we ended up with the following stack for managing storage for our primary resources:

- ResourceLoader (or a connection-specific subclass of it)
  - ResourceManager
    - StorageManager
      - Memento/SecretStorage calls

StorageManager was arguably the weakest layer, and only added to maintenance headaches and complicating the mental model.

This PR gets rid of the entire StorageManager layer we had between the extension and the VS Code Memento/SecretStorage calls.

If I had it the way I originally intended with this branch, it would've meant callers would use getExtensionContext() and work with the ExtensionContext's globalState, workspaceState, and secrets directly, but unfortunately they don't play nicely with sinon stubbing and throw TypeError: Descriptor for property [globalState|workspaceState|secrets] is non-configurable and non-writable errors.

So for a happy middle-ground, we have a few little convenience functions that lend themselves nicely to testing while also pointing directly at Memento/SecretStorage, with an arguably smaller mental model required for use:

/**
* Minimal wrapper around the {@link ExtensionContext}'s
* {@linkcode ExtensionContext.globalState globalState} to allow stubbing {@link Memento} for tests.
*/
export function getGlobalState(): GlobalState {
const context: ExtensionContext = getExtensionContext();
return context.globalState;
}
/**
* Minimal wrapper around the {@link ExtensionContext}'s
* {@linkcode ExtensionContext.workspaceState workspaceState} to allow stubbing {@link Memento} for tests.
*/
export function getWorkspaceState(): WorkspaceState {
const context: ExtensionContext = getExtensionContext();
return context.workspaceState;
}
/**
* Minimal wrapper around the {@link ExtensionContext}'s
* {@linkcode ExtensionContext.secrets secrets} to allow stubbing {@link SecretStorage} for tests.
*/
export function getSecretStorage(): SecretStorage {
const context: ExtensionContext = getExtensionContext();
return context.secrets;
}

However, there were a lot of places dealing with the StorageManager, so many of the other files edited here are updating imports and caller syntax.

Closes #1669.

Any additional details or context that should be provided?

  • We had many places in the tests that were all handling their own StorageManager method stubbing, and I wanted to start pushing for some test-wide convenience functions to help those patterns and get us away from copying and pasting the same kind of stub setup code (as part of Create stubs.ts with helper functions to attach sinon stubs/stubbedInstances/spies/etc to sandboxes #508). As a result, tests/stubs/ was created with extensionStorage.ts to include a few helper functions to get stubbed "instances" of Memento/SecretStorage: getStubbedGlobalState, getStubbedWorkspaceState, and getStubbedSecretStorage. Each of these return fully-stubbed objects that can easily be adjusted per-test/-suite and should help reduce boilerplate. Example of working with it in a test (from storage/migrationManager.test.ts) where we previously had to set up two stubs while only altering the return value of one:
image
  • Since we're working more directly with the Memento/SecretStorage interfaces, some parts of ResourceManager that were previously async calls no longer need to be async (due to StorageManager not quite being a 1:1 replacement, see background section above). This is mainly for the globalState.get/workspaceState.get calls, but the ResourceManager methods using them are left as async because changing them would've meant blowing this branch up even more.
    • To help find those, I added the require-await eslint rule, but then had to comment it out due to it flagging 150+ warnings and failing in CI:
      image

Pull request checklist

Please check if your PR fulfills the following (if applicable):

Tests
  • Added new
  • Updated existing
  • Deleted existing
Other
  • All new disposables (event listeners, views, channels, etc.) collected as for eventual cleanup?
  • Does anything in this PR need to be mentioned in the user-facing CHANGELOG or README?
  • Have you validated this change locally by packaging and installing the extension .vsix file?
    gulp clicktest

shouples added 24 commits May 20, 2025 11:08
@sonarqube-confluent

This comment has been minimized.

@sonarqube-confluent

This comment has been minimized.

1 similar comment
@sonarqube-confluent

This comment has been minimized.

Comment on lines +33 to +39
/** Clears all keys from the `workspaceState` of the {@link ExtensionContext}. */
export async function clearWorkspaceState(): Promise<void> {
const workspaceState: WorkspaceState = getWorkspaceState();
const keys: readonly string[] = workspaceState.keys();
const deletePromises: Thenable<void>[] = keys.map((key) => workspaceState.update(key, undefined));
await Promise.all(deletePromises);
}
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This was the only method from StorageManager worth saving

Comment on lines -113 to +124
const setSecretStub = sandbox.stub(getStorageManager(), "setSecret").resolves();
const setCCloudAuthStatusStub = sandbox
.stub(ResourceManager.getInstance(), "setCCloudAuthStatus")
.resolves();
getCCloudConnectionStub.resolves(TEST_AUTHENTICATED_CCLOUD_CONNECTION);
// authentication completes successfully
browserAuthFlowStub.resolves({ success: true, resetPassword: false });

await authProvider.createSession();

sinon.assert.calledWith(
setSecretStub,
SecretStorageKeys.CCLOUD_AUTH_STATUS,
setCCloudAuthStatusStub,
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Updated to stub the call made more closely to this portion of createSession():

// update the auth status in the secret store so other workspaces can be notified of the change
// and the middleware doesn't get an outdated status before the poller can update it
await getResourceManager().setCCloudAuthStatus(
authenticatedConnection.status.authentication.status,
);

@sonarqube-confluent

This comment has been minimized.

@sonarqube-confluent

This comment has been minimized.

@sonarqube-confluent

This comment has been minimized.

@sonarqube-confluent

This comment has been minimized.

Comment on lines -31 to +32
await getStorageManager().deleteWorkspaceState(WorkspaceStorageKeys.DIFF_BASE_URI);
await workspaceState.update(WorkspaceStorageKeys.DIFF_BASE_URI, undefined);
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@@ -261,7 +260,7 @@ async function _activateExtension(
// set up the local Docker event listener singleton and start watching for system events
EventListener.getInstance().start();
// reset the Docker credentials secret so `src/docker/configs.ts` can pull it fresh
getStorageManager().deleteSecret(SecretStorageKeys.DOCKER_CREDS_SECRET_KEY);
void context.secrets.delete(SecretStorageKeys.DOCKER_CREDS_SECRET_KEY);
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Starting to explicitly add void where we don't want to await/handle the promise here (as part of #1498)

Comment on lines -73 to +77
export type MigrationStorageType = "global" | "workspace" | "secret";
export enum MigrationStorageType {
GLOBAL = "global",
WORKSPACE = "workspace",
SECRET = "secret",
}
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Unrelated to this PR, but made it easier to update test assertions

Comment on lines -57 to -60
async clearGlobalState(): Promise<void> {
const keys = await this.getGlobalStateKeys();
await Promise.all(keys.map((key) => this.deleteGlobalState(key)));
}
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This hasn't been used in months

Comment on lines -36 to -54
type TestMigrationSetup = [
keyof StorageManager,
(manager: StorageManager) => Promise<void>,
MigrationStorageType,
keyof StorageManager,
];
const testSetup: TestMigrationSetup[] = [
["getGlobalState", migrateGlobalState, "global", "setGlobalState"],
["getWorkspaceState", migrateWorkspaceState, "workspace", "setWorkspaceState"],
["getSecret", migrateSecretStorage, "secret", "setSecret"],
];

for (const [
managerGetMethodName,
migrationFunc,
storageType,
managerSetMethodName,
] of testSetup) {
it(`migrate*() for "${storageType}" state/storage should call executeMigrations() when storage version is incorrect`, async () => {
Copy link
Contributor Author

@shouples shouples May 20, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This setup was getting a little too complicated; I figured it was best to split up into explicit tests for easier maintenance

@sonarqube-confluent

This comment has been minimized.

@sonarqube-confluent
Copy link

Passed

Analysis Details

0 Issues

  • Bug 0 Bugs
  • Vulnerability 0 Vulnerabilities
  • Code Smell 0 Code Smells

Coverage and Duplications

  • Coverage 94.00% Coverage (61.60% Estimated after merge)
  • Duplications No duplication information (1.10% Estimated after merge)

Project ID: vscode

View in SonarQube

@shouples shouples marked this pull request as ready for review May 21, 2025 13:03
@shouples shouples requested a review from a team as a code owner May 21, 2025 13:03
@shouples shouples requested a review from Copilot May 21, 2025 13:37
Copy link

@Copilot Copilot AI left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull Request Overview

This PR removes the StorageManager abstraction and shifts all storage interactions to lightweight helpers (getWorkspaceState, getGlobalState, getSecretStorage, etc.), simplifying and streamlining how state and secrets are accessed and tested.

  • Replaced all StorageManager calls with direct state/secret helpers in code and tests
  • Updated migrations and migrationManager to use typed enums and storage utils
  • Refactored tests to stub the new helpers and removed singleton-based stubbing boilerplate

Reviewed Changes

Copilot reviewed 26 out of 26 changed files in this pull request and generated no comments.

Show a summary per file
File Description
src/storage/resourceManager.test.ts Switched from StorageManager to getWorkspaceState/clearWorkspaceState
src/storage/migrations/v2.ts Replaced ExtensionContext.secrets with getSecretStorage()
src/storage/migrations/base.ts Switched store-type strings to MigrationStorageType enum values
src/storage/migrationManager.ts Removed StorageManager param; uses helpers for all storage ops
src/storage/migrationManager.test.ts Refactored tests to stub new helpers; removed singleton manager stubs
src/storage/index.ts Deleted StorageManager class entirely
src/storage/constants.ts Introduced MigrationStorageType enum; cleaned up comments
src/sidecar/sidecarManager.ts Replaced secret storage calls with getSecretStorage()
src/sidecar/middlewares.ts Swapped getExtensionContext().secrets for getSecretStorage()
src/loaders/resourceLoader.test.ts Replaced test activation stubs to use getTestExtensionContext()
src/flinkSql/languageClient.ts Swapped getStorageManager().getSecret for getSecretStorage().get
src/extension.ts Updated extension activation to use setupStorage() helpers
src/extension.test.ts Removed StorageManager from activation tests
src/docker/configs.ts Replaced StorageManager in Docker creds logic with getSecretStorage
src/directConnectManager.ts Swapped secret-change listener to getSecretStorage().onDidChange
src/commands/diffs.ts Replaced workspace-state calls with getWorkspaceState()
src/commands/diffs.test.ts Updated tests to read/write from context.workspaceState
src/authn/ccloudProvider.ts Replaced all secret calls with getSecretStorage()
src/authn/ccloudProvider.test.ts Refactored tests to stub getSecretStorage() and clear state via helper
eslint.config.mjs Commented out require-await rule
Comments suppressed due to low confidence (4)

src/storage/migrationManager.test.ts:11

  • The tests reference CODEBASE_STORAGE_VERSION but it’s not imported, which causes runtime ReferenceErrors. Please add an import for CODEBASE_STORAGE_VERSION (e.g. import { CODEBASE_STORAGE_VERSION } from './migrationManager';).
import { DURABLE_STORAGE_VERSION_KEY, MigrationStorageType } from "./constants";

src/sidecar/sidecarManager.ts:548

  • [nitpick] The variable existing_secret uses snake_case and should be renamed to existingSecret (or token, cachedToken) to follow the project's camelCase naming conventions.
const existing_secret: string | undefined = await getSecretStorage().get(

src/storage/utils.ts:1

  • The new clearWorkspaceState helper isn't covered by unit tests. Adding dedicated tests (e.g. setting and then clearing keys) will ensure it behaves as expected.
export function clearWorkspaceState() {

eslint.config.mjs:36

  • [nitpick] This commented-out ESLint rule adds noise. Consider either removing it or re-enabling require-await by addressing the flagged functions incrementally to enforce consistency.
// "require-await": "warn",

Copy link
Contributor

@jlrobins jlrobins left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Thanks!

@shouples shouples merged commit 7d4ac0a into main May 21, 2025
11 checks passed
@shouples shouples deleted the djs/remove-storagemanager branch May 21, 2025 16:12
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

Successfully merging this pull request may close these issues.

Remove StorageManager entirely and use getExtensionContext() directly as needed
2 participants