@@ -115,6 +115,50 @@ export class GitHubCache {
115115 return `repo:${ owner } /${ repo } :${ type } ${ strArgs } ` ;
116116 }
117117
118+ /**
119+ * An abstraction over general processing that:
120+ * 1. tries getting stuff from Redis cache
121+ * 2. calls the promise to get new data if no value is found in cache
122+ * 3. store this new value back in the cache with an optional TTL before returning the value.
123+ *
124+ * @private
125+ */
126+ #processCached< RType extends Parameters < InstanceType < typeof Redis > [ "json" ] [ "set" ] > [ 2 ] > ( ) {
127+ /**
128+ * Inner currying function to circumvent unsupported partial inference
129+ *
130+ * @param cacheKey the cache key to fetch Redis with
131+ * @param promise the promise to call to get new data if the cache is empty
132+ * @param transformer the function that transforms the return from the promise to the target return value
133+ * @param ttl the optional TTL to use for the newly cached data
134+ *
135+ * @see {@link https://github.com/microsoft/TypeScript/issues/26242|Partial type inference discussion }
136+ */
137+ return async < PromiseType > (
138+ cacheKey : string ,
139+ promise : ( ) => Promise < PromiseType > ,
140+ transformer : ( from : Awaited < PromiseType > ) => RType | Promise < RType > ,
141+ ttl : number | undefined = undefined
142+ ) : Promise < RType > => {
143+ const cachedValue = await this . #redis. json . get < RType > ( cacheKey ) ;
144+ if ( cachedValue ) {
145+ console . log ( `Cache hit for ${ cacheKey } ` ) ;
146+ return cachedValue ;
147+ }
148+
149+ console . log ( `Cache miss for ${ cacheKey } ` ) ;
150+
151+ const newValue = await transformer ( await promise ( ) ) ;
152+
153+ await this . #redis. json . set ( cacheKey , "$" , newValue ) ;
154+ if ( ttl !== undefined ) {
155+ await this . #redis. expire ( cacheKey , ttl ) ;
156+ }
157+
158+ return newValue ;
159+ } ;
160+ }
161+
118162 /**
119163 * Get the item (issue or pr) with the given information.
120164 * Return the appropriate value if the type is defined or
@@ -167,28 +211,21 @@ export class GitHubCache {
167211 * @throws Error if the issue is not found
168212 */
169213 async getIssueDetails ( owner : string , repo : string , id : number ) {
170- const cacheKey = this . #getRepoKey( owner , repo , "issue" , id ) ;
171-
172- const cachedDetails = await this . #redis. json . get < IssueDetails > ( cacheKey ) ;
173- if ( cachedDetails ) {
174- console . log ( `Cache hit for issue details for ${ cacheKey } ` ) ;
175- return cachedDetails ;
176- }
177-
178- console . log ( `Cache miss for issue details for ${ cacheKey } , fetching from the GitHub API` ) ;
179-
180- const [ { data : info } , { data : comments } , linkedPrs ] = await Promise . all ( [
181- this . #octokit. rest . issues . get ( { owner, repo, issue_number : id } ) ,
182- this . #octokit. rest . issues . listComments ( { owner, repo, issue_number : id } ) ,
183- this . #getLinkedPullRequests( owner , repo , id )
184- ] ) ;
185-
186- const details : IssueDetails = { info, comments, linkedPrs } ;
187-
188- await this . #redis. json . set ( cacheKey , "$" , details ) ;
189- await this . #redis. expire ( cacheKey , FULL_DETAILS_TTL ) ;
190-
191- return details ;
214+ return await this . #processCached< IssueDetails > ( ) (
215+ this . #getRepoKey( owner , repo , "issue" , id ) ,
216+ ( ) =>
217+ Promise . all ( [
218+ this . #octokit. rest . issues . get ( { owner, repo, issue_number : id } ) ,
219+ this . #octokit. rest . issues . listComments ( { owner, repo, issue_number : id } ) ,
220+ this . #getLinkedPullRequests( owner , repo , id )
221+ ] ) ,
222+ ( [ { data : info } , { data : comments } , linkedPrs ] ) => ( {
223+ info,
224+ comments,
225+ linkedPrs
226+ } ) ,
227+ FULL_DETAILS_TTL
228+ ) ;
192229 }
193230
194231 /**
@@ -201,32 +238,25 @@ export class GitHubCache {
201238 * @throws Error if the PR is not found
202239 */
203240 async getPullRequestDetails ( owner : string , repo : string , id : number ) {
204- const cacheKey = this . #getRepoKey( owner , repo , "pr" , id ) ;
205-
206- const cachedDetails = await this . #redis. json . get < PullRequestDetails > ( cacheKey ) ;
207- if ( cachedDetails ) {
208- console . log ( `Cache hit for PR details for ${ cacheKey } ` ) ;
209- return cachedDetails ;
210- }
211-
212- console . log ( `Cache miss for PR details for ${ id } , fetching from the GitHub API` ) ;
213-
214- const [ { data : info } , { data : comments } , { data : commits } , { data : files } , linkedIssues ] =
215- await Promise . all ( [
216- this . #octokit. rest . pulls . get ( { owner, repo, pull_number : id } ) ,
217- this . #octokit. rest . issues . listComments ( { owner, repo, issue_number : id } ) ,
218- this . #octokit. rest . pulls . listCommits ( { owner, repo, pull_number : id } ) ,
219- this . #octokit. rest . pulls . listFiles ( { owner, repo, pull_number : id } ) ,
220- this . #getLinkedIssues( owner , repo , id )
221- ] ) ;
222-
223- const details : PullRequestDetails = { info, comments, commits, files, linkedIssues } ;
224-
225- // Cache the result
226- await this . #redis. json . set ( cacheKey , "$" , details ) ;
227- await this . #redis. expire ( cacheKey , FULL_DETAILS_TTL ) ;
228-
229- return details ;
241+ return await this . #processCached< PullRequestDetails > ( ) (
242+ this . #getRepoKey( owner , repo , "pr" , id ) ,
243+ ( ) =>
244+ Promise . all ( [
245+ this . #octokit. rest . pulls . get ( { owner, repo, pull_number : id } ) ,
246+ this . #octokit. rest . issues . listComments ( { owner, repo, issue_number : id } ) ,
247+ this . #octokit. rest . pulls . listCommits ( { owner, repo, pull_number : id } ) ,
248+ this . #octokit. rest . pulls . listFiles ( { owner, repo, pull_number : id } ) ,
249+ this . #getLinkedIssues( owner , repo , id )
250+ ] ) ,
251+ ( [ { data : info } , { data : comments } , { data : commits } , { data : files } , linkedIssues ] ) => ( {
252+ info,
253+ comments,
254+ commits,
255+ files,
256+ linkedIssues
257+ } ) ,
258+ FULL_DETAILS_TTL
259+ ) ;
230260 }
231261
232262 /**
@@ -364,22 +394,12 @@ export class GitHubCache {
364394 * @returns the releases, either cached or fetched
365395 */
366396 async getReleases ( repository : Repository ) {
367- const cacheKey = this . #getRepoKey( repository . owner , repository . repoName , "releases" ) ;
368-
369- const cachedReleases = await this . #redis. json . get < GitHubRelease [ ] > ( cacheKey ) ;
370- if ( cachedReleases ) {
371- console . log ( `Cache hit for releases for ${ cacheKey } ` ) ;
372- return cachedReleases ;
373- }
374-
375- console . log ( `Cache miss for releases for ${ cacheKey } , fetching from GitHub API` ) ;
376-
377- const releases = await this . #fetchReleases( repository ) ;
378-
379- await this . #redis. json . set ( cacheKey , "$" , releases ) ;
380- await this . #redis. expire ( cacheKey , RELEASES_TTL ) ;
381-
382- return releases ;
397+ return await this . #processCached< GitHubRelease [ ] > ( ) (
398+ this . #getRepoKey( repository . owner , repository . repoName , "releases" ) ,
399+ ( ) => this . #fetchReleases( repository ) ,
400+ releases => releases ,
401+ RELEASES_TTL
402+ ) ;
383403 }
384404
385405 /**
@@ -520,90 +540,51 @@ export class GitHubCache {
520540 * @private
521541 */
522542 async getDescriptions ( owner : string , repo : string ) {
523- const cacheKey = this . #getRepoKey( owner , repo , "descriptions" ) ;
524-
525- const cachedDescriptions = await this . #redis. json . get < { [ key : string ] : string } > ( cacheKey ) ;
526- if ( cachedDescriptions ) {
527- console . log ( `Cache hit for descriptions for ${ cacheKey } ` ) ;
528- return cachedDescriptions ;
529- }
530-
531- console . log ( `Cache miss for releases for ${ cacheKey } , fetching from GitHub API` ) ;
532-
533- const { data : allFiles } = await this . #octokit. rest . git . getTree ( {
534- owner,
535- repo,
536- tree_sha : "HEAD" ,
537- recursive : "true"
538- } ) ;
539-
540- const allPackageJson = allFiles . tree
541- . map ( ( { path } ) => path )
542- . filter ( path => path !== undefined )
543- . filter (
544- path =>
545- ! path . includes ( "/test/" ) &&
546- ! path . includes ( "/e2e-tests/" ) &&
547- ( path === "package.json" || path . endsWith ( "/package.json" ) )
548- ) ;
549-
550- const descriptions = new Map < string , string > ( ) ;
551- for ( const path of allPackageJson ) {
552- const { data : packageJson } = await this . #octokit. rest . repos . getContent ( {
553- owner,
554- repo,
555- path
556- } ) ;
557-
558- if ( ! ( "content" in packageJson ) ) continue ; // filter out empty or multiple results
559- const { content, encoding, type } = packageJson ;
560- if ( type !== "file" || ! content ) continue ; // filter out directories and empty files
561- const packageFile =
562- encoding === "base64" ? Buffer . from ( content , "base64" ) . toString ( ) : content ;
563-
564- try {
565- const { description } = JSON . parse ( packageFile ) as { description : string } ;
566- if ( description ) descriptions . set ( path , description ) ;
567- } catch {
568- // ignore
569- }
570- }
571-
572- await this . #redis. json . set ( cacheKey , "$" , Object . fromEntries ( descriptions ) ) ;
573- await this . #redis. expire ( cacheKey , DESCRIPTIONS_TTL ) ;
574-
575- return Object . fromEntries ( descriptions ) ;
576- }
577-
578- /**
579- * Checks if releases are present in the cache for the
580- * given GitHub info
581- *
582- * @param owner the owner of the GitHub repository to check the
583- * existence in the cache for
584- * @param repo the name of the GitHub repository to check the
585- * existence in the cache for
586- * @param type the kind of cache to target
587- * @returns whether the repository is cached or not
588- */
589- async exists ( owner : string , repo : string , type : KeyType ) {
590- const cacheKey = this . #getRepoKey( owner , repo , type ) ;
591- const result = await this . #redis. exists ( cacheKey ) ;
592- return result === 1 ;
593- }
543+ return await this . #processCached< { [ key : string ] : string } > ( ) (
544+ this . #getRepoKey( owner , repo , "descriptions" ) ,
545+ ( ) =>
546+ this . #octokit. rest . git . getTree ( {
547+ owner,
548+ repo,
549+ tree_sha : "HEAD" ,
550+ recursive : "true"
551+ } ) ,
552+ async ( { data : allFiles } ) => {
553+ const allPackageJson = allFiles . tree
554+ . map ( ( { path } ) => path )
555+ . filter ( path => path !== undefined )
556+ . filter (
557+ path =>
558+ ! path . includes ( "/test/" ) &&
559+ ! path . includes ( "/e2e-tests/" ) &&
560+ ( path === "package.json" || path . endsWith ( "/package.json" ) )
561+ ) ;
594562
595- /**
596- * Delete a repository from the cache
597- *
598- * @param owner the owner of the GitHub repository to remove
599- * from the cache
600- * @param repo the name of the GitHub repository to remove
601- * from the cache
602- * @param type the kind of cache to target
603- */
604- async deleteEntry ( owner : string , repo : string , type : KeyType ) {
605- const cacheKey = this . #getRepoKey( owner , repo , type ) ;
606- await this . #redis. del ( cacheKey ) ;
563+ const descriptions = new Map < string , string > ( ) ;
564+ for ( const path of allPackageJson ) {
565+ const { data : packageJson } = await this . #octokit. rest . repos . getContent ( {
566+ owner,
567+ repo,
568+ path
569+ } ) ;
570+
571+ if ( ! ( "content" in packageJson ) ) continue ; // filter out empty or multiple results
572+ const { content, encoding, type } = packageJson ;
573+ if ( type !== "file" || ! content ) continue ; // filter out directories and empty files
574+ const packageFile =
575+ encoding === "base64" ? Buffer . from ( content , "base64" ) . toString ( ) : content ;
576+
577+ try {
578+ const { description } = JSON . parse ( packageFile ) as { description : string } ;
579+ if ( description ) descriptions . set ( path , description ) ;
580+ } catch {
581+ // ignore
582+ }
583+ }
584+ return Object . fromEntries ( descriptions ) ;
585+ } ,
586+ DESCRIPTIONS_TTL
587+ ) ;
607588 }
608589}
609590
0 commit comments