From 80e715bbf3e6eb354a9b6e5e327c732b89df38e3 Mon Sep 17 00:00:00 2001 From: "Justin M. Keyes" Date: Wed, 10 Jul 2024 08:36:23 -0700 Subject: [PATCH] refactor: globalState abstraction --- .../commands/onAcceptance.test.ts | 3 +- .../commands/onInlineAcceptance.test.ts | 3 +- .../service/recommendationHandler.test.ts | 5 +-- .../codewhispererCodeCoverageTracker.test.ts | 5 +-- .../tracker/codewhispererTracker.test.ts | 3 +- .../codewhisperer/util/userGroupUtil.test.ts | 10 ++--- .../core/src/amazonq/util/viewBadgeHandler.ts | 3 +- .../src/awsService/s3/commands/uploadFile.ts | 5 +-- packages/core/src/awsexplorer/activation.ts | 3 +- packages/core/src/codecatalyst/reconnect.ts | 6 +-- .../core/src/codewhisperer/util/authUtil.ts | 2 +- .../src/codewhisperer/util/userGroupUtil.ts | 3 +- packages/core/src/shared/extensionGlobals.ts | 3 +- packages/core/src/shared/globalState.ts | 42 ++++++++++--------- packages/core/src/shared/logger/logger.ts | 5 +-- .../s3/commands/downloadFileAs.test.ts | 2 +- packages/core/src/test/globalSetup.test.ts | 2 + 17 files changed, 50 insertions(+), 55 deletions(-) diff --git a/packages/amazonq/test/unit/codewhisperer/commands/onAcceptance.test.ts b/packages/amazonq/test/unit/codewhisperer/commands/onAcceptance.test.ts index 7507f7be23e..c25d648e37c 100644 --- a/packages/amazonq/test/unit/codewhisperer/commands/onAcceptance.test.ts +++ b/packages/amazonq/test/unit/codewhisperer/commands/onAcceptance.test.ts @@ -8,7 +8,6 @@ import * as vscode from 'vscode' import * as sinon from 'sinon' import { onAcceptance, - userGroupKey, UserGroup, AcceptedSuggestionEntry, session, @@ -80,7 +79,7 @@ describe('onAcceptance', function () { }) it('Should report telemetry that records this user decision event', async function () { - await globals.context.globalState.update(userGroupKey, { + await globals.context.globalState.update('CODEWHISPERER_USER_GROUP', { group: UserGroup.Control, version: extensionVersion, }) diff --git a/packages/amazonq/test/unit/codewhisperer/commands/onInlineAcceptance.test.ts b/packages/amazonq/test/unit/codewhisperer/commands/onInlineAcceptance.test.ts index 1d985c3054d..936349be8df 100644 --- a/packages/amazonq/test/unit/codewhisperer/commands/onInlineAcceptance.test.ts +++ b/packages/amazonq/test/unit/codewhisperer/commands/onInlineAcceptance.test.ts @@ -15,7 +15,6 @@ import { AuthUtil, session, CodeWhispererUserGroupSettings, - userGroupKey, UserGroup, } from 'aws-core-vscode/codewhisperer' import { globals } from 'aws-core-vscode/shared' @@ -58,7 +57,7 @@ describe('onInlineAcceptance', function () { }) it('Should report telemetry that records this user decision event', async function () { - await globals.context.globalState.update(userGroupKey, { + await globals.context.globalState.update('CODEWHISPERER_USER_GROUP', { group: UserGroup.Classifier, version: extensionVersion, }) diff --git a/packages/amazonq/test/unit/codewhisperer/service/recommendationHandler.test.ts b/packages/amazonq/test/unit/codewhisperer/service/recommendationHandler.test.ts index 5274f90d0ad..55360e71e12 100644 --- a/packages/amazonq/test/unit/codewhisperer/service/recommendationHandler.test.ts +++ b/packages/amazonq/test/unit/codewhisperer/service/recommendationHandler.test.ts @@ -16,7 +16,6 @@ import { ConfigurationEntry, RecommendationHandler, CodeWhispererCodeCoverageTracker, - userGroupKey, UserGroup, supplementalContextUtil, } from 'aws-core-vscode/codewhisperer' @@ -115,7 +114,7 @@ describe('recommendationHandler', function () { }) it('should call telemetry function that records a CodeWhisperer service invocation', async function () { - await globals.context.globalState.update(userGroupKey, { + await globals.context.globalState.update('CODEWHISPERER_USER_GROUP', { group: UserGroup.CrossFile, version: extensionVersion, }) @@ -167,7 +166,7 @@ describe('recommendationHandler', function () { }) it('should call telemetry function that records a Empty userDecision event', async function () { - await globals.context.globalState.update(userGroupKey, { + await globals.context.globalState.update('CODEWHISPERER_USER_GROUP', { group: UserGroup.CrossFile, version: extensionVersion, }) diff --git a/packages/amazonq/test/unit/codewhisperer/tracker/codewhispererCodeCoverageTracker.test.ts b/packages/amazonq/test/unit/codewhisperer/tracker/codewhispererCodeCoverageTracker.test.ts index 0dc08dc7f3e..6129856495b 100644 --- a/packages/amazonq/test/unit/codewhisperer/tracker/codewhispererCodeCoverageTracker.test.ts +++ b/packages/amazonq/test/unit/codewhisperer/tracker/codewhispererCodeCoverageTracker.test.ts @@ -11,7 +11,6 @@ import { vsCodeState, TelemetryHelper, AuthUtil, - userGroupKey, UserGroup, CodeWhispererUserGroupSettings, } from 'aws-core-vscode/codewhisperer' @@ -524,7 +523,7 @@ describe('codewhispererCodecoverageTracker', function () { }) it('should emit correct code coverage telemetry in python file', async function () { - await globals.context.globalState.update(userGroupKey, { + await globals.globalState.update('CODEWHISPERER_USER_GROUP', { group: UserGroup.Control, version: extensionVersion, }) @@ -547,7 +546,7 @@ describe('codewhispererCodecoverageTracker', function () { }) it('should emit correct code coverage telemetry when success count = 0', async function () { - await globals.context.globalState.update(userGroupKey, { + await globals.globalState.update('CODEWHISPERER_USER_GROUP', { group: UserGroup.Control, version: extensionVersion, }) diff --git a/packages/amazonq/test/unit/codewhisperer/tracker/codewhispererTracker.test.ts b/packages/amazonq/test/unit/codewhisperer/tracker/codewhispererTracker.test.ts index 456102fbd04..155e89029df 100644 --- a/packages/amazonq/test/unit/codewhisperer/tracker/codewhispererTracker.test.ts +++ b/packages/amazonq/test/unit/codewhisperer/tracker/codewhispererTracker.test.ts @@ -9,7 +9,6 @@ import { assertTelemetryCurried } from 'aws-core-vscode/test' import { AuthUtil, CodeWhispererTracker, - userGroupKey, UserGroup, CodeWhispererUserGroupSettings, } from 'aws-core-vscode/codewhisperer' @@ -96,7 +95,7 @@ describe('codewhispererTracker', function () { }) it('Should call recordCodewhispererUserModification with suggestion event', async function () { - await globals.context.globalState.update(userGroupKey, { + await globals.context.globalState.update('CODEWHISPERER_USER_GROUP', { group: UserGroup.CrossFile, version: extensionVersion, }) diff --git a/packages/amazonq/test/unit/codewhisperer/util/userGroupUtil.test.ts b/packages/amazonq/test/unit/codewhisperer/util/userGroupUtil.test.ts index 32969dd494b..cd0d2da4197 100644 --- a/packages/amazonq/test/unit/codewhisperer/util/userGroupUtil.test.ts +++ b/packages/amazonq/test/unit/codewhisperer/util/userGroupUtil.test.ts @@ -15,9 +15,9 @@ describe('getCodeWhispererUserGroup', function () { }) it('getUserGroup should set the group and version if there is none', async function () { - await globals.context.globalState.update(CodeWhispererConstants.userGroupKey, undefined) + await globals.globalState.update('CODEWHISPERER_USER_GROUP', undefined) - assert.ok(!globals.context.globalState.get(CodeWhispererConstants.userGroupKey)) + assert.ok(!globals.globalState.get('CODEWHISPERER_USER_GROUP')) assert.ok(CodeWhispererUserGroupSettings.getUserGroup()) assert.ok(CodeWhispererUserGroupSettings.instance.version) @@ -30,7 +30,7 @@ describe('getCodeWhispererUserGroup', function () { }) it('should return the same result', async function () { - await globals.context.globalState.update(CodeWhispererConstants.userGroupKey, undefined) + await globals.globalState.update('CODEWHISPERER_USER_GROUP', undefined) const group0 = CodeWhispererUserGroupSettings.getUserGroup() const group1 = CodeWhispererUserGroupSettings.getUserGroup() @@ -44,7 +44,7 @@ describe('getCodeWhispererUserGroup', function () { }) it('should return result stored in the extension context if the plugin version remains the same', async function () { - await globals.context.globalState.update(CodeWhispererConstants.userGroupKey, { + await globals.globalState.update('CODEWHISPERER_USER_GROUP', { group: CodeWhispererConstants.UserGroup.Control, version: extensionVersion, }) @@ -56,7 +56,7 @@ describe('getCodeWhispererUserGroup', function () { }) it('should return different result if the plugin version is not the same', async function () { - await globals.context.globalState.update(CodeWhispererConstants.userGroupKey, { + await globals.globalState.update('CODEWHISPERER_USER_GROUP', { group: CodeWhispererConstants.UserGroup.Control, version: 'fake-extension-version', }) diff --git a/packages/core/src/amazonq/util/viewBadgeHandler.ts b/packages/core/src/amazonq/util/viewBadgeHandler.ts index 16a42580079..1258a7acb94 100644 --- a/packages/core/src/amazonq/util/viewBadgeHandler.ts +++ b/packages/core/src/amazonq/util/viewBadgeHandler.ts @@ -7,7 +7,6 @@ import { window, TreeItem, TreeView, ViewBadge } from 'vscode' import { getLogger } from '../../shared/logger' import globals from '../../shared/extensionGlobals' import { AuthUtil } from '../../codewhisperer/util/authUtil' -import { GlobalState } from '../../shared/globalState' let badgeHelperView: TreeView | undefined @@ -43,7 +42,7 @@ export function changeViewBadge(badge?: ViewBadge) { * Removes the view badge from the badge helper view and prevents it from showing up ever again */ export function deactivateInitialViewBadge() { - GlobalState.instance.tryUpdate('hasAlreadyOpenedAmazonQ', true) + globals.globalState.tryUpdate('hasAlreadyOpenedAmazonQ', true) changeViewBadge() } diff --git a/packages/core/src/awsService/s3/commands/uploadFile.ts b/packages/core/src/awsService/s3/commands/uploadFile.ts index 61270372fd9..491693c4f37 100644 --- a/packages/core/src/awsService/s3/commands/uploadFile.ts +++ b/packages/core/src/awsService/s3/commands/uploadFile.ts @@ -24,7 +24,6 @@ import { CancellationError } from '../../../shared/utilities/timeoutUtils' import { progressReporter } from '../progressReporter' import globals from '../../../shared/extensionGlobals' import { telemetry } from '../../../shared/telemetry/telemetry' -import { GlobalState } from '../../../shared/globalState' export interface FileSizeBytes { /** @@ -102,7 +101,7 @@ export async function uploadFileCommand( }) ) if (node instanceof S3FolderNode) { - GlobalState.instance.tryUpdate('aws.lastUploadedToS3Folder', { + globals.globalState.tryUpdate('aws.lastUploadedToS3Folder', { bucket: node.bucket, folder: node.folder, }) @@ -166,7 +165,7 @@ export async function uploadFileCommand( ) if (bucketResponse.folder) { - GlobalState.instance.tryUpdate('aws.lastUploadedToS3Folder', { + globals.globalState.tryUpdate('aws.lastUploadedToS3Folder', { bucket: bucketResponse.bucket, folder: bucketResponse.folder, }) diff --git a/packages/core/src/awsexplorer/activation.ts b/packages/core/src/awsexplorer/activation.ts index 7166becd157..30f7d6d63f4 100644 --- a/packages/core/src/awsexplorer/activation.ts +++ b/packages/core/src/awsexplorer/activation.ts @@ -28,7 +28,6 @@ import { CodeCatalystRootNode } from '../codecatalyst/explorer' import { CodeCatalystAuthenticationProvider } from '../codecatalyst/auth' import { S3FolderNode } from '../awsService/s3/explorer/s3FolderNode' import { AmazonQNode, refreshAmazonQ, refreshAmazonQRootNode } from '../amazonq/explorer/amazonQTreeNode' -import { GlobalState } from '../shared/globalState' import { activateViewsShared, registerToolView } from './activationShared' import { isExtensionInstalled } from '../shared/utilities' import { CommonAuthViewProvider } from '../login/webview' @@ -53,7 +52,7 @@ export async function activate(args: { }) view.onDidExpandElement(element => { if (element.element instanceof S3FolderNode) { - GlobalState.instance.tryUpdate('aws.lastTouchedS3Folder', { + globals.globalState.tryUpdate('aws.lastTouchedS3Folder', { bucket: element.element.bucket, folder: element.element.folder, }) diff --git a/packages/core/src/codecatalyst/reconnect.ts b/packages/core/src/codecatalyst/reconnect.ts index 6ef0fca38c9..70a6c0b35b7 100644 --- a/packages/core/src/codecatalyst/reconnect.ts +++ b/packages/core/src/codecatalyst/reconnect.ts @@ -18,7 +18,6 @@ import globals from '../shared/extensionGlobals' import { isDevenvVscode } from './utils' import { telemetry } from '../shared/telemetry/telemetry' import { SsoConnection } from '../auth/connection' -import { GlobalState } from '../shared/globalState' const localize = nls.loadMessageBundle() @@ -41,8 +40,7 @@ export function watchRestartingDevEnvs(ctx: ExtContext, authProvider: CodeCataly function handleRestart(conn: SsoConnection, ctx: ExtContext, envId: string | undefined) { if (envId !== undefined) { - const pendingReconnects = - GlobalState.instance.get>('CODECATALYST_RECONNECT') ?? {} + const pendingReconnects = globals.globalState.get>('CODECATALYST_RECONNECT') ?? {} if (envId in pendingReconnects) { const devenv = pendingReconnects[envId] const devenvName = getDevEnvName(devenv.alias, envId) @@ -51,7 +49,7 @@ function handleRestart(conn: SsoConnection, ctx: ExtContext, envId: string | und localize('AWS.codecatalyst.reconnect.success', 'Reconnected to Dev Environment: {0}', devenvName) ) delete pendingReconnects[envId] - GlobalState.instance.tryUpdate('CODECATALYST_RECONNECT', pendingReconnects) + globals.globalState.tryUpdate('CODECATALYST_RECONNECT', pendingReconnects) } } else { getLogger().info('codecatalyst: attempting to poll dev environments') diff --git a/packages/core/src/codewhisperer/util/authUtil.ts b/packages/core/src/codewhisperer/util/authUtil.ts index 3ecb5c898ea..dba513d3d09 100644 --- a/packages/core/src/codewhisperer/util/authUtil.ts +++ b/packages/core/src/codewhisperer/util/authUtil.ts @@ -33,7 +33,7 @@ import { onceChanged, once } from '../../shared/utilities/functionUtils' import { indent } from '../../shared/utilities/textUtilities' import { showReauthenticateMessage } from '../../shared/utilities/messages' import { showAmazonQWalkthroughOnce } from '../../amazonq/onboardingPage/walkthrough' -import { setContext } from '../../shared' +import { setContext } from '../../shared/vscode/setContext' /** Backwards compatibility for connections w pre-chat scopes */ export const codeWhispererCoreScopes = [...scopesCodeWhispererCore] diff --git a/packages/core/src/codewhisperer/util/userGroupUtil.ts b/packages/core/src/codewhisperer/util/userGroupUtil.ts index 10673596ea3..e704e084ef8 100644 --- a/packages/core/src/codewhisperer/util/userGroupUtil.ts +++ b/packages/core/src/codewhisperer/util/userGroupUtil.ts @@ -6,7 +6,6 @@ import { UserGroup } from '../models/constants' import globals from '../../shared/extensionGlobals' import { extensionVersion } from '../../shared/vscode/env' -import { GlobalState } from '../../shared/globalState' export class CodeWhispererUserGroupSettings { private _userGroup: UserGroup | undefined @@ -50,7 +49,7 @@ export class CodeWhispererUserGroupSettings { this._version = extensionVersion this._userGroup = this.guessUserGroup() - GlobalState.instance.tryUpdate('CODEWHISPERER_USER_GROUP', { + globals.globalState.tryUpdate('CODEWHISPERER_USER_GROUP', { group: this._userGroup, version: this._version, }) diff --git a/packages/core/src/shared/extensionGlobals.ts b/packages/core/src/shared/extensionGlobals.ts index a63422d68ff..29cc7eee366 100644 --- a/packages/core/src/shared/extensionGlobals.ts +++ b/packages/core/src/shared/extensionGlobals.ts @@ -146,7 +146,7 @@ export function initialize(context: ExtensionContext, isWeb: boolean = false): T context, clock: copyClock(), didReload: checkDidReload(context), - globalState: GlobalState.instance, + globalState: new GlobalState(context), manifestPaths: {} as ToolkitGlobals['manifestPaths'], visualizationResourcePaths: {} as ToolkitGlobals['visualizationResourcePaths'], isWeb, @@ -170,6 +170,7 @@ export { globals as default } */ interface ToolkitGlobals { readonly context: ExtensionContext + /** Global, shared, mutable, persisted state (survives IDE restart), namespaced to the extension (i.e. not shared with other vscode extensions). */ readonly globalState: GlobalState /** Decides the prefix for package.json extension parameters, e.g. commands, 'setContext' values, etc. */ contextPrefix: string diff --git a/packages/core/src/shared/globalState.ts b/packages/core/src/shared/globalState.ts index 6b77031b6f7..8ff9ad54ea8 100644 --- a/packages/core/src/shared/globalState.ts +++ b/packages/core/src/shared/globalState.ts @@ -4,37 +4,44 @@ */ import * as vscode from 'vscode' -import globals from './extensionGlobals' import { getLogger } from './logger/logger' type globalKey = - | 'gumby.wasQCodeTransformationUsed' - | 'aws.toolkit.amazonq.dismissed' - | 'aws.toolkit.amazonqInstall.dismissed' - | 'hasAlreadyOpenedAmazonQ' + | 'aws.downloadPath' | 'aws.lastTouchedS3Folder' | 'aws.lastUploadedToS3Folder' - | 'aws.downloadPath' + | 'aws.toolkit.amazonq.dismissed' + | 'aws.toolkit.amazonqInstall.dismissed' + // Deprecated/legacy names. New keys should start with "aws.". | 'CODECATALYST_RECONNECT' | 'CODEWHISPERER_USER_GROUP' + | 'gumby.wasQCodeTransformationUsed' + | 'hasAlreadyOpenedAmazonQ' +/** + * Extension-local, shared state which persists after IDE restart. Shared with all instances (or + * tabs, in a web browser) of this extension for a given user, but not visible to other vscode + * extensions. Global state should be avoided, except when absolutely necessary. + * + * This wrapper adds structure and visibility to the vscode `globalState` interface. It also opens + * the door for: + * - validation + * - garbage collection + */ export class GlobalState implements vscode.Memento { - static #instance: GlobalState - static get instance(): GlobalState { - return (this.#instance ??= new GlobalState()) - } + public constructor(private readonly extContext: vscode.ExtensionContext) {} public keys(): readonly string[] { - return globals.context.globalState.keys() + return this.extContext.globalState.keys() } public get(key: globalKey, defaultValue?: T): T | undefined { - return globals.context.globalState.get(key) ?? defaultValue + return this.extContext.globalState.get(key) ?? defaultValue } /** Asynchronously updates globalState, or logs an error on failure. */ public tryUpdate(key: globalKey, value: any): void { - globals.context.globalState.update(key, value).then( + this.extContext.globalState.update(key, value).then( undefined, // TODO: log.debug() ? e => { getLogger().error('GlobalState: failed to set "%s": %s', key, (e as Error).message) @@ -43,13 +50,10 @@ export class GlobalState implements vscode.Memento { } public update(key: globalKey, value: any): Thenable { - return globals.context.globalState.update(key, value) + return this.extContext.globalState.update(key, value) } - public static samAndCfnSchemaDestinationUri() { - return vscode.Uri.joinPath(globals.context.globalStorageUri, 'sam.schema.json') + public samAndCfnSchemaDestinationUri() { + return vscode.Uri.joinPath(this.extContext.globalStorageUri, 'sam.schema.json') } } - -export const globalState = GlobalState.instance -export default globalState diff --git a/packages/core/src/shared/logger/logger.ts b/packages/core/src/shared/logger/logger.ts index 311704da0fc..12cc846fdbd 100644 --- a/packages/core/src/shared/logger/logger.ts +++ b/packages/core/src/shared/logger/logger.ts @@ -4,7 +4,6 @@ */ import * as vscode from 'vscode' -import globals from '../extensionGlobals' const toolkitLoggers: { main: Logger | undefined @@ -202,11 +201,11 @@ export class PerfLog { public constructor(public readonly topic: string) { const log = getLogger() this.log = log - this.start = globals.clock.Date.now() + this.start = performance.now() } public elapsed(): number { - return globals.clock.Date.now() - this.start + return performance.now() - this.start } public done(): void { diff --git a/packages/core/src/test/awsService/s3/commands/downloadFileAs.test.ts b/packages/core/src/test/awsService/s3/commands/downloadFileAs.test.ts index 5866ac000ad..d422b1e9256 100644 --- a/packages/core/src/test/awsService/s3/commands/downloadFileAs.test.ts +++ b/packages/core/src/test/awsService/s3/commands/downloadFileAs.test.ts @@ -52,7 +52,7 @@ describe('downloadFileAsCommand', function () { dialog.accept() }) const outputChannel = new MockOutputChannel() - await globals.context.globalState.update('aws.downloadPath', temp) + await globals.globalState.update('aws.downloadPath', temp) s3.downloadFileStream = sinon.stub().resolves(bufferToStream(Buffer.alloc(16))) diff --git a/packages/core/src/test/globalSetup.test.ts b/packages/core/src/test/globalSetup.test.ts index 72e267e748b..861ee3de270 100644 --- a/packages/core/src/test/globalSetup.test.ts +++ b/packages/core/src/test/globalSetup.test.ts @@ -23,6 +23,7 @@ import { getTestWindow, resetTestWindow } from './shared/vscode/window' import { mapTestErrors, normalizeError, setRunnableTimeout } from './setupUtil' import { TelemetryDebounceInfo } from '../shared/vscode/commands2' import { disableAwsSdkWarning } from '../shared/awsClientBuilder' +import { GlobalState } from '../shared/globalState' disableAwsSdkWarning() @@ -84,6 +85,7 @@ export const mochaHooks = { globals.telemetry.logger.clear() TelemetryDebounceInfo.instance.clear() ;(globals.context as FakeExtensionContext).globalState = new FakeMemento() + ;(globals as any).globalState = new GlobalState(globals.context) await testUtil.closeAllEditors() },