@@ -18,6 +18,7 @@ export type GitHubRelease = Awaited<
1818> [ "data" ] [ number ] ;
1919
2020type KeyType = "releases" | "descriptions" | "issue" | "pr" ;
21+ type CachedValue < T > = { value : T ; cache : boolean } ;
2122
2223export type ItemDetails = {
2324 comments : Awaited < ReturnType < Issues [ "listComments" ] > > [ "data" ] ;
@@ -70,6 +71,10 @@ const FULL_DETAILS_TTL = 60 * 60 * 2; // 2 hours
7071 * The TTL of the cached descriptions, in seconds.
7172 */
7273const DESCRIPTIONS_TTL = 60 * 60 * 24 * 10 ; // 10 days
74+ /**
75+ * The TTL for non-deprecated packages, in seconds
76+ */
77+ const DEPRECATIONS_TTL = 60 * 60 * 24 * 2 ; // 2 days
7378
7479/**
7580 * A fetch layer to reach the GitHub API
@@ -115,6 +120,21 @@ export class GitHubCache {
115120 return `repo:${ owner } /${ repo } :${ type } ${ strArgs } ` ;
116121 }
117122
123+ /**
124+ * Generates a Redis key from the passed info.
125+ *
126+ * @param packageName the package name
127+ * @param args the optional additional values to append
128+ * at the end of the key; every element will be interpolated
129+ * in a string
130+ * @returns the pure computed key
131+ * @private
132+ */
133+ #getPackageKey( packageName : string , ...args : unknown [ ] ) {
134+ const strArgs = args . map ( a => `:${ a } ` ) ;
135+ return `package:${ packageName } ${ strArgs } ` ;
136+ }
137+
118138 /**
119139 * An abstraction over general processing that:
120140 * 1. tries getting stuff from Redis cache
@@ -124,6 +144,10 @@ export class GitHubCache {
124144 * @private
125145 */
126146 #processCached< RType extends Parameters < InstanceType < typeof Redis > [ "json" ] [ "set" ] > [ 2 ] > ( ) {
147+ function isCachedValue < T > ( item : T | CachedValue < T > ) : item is CachedValue < T > {
148+ return typeof item === "object" && item !== null && "value" in item && "cache" in item ;
149+ }
150+
127151 /**
128152 * Inner currying function to circumvent unsupported partial inference
129153 *
@@ -137,8 +161,10 @@ export class GitHubCache {
137161 return async < PromiseType > (
138162 cacheKey : string ,
139163 promise : ( ) => Promise < PromiseType > ,
140- transformer : ( from : Awaited < PromiseType > ) => RType | Promise < RType > ,
141- ttl : number | undefined = undefined
164+ transformer : (
165+ from : Awaited < PromiseType >
166+ ) => RType | CachedValue < RType > | Promise < RType > | Promise < CachedValue < RType > > ,
167+ ttl : number | ( ( value : RType ) => number | undefined ) | undefined = undefined
142168 ) : Promise < RType > => {
143169 const cachedValue = await this . #redis. json . get < RType > ( cacheKey ) ;
144170 if ( cachedValue ) {
@@ -148,11 +174,25 @@ export class GitHubCache {
148174
149175 console . log ( `Cache miss for ${ cacheKey } ` ) ;
150176
151- const newValue = await transformer ( await promise ( ) ) ;
177+ let newValue = await transformer ( await promise ( ) ) ;
178+ let wantsCache = true ;
179+ if ( isCachedValue ( newValue ) ) {
180+ wantsCache = newValue . cache ;
181+ newValue = newValue . value ;
182+ }
152183
153- await this . #redis. json . set ( cacheKey , "$" , newValue ) ;
154- if ( ttl !== undefined ) {
155- await this . #redis. expire ( cacheKey , ttl ) ;
184+ if ( wantsCache ) {
185+ await this . #redis. json . set ( cacheKey , "$" , newValue ) ;
186+ if ( ttl !== undefined ) {
187+ if ( typeof ttl === "function" ) {
188+ const ttlResult = ttl ( newValue ) ;
189+ if ( ttlResult !== undefined ) {
190+ await this . #redis. expire ( cacheKey , ttlResult ) ;
191+ }
192+ } else {
193+ await this . #redis. expire ( cacheKey , ttl ) ;
194+ }
195+ }
156196 }
157197
158198 return newValue ;
@@ -537,7 +577,6 @@ export class GitHubCache {
537577 * @param repo the GitHub repository name to fetch the
538578 * descriptions in
539579 * @returns a map of paths to descriptions.
540- * @private
541580 */
542581 async getDescriptions ( owner : string , repo : string ) {
543582 return await this . #processCached< { [ key : string ] : string } > ( ) (
@@ -586,6 +625,35 @@ export class GitHubCache {
586625 DESCRIPTIONS_TTL
587626 ) ;
588627 }
628+
629+ /**
630+ * Get the deprecation state of a package from its name.
631+ *
632+ * @param packageName the name of the package to search
633+ * @returns the deprecation status message if any, `false` otherwise
634+ */
635+ async getPackageDeprecation ( packageName : string ) {
636+ return await this . #processCached< string | false > ( ) (
637+ this . #getPackageKey( packageName , "deprecation" ) ,
638+ async ( ) => {
639+ try {
640+ const res = await fetch ( `https://registry.npmjs.org/${ packageName } /latest` ) ;
641+ if ( res . status !== 200 ) return { } ;
642+ return ( await res . json ( ) ) as { deprecated ?: boolean | string } ;
643+ } catch ( error ) {
644+ console . error ( `Error fetching npmjs.org for package ${ packageName } :` , error ) ;
645+ return { } ;
646+ }
647+ } ,
648+ ( { deprecated } ) => {
649+ if ( deprecated === undefined ) return false ;
650+ if ( typeof deprecated === "boolean" )
651+ return { value : "This package is deprecated" , cache : false } ;
652+ return { value : deprecated || "This package is deprecated" , cache : false } ;
653+ } ,
654+ item => ( item === false ? DEPRECATIONS_TTL : undefined )
655+ ) ;
656+ }
589657}
590658
591659export const gitHubCache = new GitHubCache ( KV_REST_API_URL , KV_REST_API_TOKEN , GITHUB_TOKEN ) ;
0 commit comments