@@ -18,11 +18,6 @@ import { withRetries } from '../shared/utilities/functionUtils'
1818import { FileResourceFetcher } from '../shared/resourcefetcher/fileResourceFetcher'
1919import { 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 */
3833export 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+ }
0 commit comments