Skip to content

Commit 1a5b6b6

Browse files
committed
refactor to fetcher implementations
1 parent a620ccb commit 1a5b6b6

File tree

5 files changed

+210
-164
lines changed

5 files changed

+210
-164
lines changed

packages/core/src/notifications/controller.ts

Lines changed: 79 additions & 48 deletions
Original file line numberDiff line numberDiff line change
@@ -18,11 +18,6 @@ import { withRetries } from '../shared/utilities/functionUtils'
1818
import { FileResourceFetcher } from '../shared/resourcefetcher/fileResourceFetcher'
1919
import { isAmazonQ } from '../shared/extensionUtilities'
2020

21-
const startUpEndpoint = 'https://idetoolkits-hostedfiles.amazonaws.com/Notifications/VSCode/startup/1.x.json'
22-
const emergencyEndpoint = 'https://idetoolkits-hostedfiles.amazonaws.com/Notifications/VSCode/emergency/1.x.json'
23-
24-
type ResourceResponse = Awaited<ReturnType<HttpResourceFetcher['getNewETagContent']>>
25-
2621
/**
2722
* Handles fetching and maintaining the state of in-IDE notifications.
2823
* Notifications are constantly polled from a known endpoint and then stored in global state.
@@ -36,8 +31,6 @@ type ResourceResponse = Awaited<ReturnType<HttpResourceFetcher['getNewETagConten
3631
* Emergency notifications - fetched at a regular interval.
3732
*/
3833
export class NotificationsController {
39-
public static readonly retryNumber = 5
40-
public static readonly retryIntervalMs = 30000
4134
public static readonly suggestedPollIntervalMs = 1000 * 60 * 10 // 10 minutes
4235

4336
public readonly storageKey: globalKey
@@ -47,8 +40,12 @@ export class NotificationsController {
4740

4841
static #instance: NotificationsController | undefined
4942

50-
constructor(private readonly notificationsNode: NotificationsNode) {
43+
constructor(
44+
private readonly notificationsNode: NotificationsNode,
45+
private readonly fetcher: NotificationFetcher = new RemoteFetcher()
46+
) {
5147
if (!NotificationsController.#instance) {
48+
// Register on first creation only.
5249
registerDismissCommand()
5350
}
5451
NotificationsController.#instance = this
@@ -119,7 +116,7 @@ export class NotificationsController {
119116
* Fetch notifications from the endpoint and store them in the global state.
120117
*/
121118
private async fetchNotifications(category: NotificationType) {
122-
const response = _useLocalFiles ? await this.fetchLocally(category) : await this.fetchRemotely(category)
119+
const response = await this.fetcher.fetch(category, this.state[category].eTag)
123120
if (!response.content) {
124121
getLogger('notifications').verbose('No new notifications for category: %s', category)
125122
return
@@ -139,38 +136,6 @@ export class NotificationsController {
139136
)
140137
}
141138

142-
private fetchRemotely(category: NotificationType): Promise<ResourceResponse> {
143-
const fetcher = new HttpResourceFetcher(category === 'startUp' ? startUpEndpoint : emergencyEndpoint, {
144-
showUrl: true,
145-
})
146-
147-
return withRetries(async () => await fetcher.getNewETagContent(this.state[category].eTag), {
148-
maxRetries: NotificationsController.retryNumber,
149-
delay: NotificationsController.retryIntervalMs,
150-
// No exponential backoff - necessary?
151-
})
152-
}
153-
154-
/**
155-
* Fetch notifications from local files.
156-
* Intended development purposes only. In the future, we may support adding notifications
157-
* directly to the codebase.
158-
*/
159-
private async fetchLocally(category: NotificationType): Promise<ResourceResponse> {
160-
if (!_useLocalFiles) {
161-
throw new ToolkitError('fetchLocally: Local file fetching is not enabled.')
162-
}
163-
164-
const uri = category === 'startUp' ? startUpLocalPath : emergencyLocalPath
165-
const content = await new FileResourceFetcher(globals.context.asAbsolutePath(uri)).get()
166-
167-
getLogger('notifications').verbose('Fetched notifications locally for category: %s at path: %s', category, uri)
168-
return {
169-
content,
170-
eTag: 'LOCAL_PATH',
171-
}
172-
}
173-
174139
/**
175140
* Write the latest memory state to global state.
176141
*/
@@ -217,13 +182,79 @@ function registerDismissCommand() {
217182
)
218183
}
219184

185+
export type ResourceResponse = Awaited<ReturnType<HttpResourceFetcher['getNewETagContent']>>
186+
187+
export interface NotificationFetcher {
188+
/**
189+
* Fetch notifications from some source. If there is no (new) data to fetch, then the response's
190+
* content value will be undefined.
191+
*
192+
* @param type typeof NotificationType
193+
* @param versionTag last known version of the data aka ETAG. Can be used to determine if the data changed.
194+
*/
195+
fetch(type: NotificationType, versionTag?: string): Promise<ResourceResponse>
196+
}
197+
198+
export class RemoteFetcher implements NotificationFetcher {
199+
public static readonly retryNumber = 5
200+
public static readonly retryIntervalMs = 30000
201+
202+
private readonly startUpEndpoint: string =
203+
'https://idetoolkits-hostedfiles.amazonaws.com/Notifications/VSCode/startup/1.x.json'
204+
private readonly emergencyEndpoint: string =
205+
'https://idetoolkits-hostedfiles.amazonaws.com/Notifications/VSCode/emergency/1.x.json'
206+
207+
constructor(startUpPath?: string, emergencyPath?: string) {
208+
this.startUpEndpoint = startUpPath ?? this.startUpEndpoint
209+
this.emergencyEndpoint = emergencyPath ?? this.emergencyEndpoint
210+
}
211+
212+
fetch(category: NotificationType, versionTag?: string): Promise<ResourceResponse> {
213+
const endpoint = category === 'startUp' ? this.startUpEndpoint : this.emergencyEndpoint
214+
const fetcher = new HttpResourceFetcher(endpoint, {
215+
showUrl: true,
216+
})
217+
getLogger('notifications').verbose(
218+
'Attempting to fetch notifications for category: %s at endpoint: %s',
219+
category,
220+
endpoint
221+
)
222+
223+
return withRetries(async () => await fetcher.getNewETagContent(versionTag), {
224+
maxRetries: RemoteFetcher.retryNumber,
225+
delay: RemoteFetcher.retryIntervalMs,
226+
// No exponential backoff - necessary?
227+
})
228+
}
229+
}
230+
220231
/**
221-
* For development purposes only.
222-
* Enable this option to test the notifications system locally.
232+
* Can be used when developing locally. This may be expanded at some point to allow notifications
233+
* to be published via github rather than internally.
234+
*
235+
* versionTag (ETAG) is ignored.
223236
*/
224-
const _useLocalFiles = false
225-
export const _useLocalFilesCheck = _useLocalFiles // export for testing
237+
export class LocalFetcher implements NotificationFetcher {
238+
// Paths relative to running extension root folder (e.g. packages/amazonq/).
239+
private readonly startUpLocalPath: string = '../core/src/test/notifications/resources/startup/1.x.json'
240+
private readonly emergencyLocalPath: string = '../core/src/test/notifications/resources/emergency/1.x.json'
241+
242+
constructor(startUpPath?: string, emergencyPath?: string) {
243+
this.startUpLocalPath = startUpPath ?? this.startUpLocalPath
244+
this.emergencyLocalPath = emergencyPath ?? this.emergencyLocalPath
245+
}
226246

227-
// Paths relative to current extension
228-
const startUpLocalPath = '../core/src/test/notifications/resources/startup/1.x.json'
229-
const emergencyLocalPath = '../core/src/test/notifications/resources/emergency/1.x.json'
247+
async fetch(category: NotificationType, versionTag?: string): Promise<ResourceResponse> {
248+
const uri = category === 'startUp' ? this.startUpLocalPath : this.emergencyLocalPath
249+
getLogger('notifications').verbose(
250+
'Attempting to fetch notifications locally for category: %s at path: %s',
251+
category,
252+
uri
253+
)
254+
255+
return {
256+
content: await new FileResourceFetcher(globals.context.asAbsolutePath(uri)).get(),
257+
eTag: 'LOCAL_PATH',
258+
}
259+
}
260+
}

packages/core/src/notifications/types.ts

Lines changed: 12 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -90,20 +90,20 @@ export type NotificationsState = {
9090
}
9191

9292
export const NotificationsStateConstructor: TypeConstructor<NotificationsState> = (v: unknown): NotificationsState => {
93-
if (v && typeof v === 'object' && isNotificationsState(v as Partial<NotificationsState>)) {
94-
return v as NotificationsState
93+
const isNotificationsState = (v: Partial<NotificationsState>): v is NotificationsState => {
94+
const requiredKeys: (keyof NotificationsState)[] = ['startUp', 'emergency', 'dismissed']
95+
return (
96+
requiredKeys.every((key) => key in v) &&
97+
Array.isArray(v.dismissed) &&
98+
typeof v.startUp === 'object' &&
99+
typeof v.emergency === 'object'
100+
)
95101
}
96-
throw new Error('Cannot cast to NotificationsState.')
97-
}
98102

99-
function isNotificationsState(v: Partial<NotificationsState>): v is NotificationsState {
100-
const requiredKeys: (keyof NotificationsState)[] = ['startUp', 'emergency', 'dismissed']
101-
return (
102-
requiredKeys.every((key) => key in v) &&
103-
Array.isArray(v.dismissed) &&
104-
typeof v.startUp === 'object' &&
105-
typeof v.emergency === 'object'
106-
)
103+
if (v && typeof v === 'object' && isNotificationsState(v)) {
104+
return v
105+
}
106+
throw new Error('Cannot cast to NotificationsState.')
107107
}
108108

109109
export type NotificationType = keyof Omit<NotificationsState, 'dismissed'>

packages/core/src/shared/vscode/setContext.ts

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -45,6 +45,9 @@ export type contextKey =
4545
* Use "setContext" only as a last resort, to set flags that are detectable in package.json
4646
* declarations. Do not use it as a general way to store global state (which should be avoided
4747
* anyway).
48+
*
49+
* Warning: vscode context keys/values are NOT isolated to individual extensions. Other extensions
50+
* can read and modify them.
4851
*/
4952
export async function setContext(key: contextKey, val: any): Promise<void> {
5053
// eslint-disable-next-line aws-toolkits/no-banned-usages

0 commit comments

Comments
 (0)