Skip to content

Commit f355557

Browse files
authored
Merge pull request aws#6052 from hayemaxi/notifications6
fix(notifications): extension activation hangs if fetch fails
2 parents db9efb7 + 540ce54 commit f355557

File tree

5 files changed

+63
-58
lines changed

5 files changed

+63
-58
lines changed

packages/amazonq/src/extensionNode.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -75,7 +75,7 @@ async function activateAmazonQNode(context: vscode.ExtensionContext) {
7575
...authState,
7676
})
7777

78-
await activateNotifications(context, authState, getAuthState)
78+
void activateNotifications(context, authState, getAuthState)
7979
}
8080

8181
async function getAuthState(): Promise<Omit<AuthUserState, 'source'>> {

packages/core/src/extensionNode.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -246,7 +246,7 @@ export async function activate(context: vscode.ExtensionContext) {
246246
...authState,
247247
})
248248

249-
await activateNotifications(context, authState, getAuthState)
249+
void activateNotifications(context, authState, getAuthState)
250250
} catch (error) {
251251
const stacktrace = (error as Error).stack?.split('\n')
252252
// truncate if the stacktrace is unusually long

packages/core/src/notifications/activation.ts

Lines changed: 17 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -13,6 +13,8 @@ import { AuthState } from './types'
1313
import { getLogger } from '../shared/logger/logger'
1414
import { oneMinute } from '../shared/datetime'
1515

16+
const logger = getLogger('notifications')
17+
1618
/** Time in MS to poll for emergency notifications */
1719
const emergencyPollTime = oneMinute * 10
1820

@@ -33,19 +35,23 @@ export async function activate(
3335
return
3436
}
3537

36-
const panelNode = NotificationsNode.instance
37-
panelNode.registerView(context)
38+
try {
39+
const panelNode = NotificationsNode.instance
40+
panelNode.registerView(context)
3841

39-
const controller = new NotificationsController(panelNode)
40-
const engine = new RuleEngine(await getRuleContext(context, initialState))
42+
const controller = new NotificationsController(panelNode)
43+
const engine = new RuleEngine(await getRuleContext(context, initialState))
4144

42-
await controller.pollForStartUp(engine)
43-
await controller.pollForEmergencies(engine)
45+
await controller.pollForStartUp(engine)
46+
await controller.pollForEmergencies(engine)
4447

45-
globals.clock.setInterval(async () => {
46-
const ruleContext = await getRuleContext(context, await authStateFn())
47-
await controller.pollForEmergencies(new RuleEngine(ruleContext))
48-
}, emergencyPollTime)
48+
globals.clock.setInterval(async () => {
49+
const ruleContext = await getRuleContext(context, await authStateFn())
50+
await controller.pollForEmergencies(new RuleEngine(ruleContext))
51+
}, emergencyPollTime)
4952

50-
getLogger('notifications').debug('Activated in-IDE notifications polling module')
53+
logger.debug('Activated in-IDE notifications polling module')
54+
} catch (err) {
55+
logger.error('Failed to activate in-IDE notifications module.')
56+
}
5157
}

packages/core/src/notifications/controller.ts

Lines changed: 40 additions & 41 deletions
Original file line numberDiff line numberDiff line change
@@ -26,6 +26,8 @@ import { FileResourceFetcher } from '../shared/resourcefetcher/fileResourceFetch
2626
import { isAmazonQ } from '../shared/extensionUtilities'
2727
import { telemetry } from '../shared/telemetry/telemetry'
2828

29+
const logger = getLogger('notifications')
30+
2931
/**
3032
* Handles fetching and maintaining the state of in-IDE notifications.
3133
* Notifications are constantly polled from a known endpoint and then stored in global state.
@@ -39,8 +41,6 @@ import { telemetry } from '../shared/telemetry/telemetry'
3941
* Emergency notifications - fetched at a regular interval.
4042
*/
4143
export class NotificationsController {
42-
public static readonly suggestedPollIntervalMs = 1000 * 60 * 10 // 10 minutes
43-
4444
/** Internal memory state that is written to global state upon modification. */
4545
private readonly state: NotificationsState
4646

@@ -75,7 +75,7 @@ export class NotificationsController {
7575
this.readState()
7676
await this.fetchNotifications(category)
7777
} catch (err: any) {
78-
getLogger('notifications').error(`Unable to fetch %s notifications: %s`, category, err)
78+
logger.error(`Unable to fetch %s notifications: %s`, category, err)
7979
}
8080

8181
await this.displayNotifications(ruleEngine)
@@ -93,25 +93,20 @@ export class NotificationsController {
9393

9494
await NotificationsNode.instance.setNotifications(startUp, emergency)
9595

96-
// Emergency notifications can't be dismissed, but if the user minimizes the panel then
97-
// we don't want to focus it each time we set the notification nodes.
98-
// So we store it in dismissed once a focus has been fired for it.
99-
const newEmergencies = emergency.map((n) => n.id).filter((id) => !dismissed.has(id))
100-
if (newEmergencies.length > 0) {
101-
this.state.dismissed = [...this.state.dismissed, ...newEmergencies]
102-
await this.writeState()
103-
void this.notificationsNode.focusPanel()
104-
}
105-
10696
// Process on-receive behavior for newly received notifications that passes rule engine
107-
const newlyReceivedToDisplay = [...startUp, ...emergency].filter((n) => this.state.newlyReceived.includes(n.id))
108-
if (newlyReceivedToDisplay.length > 0) {
109-
await this.notificationsNode.onReceiveNotifications(newlyReceivedToDisplay)
97+
const wasNewlyReceived = (n: ToolkitNotification) => this.state.newlyReceived.includes(n.id)
98+
const newStartUp = startUp.filter(wasNewlyReceived)
99+
const newEmergency = emergency.filter(wasNewlyReceived)
100+
const newlyReceived = [...newStartUp, ...newEmergency]
101+
102+
if (newlyReceived.length > 0) {
103+
await this.notificationsNode.onReceiveNotifications(newlyReceived)
110104
// remove displayed notifications from newlyReceived
111-
this.state.newlyReceived = this.state.newlyReceived.filter(
112-
(id) => !newlyReceivedToDisplay.some((n) => n.id === id)
113-
)
105+
this.state.newlyReceived = this.state.newlyReceived.filter((id) => !newlyReceived.some((n) => n.id === id))
114106
await this.writeState()
107+
if (newEmergency.length > 0) {
108+
void this.notificationsNode.focusPanel()
109+
}
115110
}
116111
}
117112

@@ -121,7 +116,9 @@ export class NotificationsController {
121116
* hide all notifications.
122117
*/
123118
public async dismissNotification(notificationId: string) {
124-
getLogger('notifications').debug('Dismissing notification: %s', notificationId)
119+
logger.debug('Dismissing notification: %s', notificationId)
120+
121+
this.readState() // Don't overwrite dismissals from other windows
125122
this.state.dismissed.push(notificationId)
126123
await this.writeState()
127124

@@ -134,7 +131,7 @@ export class NotificationsController {
134131
private async fetchNotifications(category: NotificationType) {
135132
const response = await this.fetcher.fetch(category, this.state[category].eTag)
136133
if (!response.content) {
137-
getLogger('notifications').verbose('No new notifications for category: %s', category)
134+
logger.verbose('No new notifications for category: %s', category)
138135
return
139136
}
140137
// Parse the notifications
@@ -149,7 +146,7 @@ export class NotificationsController {
149146
const addedNotifications = newNotifications.filter((n: any) => !currentNotificationIds.has(n.id))
150147

151148
if (addedNotifications.length > 0) {
152-
getLogger('notifications').verbose(
149+
logger.verbose(
153150
'New notifications received for category %s, ids: %s',
154151
category,
155152
addedNotifications.map((n: any) => n.id).join(', ')
@@ -161,7 +158,7 @@ export class NotificationsController {
161158
this.state[category].eTag = response.eTag
162159
await this.writeState()
163160

164-
getLogger('notifications').verbose(
161+
logger.verbose(
165162
"Fetched notifications JSON for category '%s' with schema version: %s. There were %d notifications.",
166163
category,
167164
this.state[category].payload?.schemaVersion,
@@ -173,7 +170,7 @@ export class NotificationsController {
173170
* Write the latest memory state to global state.
174171
*/
175172
private async writeState() {
176-
getLogger('notifications').debug('NotificationsController: Updating notifications state at %s', this.storageKey)
173+
logger.debug('NotificationsController: Updating notifications state at %s', this.storageKey)
177174

178175
// Clean out anything in 'dismissed' that doesn't exist anymore.
179176
const notifications = new Set(
@@ -199,7 +196,7 @@ export class NotificationsController {
199196
*/
200197
private readState() {
201198
const state = this.getDefaultState()
202-
this.state.dismissed = state.dismissed
199+
this.state.dismissed = [...new Set([...this.state.dismissed, ...state.dismissed])]
203200
}
204201

205202
/**
@@ -239,7 +236,7 @@ function registerDismissCommand() {
239236
await NotificationsController.instance.dismissNotification(notification.id)
240237
})
241238
} else {
242-
getLogger('notifications').error(`${name}: Cannot dismiss notification: item is not a vscode.TreeItem`)
239+
logger.error(`${name}: Cannot dismiss notification: item is not a vscode.TreeItem`)
243240
}
244241
})
245242
)
@@ -277,17 +274,23 @@ export class RemoteFetcher implements NotificationFetcher {
277274
const fetcher = new HttpResourceFetcher(endpoint, {
278275
showUrl: true,
279276
})
280-
getLogger('notifications').verbose(
281-
'Attempting to fetch notifications for category: %s at endpoint: %s',
282-
category,
283-
endpoint
277+
logger.verbose('Attempting to fetch notifications for category: %s at endpoint: %s', category, endpoint)
278+
279+
return withRetries(
280+
async () => {
281+
try {
282+
return await fetcher.getNewETagContent(versionTag)
283+
} catch (err) {
284+
logger.error('Failed to fetch at endpoint: %s, err: %s', endpoint, err)
285+
throw err
286+
}
287+
},
288+
{
289+
maxRetries: RemoteFetcher.retryNumber,
290+
delay: RemoteFetcher.retryIntervalMs,
291+
// No exponential backoff - necessary?
292+
}
284293
)
285-
286-
return withRetries(async () => await fetcher.getNewETagContent(versionTag), {
287-
maxRetries: RemoteFetcher.retryNumber,
288-
delay: RemoteFetcher.retryIntervalMs,
289-
// No exponential backoff - necessary?
290-
})
291294
}
292295
}
293296

@@ -309,11 +312,7 @@ export class LocalFetcher implements NotificationFetcher {
309312

310313
async fetch(category: NotificationType, versionTag?: string): Promise<ResourceResponse> {
311314
const uri = category === 'startUp' ? this.startUpLocalPath : this.emergencyLocalPath
312-
getLogger('notifications').verbose(
313-
'Attempting to fetch notifications locally for category: %s at path: %s',
314-
category,
315-
uri
316-
)
315+
logger.verbose('Attempting to fetch notifications locally for category: %s at path: %s', category, uri)
317316

318317
return {
319318
content: await new FileResourceFetcher(globals.context.asAbsolutePath(uri)).get(),

packages/core/src/test/notifications/controller.test.ts

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -160,7 +160,7 @@ describe('Notifications Controller', function () {
160160
payload: content,
161161
eTag,
162162
},
163-
dismissed: [content.notifications[0].id],
163+
dismissed: [],
164164
newlyReceived: ['id:emergency2'],
165165
})
166166
assert.equal(panelNode.startUpNotifications.length, 0)
@@ -218,7 +218,7 @@ describe('Notifications Controller', function () {
218218
payload: emergencyContent,
219219
eTag: eTag2,
220220
},
221-
dismissed: [emergencyContent.notifications[0].id],
221+
dismissed: [],
222222
newlyReceived: ['id:startup2', 'id:emergency2'],
223223
})
224224
assert.equal(panelNode.startUpNotifications.length, 1)
@@ -415,7 +415,7 @@ describe('Notifications Controller', function () {
415415
payload: emergencyContent,
416416
eTag: '1',
417417
},
418-
dismissed: [emergencyContent.notifications[0].id, startUpContent.notifications[0].id],
418+
dismissed: [startUpContent.notifications[0].id],
419419
newlyReceived: [],
420420
})
421421

@@ -438,7 +438,7 @@ describe('Notifications Controller', function () {
438438
payload: emergencyContent,
439439
eTag: '1',
440440
},
441-
dismissed: [emergencyContent.notifications[0].id],
441+
dismissed: [],
442442
newlyReceived: [],
443443
})
444444
assert.equal(panelNode.getChildren().length, 1)

0 commit comments

Comments
 (0)