@microsoft/vscode-azext-azureauth 5.1.0 → 5.1.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/CHANGELOG.md CHANGED
@@ -1,5 +1,9 @@
1
1
  # Change Log
2
2
 
3
+ ## 5.1.1 - 2025-10-28
4
+
5
+ * [#2111](https://github.com/microsoft/vscode-azuretools/pull/2111) Same as https://github.com/microsoft/vscode-azuretools/pull/2110 but a better fix.
6
+
3
7
  ## 5.1.0 - 2025-10-27
4
8
 
5
9
  * [#2102](https://github.com/microsoft/vscode-azuretools/pull/2102) Fixes an issue causing infinite event loops especially in https://vscode.dev/azure
@@ -52,73 +52,83 @@ let armSubs;
52
52
  */
53
53
  class VSCodeAzureSubscriptionProvider {
54
54
  logger;
55
- lastSignInEventFired = 0;
56
- suppressSignInEvents = false;
57
- lastSignOutEventFired = 0;
55
+ listenerDisposable;
56
+ onDidSignInEmitter = new vscode.EventEmitter();
57
+ onDidSignOutEmitter = new vscode.EventEmitter();
58
+ lastEventFired = 0;
59
+ suppressEvents = false;
58
60
  priorAccounts;
61
+ accountsRemovedPromise;
59
62
  // So that customers can easily share logs, try to only log PII using trace level
60
63
  constructor(logger) {
61
64
  this.logger = logger;
62
- // Load accounts initially, then onDidChangeSessions can compare against them
63
- void vscode.authentication.getAccounts((0, configuredAzureEnv_1.getConfiguredAuthProviderId)()).then(accounts => {
64
- this.priorAccounts = Array.from(accounts); // The Array.from is to get rid of the readonly marker on the array returned by the API
65
- });
65
+ void this.accountsRemoved(); // Initialize priorAccounts
66
+ }
67
+ dispose() {
68
+ this.listenerDisposable?.dispose();
69
+ this.onDidSignInEmitter.dispose();
70
+ this.onDidSignOutEmitter.dispose();
66
71
  }
67
- // eslint-disable-next-line @typescript-eslint/no-explicit-any
68
72
  onDidSignIn(callback, thisArg, disposables) {
69
- return this.onDidChangeSessions(true, callback, thisArg, disposables);
73
+ this.listenIfNeeded();
74
+ return this.onDidSignInEmitter.event(callback, thisArg, disposables);
70
75
  }
71
- // eslint-disable-next-line @typescript-eslint/no-explicit-any
72
76
  onDidSignOut(callback, thisArg, disposables) {
73
- return this.onDidChangeSessions(false, callback, thisArg, disposables);
77
+ this.listenIfNeeded();
78
+ return this.onDidSignOutEmitter.event(callback, thisArg, disposables);
74
79
  }
75
- // eslint-disable-next-line @typescript-eslint/no-explicit-any
76
- onDidChangeSessions(signIn, callback, thisArg, disposables) {
77
- const isASignInEvent = async () => {
78
- const currentAccounts = Array.from(await vscode.authentication.getAccounts((0, configuredAzureEnv_1.getConfiguredAuthProviderId)())); // The Array.from is to get rid of the readonly marker on the array returned by the API
79
- const priorAccountCount = this.priorAccounts?.length ?? 0;
80
- this.priorAccounts = currentAccounts;
81
- // The only way a sign out happens is if an account is removed entirely from the list of accounts
82
- if (currentAccounts.length === 0 || currentAccounts.length < priorAccountCount) {
83
- return false;
84
- }
85
- return true;
86
- };
87
- const wrappedCallback = () => {
88
- const immediate = setImmediate(() => {
89
- clearImmediate(immediate);
90
- void callback.call(thisArg);
91
- });
92
- };
93
- const disposable = vscode.authentication.onDidChangeSessions(async (e) => {
80
+ listenIfNeeded() {
81
+ if (this.listenerDisposable) {
82
+ return;
83
+ }
84
+ this.listenerDisposable = vscode.authentication.onDidChangeSessions(async (e) => {
94
85
  // Ignore any sign in that isn't for the configured auth provider
95
86
  if (e.provider.id !== (0, configuredAzureEnv_1.getConfiguredAuthProviderId)()) {
96
87
  return;
97
88
  }
98
- if (signIn) {
99
- if (this.suppressSignInEvents || Date.now() < this.lastSignInEventFired + EventDebounce) {
100
- return;
101
- }
102
- else if (await isASignInEvent()) {
103
- this.lastSignInEventFired = Date.now();
104
- wrappedCallback();
105
- }
89
+ if (this.suppressEvents || Date.now() < this.lastEventFired + EventDebounce) {
90
+ return;
91
+ }
92
+ this.lastEventFired = Date.now();
93
+ if (!await this.accountsRemoved()) {
94
+ this.logger?.debug('auth: Firing onDidSignIn event');
95
+ this.onDidSignInEmitter.fire();
106
96
  }
107
97
  else {
108
- if (Date.now() < this.lastSignOutEventFired + EventDebounce) {
109
- return;
110
- }
111
- else if (!await isASignInEvent()) {
112
- this.lastSignOutEventFired = Date.now();
113
- wrappedCallback();
114
- }
98
+ this.logger?.debug('auth: Firing onDidSignOut event');
99
+ this.onDidSignOutEmitter.fire();
115
100
  }
116
101
  });
117
- disposables?.push(disposable);
118
- return disposable;
119
102
  }
120
- dispose() {
121
- // No-op, this class no longer has disposables
103
+ async accountsRemoved() {
104
+ // If there's already an ongoing accountsRemoved operation, return its result
105
+ if (this.accountsRemovedPromise) {
106
+ return this.accountsRemovedPromise;
107
+ }
108
+ // Create a new promise for this operation
109
+ this.accountsRemovedPromise = (async () => {
110
+ try {
111
+ this.suppressEvents = true;
112
+ const currentAccounts = Array.from(await vscode.authentication.getAccounts((0, configuredAzureEnv_1.getConfiguredAuthProviderId)()));
113
+ const priorAccountCount = this.priorAccounts?.length ?? 0;
114
+ this.priorAccounts = currentAccounts;
115
+ // The only way a sign out happens is if an account is removed entirely from the list of accounts
116
+ if (currentAccounts.length === 0 || currentAccounts.length < priorAccountCount) {
117
+ return true;
118
+ }
119
+ return false;
120
+ }
121
+ finally {
122
+ this.suppressEvents = false;
123
+ }
124
+ })();
125
+ try {
126
+ return await this.accountsRemovedPromise;
127
+ }
128
+ finally {
129
+ // Clear the promise when done so future calls can proceed
130
+ this.accountsRemovedPromise = undefined;
131
+ }
122
132
  }
123
133
  /**
124
134
  * Gets a list of tenants available to the user.
@@ -179,7 +189,7 @@ class VSCodeAzureSubscriptionProvider {
179
189
  const allSubscriptions = [];
180
190
  let accountCount; // only used for logging
181
191
  try {
182
- this.suppressSignInEvents = true;
192
+ this.suppressEvents = true;
183
193
  // Get the list of tenants from each account (filtered or all)
184
194
  const accounts = (0, isGetSubscriptionsFilter_1.isGetSubscriptionsAccountFilter)(filter) ? [filter.account] : await vscode.authentication.getAccounts((0, configuredAzureEnv_1.getConfiguredAuthProviderId)());
185
195
  accountCount = accounts.length;
@@ -197,7 +207,7 @@ class VSCodeAzureSubscriptionProvider {
197
207
  }
198
208
  }
199
209
  finally {
200
- this.suppressSignInEvents = false;
210
+ this.suppressEvents = false;
201
211
  }
202
212
  // It's possible that by listing subscriptions in all tenants and the "home" tenant there could be duplicate subscriptions
203
213
  // Thus, we remove duplicate subscriptions. However, if multiple accounts have the same subscription, we keep them.
@@ -260,7 +270,7 @@ class VSCodeAzureSubscriptionProvider {
260
270
  async signIn(tenantId, account) {
261
271
  this.logger?.debug(`auth: Signing in (account="${account?.label ?? 'none'}") (tenantId="${tenantId ?? 'none'}")`);
262
272
  try {
263
- this.suppressSignInEvents = true;
273
+ this.suppressEvents = true;
264
274
  const session = await (0, getSessionFromVSCode_1.getSessionFromVSCode)([], tenantId, {
265
275
  createIfNone: true,
266
276
  // If no account is provided, then clear the session preference which tells VS Code to show the account picker
@@ -270,7 +280,7 @@ class VSCodeAzureSubscriptionProvider {
270
280
  return !!session;
271
281
  }
272
282
  finally {
273
- this.suppressSignInEvents = false;
283
+ this.suppressEvents = false;
274
284
  }
275
285
  }
276
286
  /**
@@ -8,15 +8,19 @@ import type { AzureTenant } from './AzureTenant';
8
8
  */
9
9
  export declare class VSCodeAzureSubscriptionProvider implements AzureSubscriptionProvider, vscode.Disposable {
10
10
  private readonly logger?;
11
- private lastSignInEventFired;
12
- private suppressSignInEvents;
13
- private lastSignOutEventFired;
11
+ private listenerDisposable;
12
+ private readonly onDidSignInEmitter;
13
+ private readonly onDidSignOutEmitter;
14
+ private lastEventFired;
15
+ private suppressEvents;
14
16
  private priorAccounts;
17
+ private accountsRemovedPromise;
15
18
  constructor(logger?: vscode.LogOutputChannel | undefined);
16
- onDidSignIn(callback: () => any, thisArg?: any, disposables?: vscode.Disposable[]): vscode.Disposable;
17
- onDidSignOut(callback: () => any, thisArg?: any, disposables?: vscode.Disposable[]): vscode.Disposable;
18
- private onDidChangeSessions;
19
19
  dispose(): void;
20
+ onDidSignIn(callback: () => unknown, thisArg?: unknown, disposables?: vscode.Disposable[]): vscode.Disposable;
21
+ onDidSignOut(callback: () => unknown, thisArg?: unknown, disposables?: vscode.Disposable[]): vscode.Disposable;
22
+ private listenIfNeeded;
23
+ private accountsRemoved;
20
24
  /**
21
25
  * Gets a list of tenants available to the user.
22
26
  * Use {@link isSignedIn} to check if the user is signed in to a particular tenant.
@@ -16,73 +16,83 @@ let armSubs;
16
16
  */
17
17
  export class VSCodeAzureSubscriptionProvider {
18
18
  logger;
19
- lastSignInEventFired = 0;
20
- suppressSignInEvents = false;
21
- lastSignOutEventFired = 0;
19
+ listenerDisposable;
20
+ onDidSignInEmitter = new vscode.EventEmitter();
21
+ onDidSignOutEmitter = new vscode.EventEmitter();
22
+ lastEventFired = 0;
23
+ suppressEvents = false;
22
24
  priorAccounts;
25
+ accountsRemovedPromise;
23
26
  // So that customers can easily share logs, try to only log PII using trace level
24
27
  constructor(logger) {
25
28
  this.logger = logger;
26
- // Load accounts initially, then onDidChangeSessions can compare against them
27
- void vscode.authentication.getAccounts(getConfiguredAuthProviderId()).then(accounts => {
28
- this.priorAccounts = Array.from(accounts); // The Array.from is to get rid of the readonly marker on the array returned by the API
29
- });
29
+ void this.accountsRemoved(); // Initialize priorAccounts
30
+ }
31
+ dispose() {
32
+ this.listenerDisposable?.dispose();
33
+ this.onDidSignInEmitter.dispose();
34
+ this.onDidSignOutEmitter.dispose();
30
35
  }
31
- // eslint-disable-next-line @typescript-eslint/no-explicit-any
32
36
  onDidSignIn(callback, thisArg, disposables) {
33
- return this.onDidChangeSessions(true, callback, thisArg, disposables);
37
+ this.listenIfNeeded();
38
+ return this.onDidSignInEmitter.event(callback, thisArg, disposables);
34
39
  }
35
- // eslint-disable-next-line @typescript-eslint/no-explicit-any
36
40
  onDidSignOut(callback, thisArg, disposables) {
37
- return this.onDidChangeSessions(false, callback, thisArg, disposables);
41
+ this.listenIfNeeded();
42
+ return this.onDidSignOutEmitter.event(callback, thisArg, disposables);
38
43
  }
39
- // eslint-disable-next-line @typescript-eslint/no-explicit-any
40
- onDidChangeSessions(signIn, callback, thisArg, disposables) {
41
- const isASignInEvent = async () => {
42
- const currentAccounts = Array.from(await vscode.authentication.getAccounts(getConfiguredAuthProviderId())); // The Array.from is to get rid of the readonly marker on the array returned by the API
43
- const priorAccountCount = this.priorAccounts?.length ?? 0;
44
- this.priorAccounts = currentAccounts;
45
- // The only way a sign out happens is if an account is removed entirely from the list of accounts
46
- if (currentAccounts.length === 0 || currentAccounts.length < priorAccountCount) {
47
- return false;
48
- }
49
- return true;
50
- };
51
- const wrappedCallback = () => {
52
- const immediate = setImmediate(() => {
53
- clearImmediate(immediate);
54
- void callback.call(thisArg);
55
- });
56
- };
57
- const disposable = vscode.authentication.onDidChangeSessions(async (e) => {
44
+ listenIfNeeded() {
45
+ if (this.listenerDisposable) {
46
+ return;
47
+ }
48
+ this.listenerDisposable = vscode.authentication.onDidChangeSessions(async (e) => {
58
49
  // Ignore any sign in that isn't for the configured auth provider
59
50
  if (e.provider.id !== getConfiguredAuthProviderId()) {
60
51
  return;
61
52
  }
62
- if (signIn) {
63
- if (this.suppressSignInEvents || Date.now() < this.lastSignInEventFired + EventDebounce) {
64
- return;
65
- }
66
- else if (await isASignInEvent()) {
67
- this.lastSignInEventFired = Date.now();
68
- wrappedCallback();
69
- }
53
+ if (this.suppressEvents || Date.now() < this.lastEventFired + EventDebounce) {
54
+ return;
55
+ }
56
+ this.lastEventFired = Date.now();
57
+ if (!await this.accountsRemoved()) {
58
+ this.logger?.debug('auth: Firing onDidSignIn event');
59
+ this.onDidSignInEmitter.fire();
70
60
  }
71
61
  else {
72
- if (Date.now() < this.lastSignOutEventFired + EventDebounce) {
73
- return;
74
- }
75
- else if (!await isASignInEvent()) {
76
- this.lastSignOutEventFired = Date.now();
77
- wrappedCallback();
78
- }
62
+ this.logger?.debug('auth: Firing onDidSignOut event');
63
+ this.onDidSignOutEmitter.fire();
79
64
  }
80
65
  });
81
- disposables?.push(disposable);
82
- return disposable;
83
66
  }
84
- dispose() {
85
- // No-op, this class no longer has disposables
67
+ async accountsRemoved() {
68
+ // If there's already an ongoing accountsRemoved operation, return its result
69
+ if (this.accountsRemovedPromise) {
70
+ return this.accountsRemovedPromise;
71
+ }
72
+ // Create a new promise for this operation
73
+ this.accountsRemovedPromise = (async () => {
74
+ try {
75
+ this.suppressEvents = true;
76
+ const currentAccounts = Array.from(await vscode.authentication.getAccounts(getConfiguredAuthProviderId()));
77
+ const priorAccountCount = this.priorAccounts?.length ?? 0;
78
+ this.priorAccounts = currentAccounts;
79
+ // The only way a sign out happens is if an account is removed entirely from the list of accounts
80
+ if (currentAccounts.length === 0 || currentAccounts.length < priorAccountCount) {
81
+ return true;
82
+ }
83
+ return false;
84
+ }
85
+ finally {
86
+ this.suppressEvents = false;
87
+ }
88
+ })();
89
+ try {
90
+ return await this.accountsRemovedPromise;
91
+ }
92
+ finally {
93
+ // Clear the promise when done so future calls can proceed
94
+ this.accountsRemovedPromise = undefined;
95
+ }
86
96
  }
87
97
  /**
88
98
  * Gets a list of tenants available to the user.
@@ -143,7 +153,7 @@ export class VSCodeAzureSubscriptionProvider {
143
153
  const allSubscriptions = [];
144
154
  let accountCount; // only used for logging
145
155
  try {
146
- this.suppressSignInEvents = true;
156
+ this.suppressEvents = true;
147
157
  // Get the list of tenants from each account (filtered or all)
148
158
  const accounts = isGetSubscriptionsAccountFilter(filter) ? [filter.account] : await vscode.authentication.getAccounts(getConfiguredAuthProviderId());
149
159
  accountCount = accounts.length;
@@ -161,7 +171,7 @@ export class VSCodeAzureSubscriptionProvider {
161
171
  }
162
172
  }
163
173
  finally {
164
- this.suppressSignInEvents = false;
174
+ this.suppressEvents = false;
165
175
  }
166
176
  // It's possible that by listing subscriptions in all tenants and the "home" tenant there could be duplicate subscriptions
167
177
  // Thus, we remove duplicate subscriptions. However, if multiple accounts have the same subscription, we keep them.
@@ -224,7 +234,7 @@ export class VSCodeAzureSubscriptionProvider {
224
234
  async signIn(tenantId, account) {
225
235
  this.logger?.debug(`auth: Signing in (account="${account?.label ?? 'none'}") (tenantId="${tenantId ?? 'none'}")`);
226
236
  try {
227
- this.suppressSignInEvents = true;
237
+ this.suppressEvents = true;
228
238
  const session = await getSessionFromVSCode([], tenantId, {
229
239
  createIfNone: true,
230
240
  // If no account is provided, then clear the session preference which tells VS Code to show the account picker
@@ -234,7 +244,7 @@ export class VSCodeAzureSubscriptionProvider {
234
244
  return !!session;
235
245
  }
236
246
  finally {
237
- this.suppressSignInEvents = false;
247
+ this.suppressEvents = false;
238
248
  }
239
249
  }
240
250
  /**
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "@microsoft/vscode-azext-azureauth",
3
3
  "author": "Microsoft Corporation",
4
- "version": "5.1.0",
4
+ "version": "5.1.1",
5
5
  "description": "Azure authentication helpers for Visual Studio Code",
6
6
  "tags": [
7
7
  "azure",