Skip to content

Commit 702cd7d

Browse files
committed
feat: [W-17969984] add getServiceWithWait
@W-17969984@
1 parent d1295ab commit 702cd7d

File tree

8 files changed

+265
-149
lines changed

8 files changed

+265
-149
lines changed

package.json

+2-1
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,7 @@
1111
"url": "https://github.com/forcedotcom/salesforcedx-vscode-service-provider"
1212
},
1313
"dependencies": {
14+
"async-lock": "^1.4.1",
1415
"semver": "^7.7.1"
1516
},
1617
"devDependencies": {
@@ -46,7 +47,7 @@
4647
"lib/src"
4748
],
4849
"scripts": {
49-
"test": "jest --coverage",
50+
"test": "jest --coverage --runInBand",
5051
"build": "shx rm -rf lib && tsc -b",
5152
"commit-init": "commitizen init cz-conventional-changelog --save-dev --save-exact --force",
5253
"commit": "git-cz",

src/extensions/extensionInstaller.ts

-29
This file was deleted.

src/extensions/extensionManager.ts

+78-43
Original file line numberDiff line numberDiff line change
@@ -7,44 +7,50 @@
77
import * as vscode from 'vscode';
88
import {
99
ExtensionState,
10+
normalizeWaitOptions,
1011
ServiceProviders,
1112
serviceTypeToProvider,
1213
ServiceWaitResult,
1314
WaitOptions
1415
} from '../types';
15-
import ExtensionInstaller from './extensionInstaller';
16+
// Below import has to be required for bundling
17+
// eslint-disable-next-line @typescript-eslint/no-var-requires, @typescript-eslint/no-unsafe-assignment
18+
const AsyncLock = require('async-lock');
1619

1720
export default class ExtensionManager {
1821
private static instance: ExtensionManager;
1922
private readonly extensionStates: Map<
2023
ServiceProviders,
2124
vscode.Extension<unknown> | undefined
2225
>;
26+
private lock = new AsyncLock();
2327

2428
private constructor() {
2529
this.extensionStates = new Map<
2630
ServiceProviders,
2731
vscode.Extension<unknown> | undefined
2832
>();
29-
this.initializeExtensionStates();
30-
vscode.extensions.onDidChange(this.handleExtensionChange.bind(this));
33+
this.setupExtensionChangeHandler();
3134
}
3235

33-
public static getInstance(): ExtensionManager {
36+
public static async getInstance(): Promise<ExtensionManager> {
3437
if (!ExtensionManager.instance) {
3538
ExtensionManager.instance = new ExtensionManager();
39+
await ExtensionManager.instance.initializeExtensionStates();
3640
}
3741
return ExtensionManager.instance;
3842
}
3943

40-
private initializeExtensionStates() {
41-
this.extensionStates.clear();
42-
const processedProviders = new Set(Object.values(serviceTypeToProvider));
44+
protected async initializeExtensionStates(): Promise<void> {
45+
await this.lock.acquire(this.extensionStates, () => {
46+
this.extensionStates.clear();
47+
const processedProviders = new Set(Object.values(serviceTypeToProvider));
4348

44-
processedProviders.forEach((provider) => {
45-
const extension = vscode.extensions.getExtension(provider);
46-
this.extensionStates.set(provider, extension);
47-
processedProviders.add(provider);
49+
processedProviders.forEach((provider) => {
50+
const extension = vscode.extensions.getExtension(provider);
51+
this.extensionStates.set(provider, extension);
52+
processedProviders.add(provider);
53+
});
4854
});
4955
}
5056

@@ -75,69 +81,98 @@ export default class ExtensionManager {
7581

7682
public static async waitForExtensionToBecomeActive(
7783
provider: ServiceProviders,
78-
options: WaitOptions = {}
84+
options: WaitOptions = {
85+
timeout: 60_000,
86+
waitInterval: 100,
87+
throwOnTimeout: true
88+
}
7989
): Promise<ServiceWaitResult> {
80-
const {
81-
timeout = 30_000,
82-
waitInterval = 100,
83-
waitTimeUntilForceActivate = 30_000,
84-
forceActivate = false,
85-
install = false,
86-
throwOnTimeout = true
87-
} = options;
88-
89-
const extensionManager = ExtensionManager.getInstance();
90-
let state: ExtensionState = 'NotInstalled';
91-
const start = Date.now();
90+
const { timeout, waitInterval, forceActivate, throwOnTimeout } =
91+
normalizeWaitOptions(options);
92+
93+
this.validateWaitOptions(timeout, waitInterval);
9294

95+
const extensionManager = await ExtensionManager.getInstance();
96+
let state: ExtensionState = 'Unavailable';
97+
let interval: NodeJS.Timeout;
9398
const checkExtensionState = (): Promise<ServiceWaitResult> => {
9499
return new Promise((resolve) => {
95-
const interval = setInterval(async () => {
100+
interval = setInterval(async () => {
96101
const extension = extensionManager.getExtensionState(provider);
97102
if (extension) {
98103
if (extension.isActive) {
99104
clearInterval(interval);
100105
resolve({
101106
success: true,
102107
message: `Extension ${provider} is active.`,
103-
state: 'InstalledActive'
108+
state: 'Active'
104109
});
105110
}
106-
state = 'InstalledInactive';
107-
if (
108-
forceActivate &&
109-
Date.now() - start >= waitTimeUntilForceActivate
110-
) {
111-
await extension.activate();
112-
}
113-
} else if (install) {
114-
await ExtensionInstaller.installExtension(provider);
111+
state = 'Inactive';
115112
} else {
116113
clearInterval(interval);
117114
resolve({
118115
success: false,
119-
message: `Extension ${provider} is not installed.`,
120-
state: 'NotInstalled'
116+
message: `Extension ${provider} is not installed or disabled.`,
117+
state: 'Unavailable'
121118
});
122119
}
123120
}, waitInterval);
124121
});
125122
};
126123

127-
const timeoutPromise = new Promise<ServiceWaitResult>((_, reject) =>
128-
setTimeout(() => {
124+
clearInterval(interval);
125+
126+
const timeoutPromise = new Promise<ServiceWaitResult>((resolve, reject) =>
127+
setTimeout(async () => {
129128
const errorMessage = `Extension ${provider} did not become active within ${timeout}ms`;
130-
if (throwOnTimeout) {
129+
if (forceActivate) {
130+
const extension = extensionManager.getExtensionState(provider);
131+
if (extension) {
132+
try {
133+
await extension.activate();
134+
resolve({
135+
success: true,
136+
message: `Extension ${provider} is active.`,
137+
state: 'Active'
138+
});
139+
} catch (error) {
140+
reject(error);
141+
}
142+
} else {
143+
reject(
144+
new Error(`Extension ${provider} is not installed or disabled.`)
145+
);
146+
}
147+
} else if (throwOnTimeout) {
131148
reject(new Error(errorMessage));
132149
} else {
133-
Promise.resolve({ success: false, message: errorMessage, state });
150+
resolve({ success: false, message: errorMessage, state });
134151
}
135152
}, timeout)
136153
);
137154

138155
return Promise.race([checkExtensionState(), timeoutPromise]);
139156
}
140-
public refresh() {
141-
this.initializeExtensionStates();
157+
158+
private static validateWaitOptions(timeout: number, waitInterval: number) {
159+
if (timeout < 0) {
160+
throw new Error('Timeout must be a positive number');
161+
}
162+
if (waitInterval < 0) {
163+
throw new Error('waitInterval must be a positive number');
164+
}
165+
166+
if (timeout < waitInterval) {
167+
throw new Error('Timeout must be greater than or equal to waitInterval');
168+
}
169+
}
170+
171+
public refresh(): Promise<void> {
172+
return this.initializeExtensionStates();
173+
}
174+
175+
private setupExtensionChangeHandler() {
176+
vscode.extensions.onDidChange(this.handleExtensionChange.bind(this));
142177
}
143178
}

src/extensions/index.ts

-1
Original file line numberDiff line numberDiff line change
@@ -5,4 +5,3 @@
55
* For full license text, see LICENSE.txt file in the repo root or https://opensource.org/licenses/BSD-3-Clause
66
*/
77
export { default as ExtensionManager } from './extensionManager';
8-
export { default as ExtensionInstaller } from './extensionInstaller';

0 commit comments

Comments
 (0)